cheatah
Source

tests/benchmarks/crypto_openssl_bench.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// crypto_openssl_bench — cheatah's crypto primitives vs OpenSSL (libcrypto), the de-facto
4// reference implementation. Digests, HMAC, and the two TLS 1.3 AEADs are timed on the same
5// 4 KiB payload (a typical TLS record) so the rows are directly comparable. The OpenSSL
6// comparison rows compile only when libcrypto headers are present (CHEATAH_HAVE_OPENSSL,
7// set by the benchmark CMake when find_package(OpenSSL) succeeds); otherwise only the
8// cheatah rows build, so the benchmark never becomes a hard dependency.
9//
10// Run with the release preset:
11// cmake --build --preset release-benchmarks
12// ./build/release/bin/cheatah_benchmarks --benchmark_filter='Crypto'
13#include <benchmark/benchmark.h>
15#include <string>
16#include <vector>
18#include "bench_labels.hpp"
20#include "aead.hpp"
21#include "hashlib.hpp"
23#ifdef CHEATAH_HAVE_OPENSSL
24#include <openssl/evp.h>
25#include <openssl/hmac.h>
26#endif
28namespace {
30std::string fill(std::size_t n, unsigned seed) {
31 std::string s;
32 s.reserve(n);
33 for (std::size_t i = 0; i < n; ++i) s.push_back(static_cast<char>((i * 131u + seed) & 0xFF));
34 return s;
37const std::string kData = fill(4096, 7); // 4 KiB payload
38const std::string kKey = fill(32, 19); // raw HMAC key
39const std::string kAad = "x-record-header"; // associated data
41// AEAD takes HEX key/nonce. 32-byte key (ChaCha20 / two AES-128 keys), 16-byte AES-128 key,
42// 12-byte nonce. The matching RAW bytes feed OpenSSL's EVP API.
43std::string to_hex(const std::string& raw) {
44 static constexpr char H[] = "0123456789abcdef";
45 std::string o;
46 o.reserve(raw.size() * 2);
47 for (unsigned char c : raw) { o.push_back(H[c >> 4]); o.push_back(H[c & 0xF]); }
48 return o;
50const std::string kKey32 = fill(32, 23);
51const std::string kKey16 = fill(16, 29);
52const std::string kNonce = fill(12, 31);
53const std::string kKey32Hex = to_hex(kKey32);
54const std::string kKey16Hex = to_hex(kKey16);
55const std::string kNonceHex = to_hex(kNonce);
57} // namespace
59// ---------------- SHA-256 ----------------
60static void BM_CryptoSha256_Cheatah(benchmark::State& s) {
61 for (auto _ : s) benchmark::DoNotOptimize(cheatah::hashlib::sha256_digest(kData));
62 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
65// ---------------- SHA-512 ----------------
66static void BM_CryptoSha512_Cheatah(benchmark::State& s) {
67 for (auto _ : s) benchmark::DoNotOptimize(cheatah::hashlib::sha512_digest(kData));
68 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
71// ---------------- HMAC-SHA256 ----------------
72static void BM_CryptoHmacSha256_Cheatah(benchmark::State& s) {
73 for (auto _ : s) benchmark::DoNotOptimize(cheatah::hashlib::hmac_sha256(kKey, kData));
74 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
77// ---------------- ChaCha20-Poly1305 ----------------
78static void BM_CryptoChaCha20Poly1305_Cheatah(benchmark::State& s) {
79 for (auto _ : s)
80 benchmark::DoNotOptimize(
81 cheatah::aead::chacha20poly1305_encrypt(kKey32Hex, kNonceHex, kAad, kData));
82 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
85// The allocation-free form, measured against the row above. Same algorithm and the same code
86// paths — the only difference is that this one neither allocates its result nor assembles a
87// MAC-input buffer, so the gap is exactly the cost those two allocations were adding.
88static void BM_CryptoChaCha20Poly1305_Cheatah_Into(benchmark::State& s) {
89 unsigned char key[32], nonce[12];
90 for (int i = 0; i < 32; ++i) key[i] = static_cast<unsigned char>(i);
91 for (int i = 0; i < 12; ++i) nonce[i] = static_cast<unsigned char>(i);
92 std::vector<unsigned char> out(kData.size() + 16);
93 for (auto _ : s) {
94 benchmark::DoNotOptimize(cheatah::aead::chacha20poly1305_encrypt_into(
95 key, nonce, reinterpret_cast<const unsigned char*>(kAad.data()), kAad.size(),
96 reinterpret_cast<const unsigned char*>(kData.data()), kData.size(), out.data()));
97 }
98 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
101// ---------------- AES-128-GCM ----------------
102static void BM_CryptoAes128Gcm_Cheatah(benchmark::State& s) {
103 for (auto _ : s)
104 benchmark::DoNotOptimize(
105 cheatah::aead::aes128gcm_encrypt(kKey16Hex, kNonceHex, kAad, kData));
106 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
109#ifdef CHEATAH_HAVE_OPENSSL
110namespace {
111// One-shot AEAD encrypt with OpenSSL's EVP interface (ciphertext || 16-byte tag).
112std::string ossl_aead(const EVP_CIPHER* cipher, const std::string& key, const std::string& nonce,
113 const std::string& aad, const std::string& pt) {
114 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
115 std::string out(pt.size() + 16, '\0');
116 int len = 0, total = 0;
117 EVP_EncryptInit_ex(ctx, cipher, nullptr,
118 reinterpret_cast<const unsigned char*>(key.data()),
119 reinterpret_cast<const unsigned char*>(nonce.data()));
120 EVP_EncryptUpdate(ctx, nullptr, &len, reinterpret_cast<const unsigned char*>(aad.data()),
121 static_cast<int>(aad.size()));
122 EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(&out[0]), &len,
123 reinterpret_cast<const unsigned char*>(pt.data()), static_cast<int>(pt.size()));
124 total = len;
125 EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(&out[0]) + total, &len);
126 total += len;
127 EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16,
128 reinterpret_cast<unsigned char*>(&out[0]) + total);
129 EVP_CIPHER_CTX_free(ctx);
130 return out;
132} // namespace
134static void BM_CryptoSha256_OpenSSL(benchmark::State& s) {
135 unsigned char md[32];
136 unsigned int n;
137 for (auto _ : s) {
138 EVP_Digest(kData.data(), kData.size(), md, &n, EVP_sha256(), nullptr);
139 benchmark::DoNotOptimize(md);
140 }
141 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
144static void BM_CryptoSha512_OpenSSL(benchmark::State& s) {
145 unsigned char md[64];
146 unsigned int n;
147 for (auto _ : s) {
148 EVP_Digest(kData.data(), kData.size(), md, &n, EVP_sha512(), nullptr);
149 benchmark::DoNotOptimize(md);
150 }
151 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
154static void BM_CryptoHmacSha256_OpenSSL(benchmark::State& s) {
155 unsigned char md[32];
156 unsigned int n;
157 for (auto _ : s) {
158 HMAC(EVP_sha256(), kKey.data(), static_cast<int>(kKey.size()),
159 reinterpret_cast<const unsigned char*>(kData.data()), kData.size(), md, &n);
160 benchmark::DoNotOptimize(md);
161 }
162 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
165static void BM_CryptoChaCha20Poly1305_OpenSSL(benchmark::State& s) {
166 for (auto _ : s)
167 benchmark::DoNotOptimize(ossl_aead(EVP_chacha20_poly1305(), kKey32, kNonce, kAad, kData));
168 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
171static void BM_CryptoAes128Gcm_OpenSSL(benchmark::State& s) {
172 for (auto _ : s)
173 benchmark::DoNotOptimize(ossl_aead(EVP_aes_128_gcm(), kKey16, kNonce, kAad, kData));
174 s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());
176#endif // CHEATAH_HAVE_OPENSSL
178// ---- registration: each pair back-to-back ------------------------------------------
179//
180// Order matters, and it is the whole reason these registrations are not next to their
181// functions. Google Benchmark runs registrations in order, so when all six cheatah rows
182// were registered first and all five OpenSSL rows after, a row and its twin were measured
183// minutes apart under the gate profile (15 repetitions x 0.5 s). Any clock or thermal
184// drift over that window lands entirely on one side of the ratio — which is exactly the
185// bias the comparison exists to avoid. Registering each pair adjacently puts the two sides
186// seconds apart instead. (--benchmark_enable_random_interleaving then scatters the
187// repetitions, turning what remains into zero-mean noise.)
188CHEATAH_BENCH_LABEL("BM_CryptoSha256", "SHA-256");
189BENCHMARK(BM_CryptoSha256_Cheatah);
190#ifdef CHEATAH_HAVE_OPENSSL
191BENCHMARK(BM_CryptoSha256_OpenSSL);
192#endif
194CHEATAH_BENCH_LABEL("BM_CryptoSha512", "SHA-512");
195BENCHMARK(BM_CryptoSha512_Cheatah);
196#ifdef CHEATAH_HAVE_OPENSSL
197BENCHMARK(BM_CryptoSha512_OpenSSL);
198#endif
200CHEATAH_BENCH_LABEL("BM_CryptoHmacSha256", "HMAC-SHA256");
201BENCHMARK(BM_CryptoHmacSha256_Cheatah);
202#ifdef CHEATAH_HAVE_OPENSSL
203BENCHMARK(BM_CryptoHmacSha256_OpenSSL);
204#endif
206CHEATAH_BENCH_LABEL("BM_CryptoChaCha20Poly1305", "ChaCha20-Poly1305");
207BENCHMARK(BM_CryptoChaCha20Poly1305_Cheatah);
208#ifdef CHEATAH_HAVE_OPENSSL
209BENCHMARK(BM_CryptoChaCha20Poly1305_OpenSSL);
210#endif
212// The allocation-free variant: OURS, not a rival (bench_pairs.hpp reads the side token
213// before the `_Into` variant tag). It has no OpenSSL twin, so it reports on its own.
214CHEATAH_BENCH_LABEL("BM_CryptoChaCha20Poly1305_Into", "ChaCha20-Poly1305 (allocation-free)");
215BENCHMARK(BM_CryptoChaCha20Poly1305_Cheatah_Into);
217CHEATAH_BENCH_LABEL("BM_CryptoAes128Gcm", "AES-128-GCM (AES-NI + PCLMULQDQ)");
218BENCHMARK(BM_CryptoAes128Gcm_Cheatah);
219#ifdef CHEATAH_HAVE_OPENSSL
220BENCHMARK(BM_CryptoAes128Gcm_OpenSSL);
221#endif