Source
stdlib/tests/hashlib_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#include "hashlib.hpp"5
#include <stdexcept>6
#include <string>8
#include <gtest/gtest.h>10
namespace hl = cheatah::hashlib;12
TEST(CheatahHashlib, KnownVectors) {13
// Standard NIST/FIPS-180 SHA-256 test vectors.14
EXPECT_EQ(hl::sha256(""),15
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");16
EXPECT_EQ(hl::sha256("abc"),17
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");18
// 56 bytes — crosses a second 64-byte block (length-padding edge case).19
EXPECT_EQ(hl::sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),20
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");21
}23
TEST(CheatahHashlib, DigestShape) {24
const std::string h = hl::sha256("cheatah");25
EXPECT_EQ(h.size(), 64u); // 64 lowercase hex chars26
for (char c : h) {27
EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "non-hex char: " << c;28
}29
}31
TEST(CheatahHashlib, EmbeddedNulIsHashed) {32
// std::string_view carries the length, so an embedded NUL is part of the input.33
const std::string with_nul("a\0b", 3);34
EXPECT_NE(hl::sha256(with_nul), hl::sha256("a"));35
EXPECT_EQ(hl::sha256(with_nul).size(), 64u);36
}38
TEST(CheatahHashlib, Sha512KnownVectors) {39
// Standard NIST/FIPS-180 SHA-512 test vectors.40
EXPECT_EQ(hl::sha512(""),41
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"42
"47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");43
EXPECT_EQ(hl::sha512("abc"),44
"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a"45
"2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f");46
// 112 bytes — crosses a second 128-byte block (length-padding edge case).47
EXPECT_EQ(hl::sha512("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn"48
"hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu"),49
"8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018"50
"501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909");51
}53
TEST(CheatahHashlib, Sha384KnownVectors) {54
// Standard NIST/FIPS-180 SHA-384 test vectors.55
EXPECT_EQ(hl::sha384(""),56
"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da"57
"274edebfe76f65fbd51ad2f14898b95b");58
EXPECT_EQ(hl::sha384("abc"),59
"cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed"60
"8086072ba1e7cc2358baeca134c825a7");61
// 112 bytes — crosses a second 128-byte block (length-padding edge case).62
EXPECT_EQ(hl::sha384("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn"63
"hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu"),64
"09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712"65
"fcc7c71a557e2db966c3e9fa91746039");66
}68
TEST(CheatahHashlib, Sha384DigestShape) {69
const std::string h = hl::sha384("cheatah");70
EXPECT_EQ(h.size(), 96u); // 96 lowercase hex chars71
for (char c : h)72
EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "non-hex char: " << c;73
}75
TEST(CheatahHashlib, Sha512DigestShape) {76
const std::string h = hl::sha512("cheatah");77
EXPECT_EQ(h.size(), 128u); // 128 lowercase hex chars78
for (char c : h)79
EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "non-hex char: " << c;80
}82
TEST(CheatahHashlib, RawDigestMatchesHex) {83
// The raw digest is exactly the bytes the hex form spells (Python digest()/hexdigest()).84
static const char hexd[] = "0123456789abcdef";85
auto to_hex = [](const std::string& raw) {86
std::string out;87
for (unsigned char b : raw) { out.push_back(hexd[b >> 4]); out.push_back(hexd[b & 0xF]); }88
return out;89
};90
EXPECT_EQ(hl::sha256_digest("abc").size(), 32u);91
EXPECT_EQ(hl::sha384_digest("abc").size(), 48u);92
EXPECT_EQ(hl::sha512_digest("abc").size(), 64u);93
EXPECT_EQ(to_hex(hl::sha256_digest("abc")), hl::sha256("abc"));94
EXPECT_EQ(to_hex(hl::sha384_digest("abc")), hl::sha384("abc"));95
EXPECT_EQ(to_hex(hl::sha512_digest("abc")), hl::sha512("abc"));96
EXPECT_EQ(to_hex(hl::sha256_digest("")), hl::sha256(""));97
EXPECT_EQ(to_hex(hl::sha384_digest("")), hl::sha384(""));98
EXPECT_EQ(to_hex(hl::sha512_digest("")), hl::sha512(""));99
}101
// ---- HMAC + HKDF (added with the TLS crypto work) ----------------------------------103
// RFC 4231 test case 2: HMAC-SHA-256("Jefe", "what do ya want for nothing?").104
TEST(CheatahHashlib, HmacSha256) {105
const std::string mac = cheatah::hashlib::hmac_sha256("Jefe", "what do ya want for nothing?");106
std::string hex;107
static constexpr char kHex[] = "0123456789abcdef";108
for (const char ch : mac) {109
hex.push_back(kHex[static_cast<unsigned char>(ch) >> 4]);110
hex.push_back(kHex[static_cast<unsigned char>(ch) & 0xF]);111
}112
EXPECT_EQ(hex, "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843");113
}115
namespace {116
// Lowercase-hex of a raw byte string (for comparing raw MACs to published hex vectors).117
std::string to_hex(const std::string& raw) {118
static constexpr char kHex[] = "0123456789abcdef";119
std::string out;120
out.reserve(raw.size() * 2);121
for (const char ch : raw) {122
out.push_back(kHex[static_cast<unsigned char>(ch) >> 4]);123
out.push_back(kHex[static_cast<unsigned char>(ch) & 0xF]);124
}125
return out;126
}127
} // namespace129
// RFC 4231 HMAC-SHA-512 known-answer vectors (test cases 1-7), checked byte-for-byte against the130
// published hex digests.131
TEST(CheatahHashlib, HmacSha512) {132
// Case 1: key = 0x0b*20, data = "Hi There".133
EXPECT_EQ(to_hex(hl::hmac_sha512(std::string(20, '\x0b'), "Hi There")),134
"87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde"135
"daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854");136
// Case 2: key = "Jefe", data = "what do ya want for nothing?".137
EXPECT_EQ(to_hex(hl::hmac_sha512("Jefe", "what do ya want for nothing?")),138
"164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea250554"139
"9758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737");140
// Case 3: key = 0xaa*20, data = 0xdd*50.141
EXPECT_EQ(to_hex(hl::hmac_sha512(std::string(20, '\xaa'), std::string(50, '\xdd'))),142
"fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39"143
"bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb");144
// Case 4: key = 0x01..0x19 (25 bytes), data = 0xcd*50.145
std::string key4;146
for (int i = 1; i <= 25; ++i) key4.push_back(static_cast<char>(i));147
EXPECT_EQ(to_hex(hl::hmac_sha512(key4, std::string(50, '\xcd'))),148
"b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3db"149
"a91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd");150
// Case 6: key = 0xaa*131, data = "Test Using Larger Than Block-Size Key - Hash Key First".151
EXPECT_EQ(to_hex(hl::hmac_sha512(std::string(131, '\xaa'),152
"Test Using Larger Than Block-Size Key - Hash Key First")),153
"80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f352"154
"6b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598");155
// Case 7: key = 0xaa*131, longer data.156
EXPECT_EQ(to_hex(hl::hmac_sha512(157
std::string(131, '\xaa'),158
"This is a test using a larger than block-size key and a larger than block-size "159
"data. The key needs to be hashed before being used by the HMAC algorithm.")),160
"e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944"161
"b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58");162
}164
// RFC 4648 section 10 base64 ENCODE vectors, byte-for-byte.165
TEST(CheatahHashlib, Base64KnownVectors) {166
EXPECT_EQ(hl::base64_encode(""), "");167
EXPECT_EQ(hl::base64_encode("f"), "Zg==");168
EXPECT_EQ(hl::base64_encode("fo"), "Zm8=");169
EXPECT_EQ(hl::base64_encode("foo"), "Zm9v");170
EXPECT_EQ(hl::base64_encode("foob"), "Zm9vYg==");171
EXPECT_EQ(hl::base64_encode("fooba"), "Zm9vYmE=");172
EXPECT_EQ(hl::base64_encode("foobar"), "Zm9vYmFy");173
// DECODE is the exact inverse of each vector.174
EXPECT_EQ(hl::base64_decode(""), "");175
EXPECT_EQ(hl::base64_decode("Zg=="), "f");176
EXPECT_EQ(hl::base64_decode("Zm8="), "fo");177
EXPECT_EQ(hl::base64_decode("Zm9v"), "foo");178
EXPECT_EQ(hl::base64_decode("Zm9vYg=="), "foob");179
EXPECT_EQ(hl::base64_decode("Zm9vYmE="), "fooba");180
EXPECT_EQ(hl::base64_decode("Zm9vYmFy"), "foobar");181
}183
TEST(CheatahHashlib, Base64RoundTrip) {184
// Every one of the 256 byte values survives encode->decode unchanged.185
std::string all;186
for (int i = 0; i < 256; ++i) all.push_back(static_cast<char>(i));187
EXPECT_EQ(hl::base64_decode(hl::base64_encode(all)), all);188
// Embedded NULs are preserved (length-carrying string_view).189
const std::string with_nul("a\0b\0\0c", 6);190
EXPECT_EQ(hl::base64_decode(hl::base64_encode(with_nul)), with_nul);191
// A known non-text vector: the 3 bytes 0x14 0xfb 0x9c encode to "FPuc".192
const std::string raw3("\x14\xfb\x9c", 3);193
EXPECT_EQ(hl::base64_encode(raw3), "FPuc");194
EXPECT_EQ(hl::base64_decode("FPuc"), raw3);195
// Decode tolerates embedded whitespace/newlines (PEM-style wrapping).196
EXPECT_EQ(hl::base64_decode("Zm9v\r\nYmFy"), "foobar");197
}199
// Hex ENCODE/DECODE are ONE canonical pair (shared by tls/ed25519/x509) — tested together here.200
TEST(CheatahHashlib, HexRoundTrip) {201
// to_hex: string_view overload — lowercase, no `0x` prefix, no separator.202
EXPECT_EQ(hl::to_hex(""), "");203
EXPECT_EQ(hl::to_hex(std::string("\x00\x0f\xff\x10", 4)), "000fff10");204
// to_hex: the raw (pointer, length) overload agrees with the string_view one.205
const unsigned char raw[] = {0xde, 0xad, 0xbe, 0xef};206
EXPECT_EQ(hl::to_hex(raw, sizeof raw), "deadbeef");207
// from_hex is the exact inverse: every one of the 256 byte values round-trips.208
std::string all;209
for (int i = 0; i < 256; ++i) all.push_back(static_cast<char>(i));210
EXPECT_EQ(hl::from_hex(hl::to_hex(all)), all);211
// Uppercase hex decodes identically to lowercase.212
EXPECT_EQ(hl::from_hex("DEADBEEF"), hl::from_hex("deadbeef"));213
// Fails closed: an odd length and a non-hex character both throw.214
EXPECT_THROW(hl::from_hex("abc"), std::invalid_argument);215
EXPECT_THROW(hl::from_hex("zz"), std::invalid_argument);216
}218
// The strict base64_decode (fail-closed on a bad byte) that X.509 PEM parsing relies on.219
TEST(CheatahHashlib, Base64DecodeStrict) {220
EXPECT_EQ(hl::base64_decode("Zm9v!YmFy"), "foobar"); // lenient: '!' ignored221
EXPECT_EQ(hl::base64_decode("Zm9v!YmFy", /*strict=*/true), ""); // strict: '!' rejected -> ""222
// Strict still skips PEM whitespace and honors '=' padding.223
EXPECT_EQ(hl::base64_decode("Zm9v\r\nYmE=", /*strict=*/true), "fooba");224
}226
// RFC 5869 test case 1 (SHA-256): extract + expand to 42 bytes.227
TEST(CheatahHashlib, HkdfRfc5869Case1) {228
const std::string ikm(22, '\x0b');229
const std::string salt = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,230
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c};231
std::string info;232
for (int i = 0; i < 10; ++i) info.push_back(static_cast<char>(0xf0 + i));233
const std::string prk = cheatah::hashlib::hkdf_extract(salt, ikm);234
const std::string okm = cheatah::hashlib::hkdf_expand(prk, info, 42);235
std::string hex;236
static constexpr char kHex[] = "0123456789abcdef";237
for (const char ch : okm) {238
hex.push_back(kHex[static_cast<unsigned char>(ch) >> 4]);239
hex.push_back(kHex[static_cast<unsigned char>(ch) & 0xF]);240
}241
EXPECT_EQ(hex,242
"3cb25f25faacd57a90434f64d0362f2a"243
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"244
"34007208d5b887185865");245
}247
// The RFC bound: zero or oversized lengths are rejected.248
TEST(CheatahHashlib, HkdfBounds) {249
EXPECT_EQ(cheatah::hashlib::hkdf_expand("prk", "info", 0), "");250
EXPECT_EQ(cheatah::hashlib::hkdf_expand("prk", "info", 255 * 32 + 1), "");251
}