cheatah
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>
10namespace hl = cheatah::hashlib;
12TEST(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");
23TEST(CheatahHashlib, DigestShape) {
24 const std::string h = hl::sha256("cheatah");
25 EXPECT_EQ(h.size(), 64u); // 64 lowercase hex chars
26 for (char c : h) {
27 EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "non-hex char: " << c;
28 }
31TEST(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);
38TEST(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");
53TEST(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");
68TEST(CheatahHashlib, Sha384DigestShape) {
69 const std::string h = hl::sha384("cheatah");
70 EXPECT_EQ(h.size(), 96u); // 96 lowercase hex chars
71 for (char c : h)
72 EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "non-hex char: " << c;
75TEST(CheatahHashlib, Sha512DigestShape) {
76 const std::string h = hl::sha512("cheatah");
77 EXPECT_EQ(h.size(), 128u); // 128 lowercase hex chars
78 for (char c : h)
79 EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "non-hex char: " << c;
82TEST(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(""));
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?").
104TEST(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");
115namespace {
116// Lowercase-hex of a raw byte string (for comparing raw MACs to published hex vectors).
117std::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;
127} // namespace
129// RFC 4231 HMAC-SHA-512 known-answer vectors (test cases 1-7), checked byte-for-byte against the
130// published hex digests.
131TEST(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");
164// RFC 4648 section 10 base64 ENCODE vectors, byte-for-byte.
165TEST(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");
183TEST(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");
199// Hex ENCODE/DECODE are ONE canonical pair (shared by tls/ed25519/x509) — tested together here.
200TEST(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);
218// The strict base64_decode (fail-closed on a bad byte) that X.509 PEM parsing relies on.
219TEST(CheatahHashlib, Base64DecodeStrict) {
220 EXPECT_EQ(hl::base64_decode("Zm9v!YmFy"), "foobar"); // lenient: '!' ignored
221 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");
226// RFC 5869 test case 1 (SHA-256): extract + expand to 42 bytes.
227TEST(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");
247// The RFC bound: zero or oversized lengths are rejected.
248TEST(CheatahHashlib, HkdfBounds) {
249 EXPECT_EQ(cheatah::hashlib::hkdf_expand("prk", "info", 0), "");
250 EXPECT_EQ(cheatah::hashlib::hkdf_expand("prk", "info", 255 * 32 + 1), "");