cheatah
Source

stdlib/tests/crypto_platform_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// Platform-sensitive crypto validation. These are the checks most likely to expose an
4// architecture / OS / compiler-specific bug, gathered into one self-contained, self-reporting
5// test so the SAME correctness bar can be confirmed on each target (x86-64, ARM/Apple Silicon,
6// …) simply by running the suite there. It prints the detected arch/OS and whether the AES-GCM
7// hardware path is active, then:
8// • cross-checks the hardware path against the portable scalar reference, byte-for-byte,
9// across every block-boundary size with random keys/nonces/AAD/plaintext (the check that
10// a NEW hardware path on a new architecture must pass);
11// • round-trips AES-GCM and ChaCha20-Poly1305 (+ tamper rejection) over the same corpus;
12// • re-verifies canonical NIST/RFC known-answer vectors, so a miscompiled primitive on any
13// platform fails here regardless of which code path is taken.
14// The random corpus is generated from a FIXED seed, so a failure reproduces identically
15// everywhere. NOTE: results are only "verified" on a platform once this has actually been run
16// and passed there — see docs/performance.md ("Where the cryptography is verified").
17#include <gtest/gtest.h>
19#include <cstdint>
20#include <cstdio>
21#include <string>
22#include <vector>
24#include "aead.hpp"
25#include "hashlib.hpp"
27namespace {
28namespace ae = cheatah::aead;
29namespace hl = cheatah::hashlib;
31// Tiny deterministic PRNG (xorshift64) — reproducible corpus across platforms.
32struct Rng {
33 std::uint64_t s;
34 std::uint64_t next() {
35 s ^= s << 13;
36 s ^= s >> 7;
37 s ^= s << 17;
38 return s;
39 }
40 std::size_t below(std::size_t n) { return static_cast<std::size_t>(next() % n); }
41 std::string bytes(std::size_t n) {
42 std::string out(n, '\0');
43 for (char& c : out) c = static_cast<char>(next() & 0xFF);
44 return out;
45 }
46};
48std::string to_hex(const std::string& b) {
49 static constexpr char H[] = "0123456789abcdef";
50 std::string o;
51 o.reserve(b.size() * 2);
52 for (unsigned char c : b) {
53 o.push_back(H[c >> 4]);
54 o.push_back(H[c & 0xF]);
55 }
56 return o;
59const char* arch() {
60#if defined(__aarch64__) || defined(_M_ARM64)
61 return "arm64";
62#elif defined(__arm__) || defined(_M_ARM)
63 return "arm";
64#elif defined(__x86_64__) || defined(_M_X64)
65 return "x86-64";
66#elif defined(__i386__) || defined(_M_IX86)
67 return "x86";
68#else
69 return "unknown";
70#endif
72const char* os_name() {
73#if defined(__APPLE__)
74 return "macOS/Apple";
75#elif defined(_WIN32)
76 return "Windows";
77#elif defined(__linux__)
78 return "Linux";
79#else
80 return "other";
81#endif
83bool is_x86() {
84#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
85 return true;
86#else
87 return false;
88#endif
90// Apple Silicon: every arm64 Mac ships the ARMv8 Cryptography Extension (FEAT_AES/FEAT_PMULL),
91// so the NEON AES + PMULL AES-GCM path MUST be the one that runs — if it isn't, the intrinsics
92// mis-compiled or the self-test rejected the path, and we silently dropped to scalar crypto.
93bool is_apple_silicon() {
94#if defined(__aarch64__) && defined(__APPLE__)
95 return true;
96#else
97 return false;
98#endif
101// Every size that straddles a block (16), the 4-wide (64) and 8-wide (128) group boundaries,
102// plus a few larger buffers — where a CTR/GHASH tail bug would hide.
103const std::vector<std::size_t> kSizes = {0, 1, 15, 16, 17, 31, 32, 63, 64, 65,
104 80, 127, 128, 129, 200, 255, 256, 512, 1000};
106} // namespace
108TEST(CryptoPlatform, Report) {
109 const char* hw = is_apple_silicon() ? "YES (ARMv8 AES + PMULL)" : "YES (AES-NI + PCLMULQDQ)";
110 std::printf("[crypto-platform] arch=%s os=%s | AES-GCM hardware path active: %s\n", arch(),
111 os_name(),
112 ae::crypto_hardware_active() ? hw : "no (portable scalar reference)");
113 std::fflush(stdout);
114 // A build on a platform with guaranteed crypto instructions MUST take the hardware path,
115 // else CPU detection / the target-attributed SIMD path / the self-test is broken and we
116 // quietly fell back to scalar crypto. x86 here means AES-NI+PCLMULQDQ; Apple Silicon means
117 // the ARMv8 AES+PMULL NEON path (every arm64 Mac has FEAT_AES/FEAT_PMULL). On other ARM
118 // targets the extension is not guaranteed, so there it stays informational.
119 if (is_x86()) {
120 EXPECT_TRUE(ae::crypto_hardware_active())
121 << "x86 build is not using the AES-NI/PCLMULQDQ path";
122 }
123 if (is_apple_silicon()) { // arm64-macOS-only branch; the body is dead on the Linux/x86 CI.
124 EXPECT_TRUE(ae::crypto_hardware_active()) // LCOV_EXCL_LINE
125 << "Apple Silicon build is not using the ARMv8 AES+PMULL crypto path (fell back to scalar)"; // LCOV_EXCL_LINE
126 } // LCOV_EXCL_LINE
127 SUCCEED();
130// The key cross-architecture check: the hardware path and the portable scalar reference must
131// agree byte-for-byte, and both must round-trip, over the whole size corpus with random inputs.
132TEST(CryptoPlatform, AesGcmHardwareEqualsPortableAndRoundTrips) {
133 Rng r{0x1234567890abcdefULL};
134 for (std::size_t n : kSizes) {
135 for (int trial = 0; trial < 4; ++trial) {
136 const std::string key = to_hex(r.bytes(16));
137 const std::string nonce = to_hex(r.bytes(12));
138 const std::string aad = r.bytes(r.below(40));
139 const std::string pt = r.bytes(n);
141 ae::set_force_portable_crypto(false);
142 const std::string hw = ae::aes128gcm_encrypt(key, nonce, aad, pt);
143 ae::set_force_portable_crypto(true);
144 const std::string sw = ae::aes128gcm_encrypt(key, nonce, aad, pt);
145 ae::set_force_portable_crypto(false);
147 ASSERT_EQ(hw.size(), pt.size() + 16) << "arch=" << arch() << " size=" << n;
148 ASSERT_EQ(hw, sw) << "hardware != portable, arch=" << arch() << " size=" << n;
149 EXPECT_EQ(ae::aes128gcm_decrypt(key, nonce, aad, hw), pt); // hardware decrypt
150 ae::set_force_portable_crypto(true);
151 EXPECT_EQ(ae::aes128gcm_decrypt(key, nonce, aad, hw), pt); // portable decrypt
152 ae::set_force_portable_crypto(false);
153 }
154 }
157TEST(CryptoPlatform, ChaCha20Poly1305RoundTripAndTamper) {
158 Rng r{0xfeedfacecafebeefULL};
159 for (std::size_t n : kSizes) {
160 const std::string key = to_hex(r.bytes(32));
161 const std::string nonce = to_hex(r.bytes(12));
162 const std::string aad = r.bytes(r.below(40));
163 const std::string pt = r.bytes(n);
164 const std::string ct = ae::chacha20poly1305_encrypt(key, nonce, aad, pt);
165 ASSERT_EQ(ct.size(), pt.size() + 16) << "arch=" << arch() << " size=" << n;
166 EXPECT_EQ(ae::chacha20poly1305_decrypt(key, nonce, aad, ct), pt);
167 std::string tampered = ct;
168 tampered[tampered.size() - 1] = static_cast<char>(tampered.back() ^ 0x01);
169 EXPECT_EQ(ae::chacha20poly1305_decrypt(key, nonce, aad, tampered), "");
170 }
173// Canonical known-answer vectors — independent of which code path is taken, so a miscompiled
174// primitive on any platform is caught here too.
175TEST(CryptoPlatform, KnownAnswerVectors) {
176 // SHA-2 (NIST FIPS 180-4).
177 EXPECT_EQ(hl::sha256("abc"),
178 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
179 EXPECT_EQ(hl::sha512("abc"),
180 "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a"
181 "2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f");
183 // AES-128-GCM (NIST GCM test case 4): K, IV, AAD, P -> C || T.
184 auto unhex = [](const std::string& h) {
185 auto nib = [](char c) { return c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10; };
186 std::string o;
187 for (std::size_t i = 0; i + 1 < h.size(); i += 2)
188 o.push_back(static_cast<char>((nib(h[i]) << 4) | nib(h[i + 1])));
189 return o;
190 };
191 const std::string key = "feffe9928665731c6d6a8f9467308308";
192 const std::string iv = "cafebabefacedbaddecaf888";
193 const std::string aad = unhex("feedfacedeadbeeffeedfacedeadbeefabaddad2");
194 const std::string p = unhex(
195 "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2"
196 "449a6b525b16aedf5aa0de657ba637b39");
197 const std::string ct = ae::aes128gcm_encrypt(key, iv, aad, p);
198 const std::string expected =
199 "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5"
200 "aac84aa051ba30b396a0aac973d58e091" // ciphertext
201 "5bc94fbc3221a5db94fae95ae7121a47"; // 16-byte tag
202 EXPECT_EQ(to_hex(ct), expected);
203 EXPECT_EQ(ae::aes128gcm_decrypt(key, iv, aad, ct), p); // round trip