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 an4
// architecture / OS / compiler-specific bug, gathered into one self-contained, self-reporting5
// 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-GCM7
// 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 that10
// 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 any13
// platform fails here regardless of which code path is taken.14
// The random corpus is generated from a FIXED seed, so a failure reproduces identically15
// everywhere. NOTE: results are only "verified" on a platform once this has actually been run16
// 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"27
namespace {28
namespace ae = cheatah::aead;29
namespace hl = cheatah::hashlib;31
// Tiny deterministic PRNG (xorshift64) — reproducible corpus across platforms.32
struct 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
};48
std::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;57
}59
const 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
#else69
return "unknown";70
#endif71
}72
const 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
#else80
return "other";81
#endif82
}83
bool is_x86() {84
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)85
return true;86
#else87
return false;88
#endif89
}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 intrinsics92
// mis-compiled or the self-test rejected the path, and we silently dropped to scalar crypto.93
bool is_apple_silicon() {94
#if defined(__aarch64__) && defined(__APPLE__)95
return true;96
#else97
return false;98
#endif99
}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.103
const 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
} // namespace108
TEST(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 we116
// quietly fell back to scalar crypto. x86 here means AES-NI+PCLMULQDQ; Apple Silicon means117
// the ARMv8 AES+PMULL NEON path (every arm64 Mac has FEAT_AES/FEAT_PMULL). On other ARM118
// 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_LINE125
<< "Apple Silicon build is not using the ARMv8 AES+PMULL crypto path (fell back to scalar)"; // LCOV_EXCL_LINE126
} // LCOV_EXCL_LINE127
SUCCEED();128
}130
// The key cross-architecture check: the hardware path and the portable scalar reference must131
// agree byte-for-byte, and both must round-trip, over the whole size corpus with random inputs.132
TEST(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 decrypt150
ae::set_force_portable_crypto(true);151
EXPECT_EQ(ae::aes128gcm_decrypt(key, nonce, aad, hw), pt); // portable decrypt152
ae::set_force_portable_crypto(false);153
}154
}155
}157
TEST(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
}171
}173
// Canonical known-answer vectors — independent of which code path is taken, so a miscompiled174
// primitive on any platform is caught here too.175
TEST(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" // ciphertext201
"5bc94fbc3221a5db94fae95ae7121a47"; // 16-byte tag202
EXPECT_EQ(to_hex(ct), expected);203
EXPECT_EQ(ae::aes128gcm_decrypt(key, iv, aad, ct), p); // round trip204
}