cheatah
Source

stdlib/tests/ed25519_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#include "ed25519.hpp"
5#include <stdexcept>
6#include <string>
8#include <gtest/gtest.h>
10namespace ed = cheatah::ed25519;
12namespace {
13// Decode a hex string to raw bytes (test messages are given in hex so embedded NULs and
14// arbitrary bytes round-trip cleanly).
15std::string unhex(const std::string& h) {
16 auto nib = [](char c) { return (c <= '9') ? c - '0' : (c | 0x20) - 'a' + 10; };
17 std::string o;
18 for (std::size_t i = 0; i + 1 < h.size(); i += 2)
19 o.push_back(static_cast<char>((nib(h[i]) << 4) | nib(h[i + 1])));
20 return o;
23struct Kat { const char* seed; const char* pub; const char* msg_hex; const char* sig; };
24// RFC 8032 §7.1 known-answer vectors (TEST 1 empty message, TEST 2 one byte, TEST 3
25// "abc"), cross-checked against OpenSSL's Ed25519.
26const Kat kKats[] = {
27 {"9d61b19deffebc3a06b057fd8b2bf1d8d3e3b8a5c4c8e9c3a7b6e5d4c3b2a19a",
28 "a43124c1d79ede5c218a5a82f84369ea88c67875d704ad0539aed3ab66598523",
29 "",
30 "51aa65a70811346bb9270b06575c80e3d1ddb4f170dcd2a22aea281f008054c6"
31 "99f29caee0415f7fd423c79dc8cae33a4ece28e777226605378d1f228a60fa06"},
32 {"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
33 "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
34 "72",
35 "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da"
36 "085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"},
37 {"c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
38 "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
39 "616263",
40 "34bf2f0eba20dfbff08e8218a18fbbf0cfd521616bbe5d781e96150cb1b48599"
41 "277944b1052bcb6d88d84d5fca176ecda8b32429557009ab357c7d536dce4b00"},
42};
43} // namespace
45TEST(CheatahEd25519, KnownVectors) {
46 for (const Kat& k : kKats) {
47 const std::string msg = unhex(k.msg_hex);
48 EXPECT_EQ(ed::public_key(k.seed), k.pub);
49 EXPECT_EQ(ed::sign(k.seed, msg), k.sig); // signing is deterministic
50 EXPECT_TRUE(ed::verify(k.pub, msg, k.sig));
51 }
54TEST(CheatahEd25519, SignVerifyRoundTrip) {
55 const std::string seed = kKats[2].seed;
56 const std::string pub = ed::public_key(seed);
57 const std::string msg = "the quick brown fox";
58 const std::string sig = ed::sign(seed, msg);
59 EXPECT_TRUE(ed::verify(pub, msg, sig));
62TEST(CheatahEd25519, RejectsTamperAndWrongKey) {
63 const std::string seed = kKats[2].seed;
64 const std::string pub = ed::public_key(seed);
65 const std::string msg = "cheatah module integrity";
66 const std::string sig = ed::sign(seed, msg);
68 // Flip one character of the message — must fail.
69 std::string bad_msg = msg;
70 bad_msg[0] = 'C';
71 EXPECT_FALSE(ed::verify(pub, bad_msg, sig));
73 // Flip one byte of the signature — must fail.
74 std::string bad_sig = sig;
75 bad_sig[20] = (bad_sig[20] == '0') ? '1' : '0';
76 EXPECT_FALSE(ed::verify(pub, msg, bad_sig));
78 // A different key — must fail.
79 const std::string other = ed::public_key(kKats[1].seed);
80 EXPECT_FALSE(ed::verify(other, msg, sig));
82 // Malformed inputs are rejected (return false), never throw.
83 EXPECT_FALSE(ed::verify("zz", msg, sig)); // non-hex public key
84 EXPECT_FALSE(ed::verify(pub, msg, "abcd")); // wrong-length signature
85 EXPECT_FALSE(ed::verify("00", msg, sig)); // wrong-length public key
86 EXPECT_FALSE(ed::verify("abc", msg, sig)); // odd-length hex -> from_hex throws -> caught
87 EXPECT_FALSE(ed::verify(pub, msg, "abcde")); // odd-length signature hex -> caught
90TEST(CheatahEd25519, RejectsNonCanonicalS) {
91 // RFC 8032 strict verification rejects a signature whose scalar S is not reduced mod
92 // the group order L (anti-malleability). Build R||S with S exactly == L (little-endian)
93 // and with S all-0xFF (well above L); both must be refused.
94 const std::string seed = kKats[2].seed;
95 const std::string pub = ed::public_key(seed);
96 const std::string msg = "non-canonical S";
97 const std::string good = ed::sign(seed, msg);
98 ASSERT_TRUE(ed::verify(pub, msg, good)); // the canonical signature is accepted
100 const std::string R = good.substr(0, 64); // reuse a real R half
101 const std::string L_hex = // L, little-endian (S == L)
102 "edd3f55c1a631258d69cf7a2def9de14000000000000000000000000000000" "10";
103 EXPECT_FALSE(ed::verify(pub, msg, R + L_hex));
104 EXPECT_FALSE(ed::verify(pub, msg, R + std::string(64, 'f'))); // S = 0xFF…FF >> L
107TEST(CheatahEd25519, GenerateRoundTrip) {
108 const std::string s1 = ed::generate();
109 const std::string s2 = ed::generate();
110 EXPECT_EQ(s1.size(), 64u); // 32-byte seed as hex
111 EXPECT_NE(s1, s2); // fresh entropy each call
112 const std::string pub = ed::public_key(s1);
113 EXPECT_EQ(pub.size(), 64u);
114 const std::string msg = "sign with a generated key";
115 EXPECT_TRUE(ed::verify(pub, msg, ed::sign(s1, msg)));
118TEST(CheatahEd25519, RejectsMalformedSecret) {
119 EXPECT_THROW(ed::public_key("abc"), std::invalid_argument); // odd length
120 EXPECT_THROW(ed::public_key("zz"), std::invalid_argument); // non-hex
121 EXPECT_THROW(ed::public_key("00"), std::invalid_argument); // wrong byte count
122 EXPECT_THROW(ed::sign("00", "m"), std::invalid_argument);