cheatah
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 system
4// `openssl` CLI over a range of inputs — empty, short, long, a sentence, and ALL 256 byte
5// values — so we validate against an independent reference implementation, not only the
6// fixed standard vectors in hashlib_test.cpp. The whole suite SKIPS when `openssl` is
7// unavailable, so it strengthens assurance where present without becoming a build/runtime
8// dependency. Input bytes go through a temp FILE (never the shell), so arbitrary bytes are
9// 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"
22namespace hl = cheatah::hashlib;
24namespace {
26bool has_openssl() { return std::system("openssl version >/dev/null 2>&1") == 0; }
28// Run @p cmd, capture its stdout.
29std::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;
39// The first whitespace-delimited token — openssl `-r` prints "<hex> *<file>".
40std::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;
49// Strip trailing newlines (openssl base64 ends with one).
50std::string chomp(std::string s) {
51 while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
52 return s;
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.
57std::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;
68// Write @p data to a fresh temp file; returns its path (removed by the caller).
69std::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);
84// The shared input corpus: empty, short, long-repeat, a sentence, and every byte 0x00..0xFF.
85const 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;
95} // namespace
97TEST(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 }
108TEST(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 }
119TEST(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 }
130TEST(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 }
146TEST(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 }
162TEST(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 }
177TEST(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 }