Source
stdlib/tests/hashlib_openssl_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
// Byte-for-byte cross-check of the hashlib digests / HMAC / base64 against the system4
// `openssl` CLI over a range of inputs — empty, short, long, a sentence, and ALL 256 byte5
// values — so we validate against an independent reference implementation, not only the6
// fixed standard vectors in hashlib_test.cpp. The whole suite SKIPS when `openssl` is7
// unavailable, so it strengthens assurance where present without becoming a build/runtime8
// dependency. Input bytes go through a temp FILE (never the shell), so arbitrary bytes are9
// compared exactly; only ASCII, quote-free HMAC keys appear on the command line.10
#include <gtest/gtest.h>12
#include <unistd.h>14
#include <array>15
#include <cstdio>16
#include <cstdlib>17
#include <string>18
#include <vector>20
#include "hashlib.hpp"22
namespace hl = cheatah::hashlib;24
namespace {26
bool has_openssl() { return std::system("openssl version >/dev/null 2>&1") == 0; }28
// Run @p cmd, capture its stdout.29
std::string capture(const std::string& cmd) {30
std::array<char, 4096> buf{};31
std::string out;32
FILE* p = popen(cmd.c_str(), "r");33
if (!p) return "";34
while (std::fgets(buf.data(), static_cast<int>(buf.size()), p)) out += buf.data();35
pclose(p);36
return out;37
}39
// The first whitespace-delimited token — openssl `-r` prints "<hex> *<file>".40
std::string first_token(const std::string& s) {41
std::string t;42
for (char c : s) {43
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') break;44
t += c;45
}46
return t;47
}49
// Strip trailing newlines (openssl base64 ends with one).50
std::string chomp(std::string s) {51
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();52
return s;53
}55
// Lowercase hex of a raw byte string — hmac_sha256/512 return RAW bytes (by design),56
// while openssl `-r` prints hex, so encode hl's output before comparing.57
std::string to_hex(const std::string& raw) {58
static constexpr char kHex[] = "0123456789abcdef";59
std::string out;60
out.reserve(raw.size() * 2);61
for (unsigned char c : raw) {62
out.push_back(kHex[c >> 4]);63
out.push_back(kHex[c & 0xF]);64
}65
return out;66
}68
// Write @p data to a fresh temp file; returns its path (removed by the caller).69
std::string write_tmp(const std::string& data) {70
char path[] = "/tmp/cheatah_ossl_XXXXXX";71
const int fd = mkstemp(path);72
if (fd >= 0) {73
ssize_t off = 0;74
while (off < static_cast<ssize_t>(data.size())) {75
const ssize_t n = ::write(fd, data.data() + off, data.size() - off);76
if (n <= 0) break;77
off += n;78
}79
::close(fd);80
}81
return std::string(path);82
}84
// The shared input corpus: empty, short, long-repeat, a sentence, and every byte 0x00..0xFF.85
const std::vector<std::string>& inputs() {86
static const std::vector<std::string> v = [] {87
std::string all;88
for (int i = 0; i < 256; ++i) all.push_back(static_cast<char>(i));89
return std::vector<std::string>{"", "abc", std::string(1000, 'a'),90
"The quick brown fox jumps over the lazy dog", all};91
}();92
return v;93
}95
} // namespace97
TEST(HashlibVsOpenssl, Sha256) {98
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";99
for (const auto& in : inputs()) {100
const std::string f = write_tmp(in);101
const std::string ref = first_token(capture("openssl dgst -sha256 -r '" + f + "'"));102
std::remove(f.c_str());103
ASSERT_EQ(ref.size(), 64u) << "openssl output unexpected for input of size " << in.size();104
EXPECT_EQ(hl::sha256(in), ref) << "sha256 mismatch for input of size " << in.size();105
}106
}108
TEST(HashlibVsOpenssl, Sha384) {109
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";110
for (const auto& in : inputs()) {111
const std::string f = write_tmp(in);112
const std::string ref = first_token(capture("openssl dgst -sha384 -r '" + f + "'"));113
std::remove(f.c_str());114
ASSERT_EQ(ref.size(), 96u);115
EXPECT_EQ(hl::sha384(in), ref) << "sha384 mismatch for input of size " << in.size();116
}117
}119
TEST(HashlibVsOpenssl, Sha512) {120
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";121
for (const auto& in : inputs()) {122
const std::string f = write_tmp(in);123
const std::string ref = first_token(capture("openssl dgst -sha512 -r '" + f + "'"));124
std::remove(f.c_str());125
ASSERT_EQ(ref.size(), 128u);126
EXPECT_EQ(hl::sha512(in), ref) << "sha512 mismatch for input of size " << in.size();127
}128
}130
TEST(HashlibVsOpenssl, HmacSha256) {131
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";132
for (const std::string key : {std::string("k"), std::string("secretkey"),133
std::string(40, 'K')}) {134
for (const auto& in : inputs()) {135
const std::string f = write_tmp(in);136
const std::string ref = first_token(137
capture("openssl dgst -sha256 -hmac '" + key + "' -r '" + f + "'"));138
std::remove(f.c_str());139
ASSERT_EQ(ref.size(), 64u);140
EXPECT_EQ(to_hex(hl::hmac_sha256(key, in)), ref)141
<< "hmac-sha256 mismatch (key '" << key << "', input size " << in.size() << ")";142
}143
}144
}146
TEST(HashlibVsOpenssl, HmacSha384) {147
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";148
for (const std::string key : {std::string("k"), std::string("secretkey"),149
std::string(40, 'K')}) {150
for (const auto& in : inputs()) {151
const std::string f = write_tmp(in);152
const std::string ref = first_token(153
capture("openssl dgst -sha384 -hmac '" + key + "' -r '" + f + "'"));154
std::remove(f.c_str());155
ASSERT_EQ(ref.size(), 96u);156
EXPECT_EQ(to_hex(hl::hmac_sha384(key, in)), ref)157
<< "hmac-sha384 mismatch (key '" << key << "', input size " << in.size() << ")";158
}159
}160
}162
TEST(HashlibVsOpenssl, HmacSha512) {163
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";164
for (const std::string key : {std::string("k"), std::string("secretkey")}) {165
for (const auto& in : inputs()) {166
const std::string f = write_tmp(in);167
const std::string ref = first_token(168
capture("openssl dgst -sha512 -hmac '" + key + "' -r '" + f + "'"));169
std::remove(f.c_str());170
ASSERT_EQ(ref.size(), 128u);171
EXPECT_EQ(to_hex(hl::hmac_sha512(key, in)), ref)172
<< "hmac-sha512 mismatch (key '" << key << "', input size " << in.size() << ")";173
}174
}175
}177
TEST(HashlibVsOpenssl, Base64Encode) {178
if (!has_openssl()) GTEST_SKIP() << "openssl CLI not available";179
for (const auto& in : inputs()) {180
const std::string f = write_tmp(in);181
const std::string ref = chomp(capture("openssl base64 -A -in '" + f + "'"));182
std::remove(f.c_str());183
EXPECT_EQ(hl::base64_encode(in), ref) << "base64 mismatch for input of size " << in.size();184
}185
}