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>10
namespace ed = cheatah::ed25519;12
namespace {13
// Decode a hex string to raw bytes (test messages are given in hex so embedded NULs and14
// arbitrary bytes round-trip cleanly).15
std::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;21
}23
struct 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 325
// "abc"), cross-checked against OpenSSL's Ed25519.26
const 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
} // namespace45
TEST(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 deterministic50
EXPECT_TRUE(ed::verify(k.pub, msg, k.sig));51
}52
}54
TEST(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));60
}62
TEST(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 key84
EXPECT_FALSE(ed::verify(pub, msg, "abcd")); // wrong-length signature85
EXPECT_FALSE(ed::verify("00", msg, sig)); // wrong-length public key86
EXPECT_FALSE(ed::verify("abc", msg, sig)); // odd-length hex -> from_hex throws -> caught87
EXPECT_FALSE(ed::verify(pub, msg, "abcde")); // odd-length signature hex -> caught88
}90
TEST(CheatahEd25519, RejectsNonCanonicalS) {91
// RFC 8032 strict verification rejects a signature whose scalar S is not reduced mod92
// 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 accepted100
const std::string R = good.substr(0, 64); // reuse a real R half101
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 >> L105
}107
TEST(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 hex111
EXPECT_NE(s1, s2); // fresh entropy each call112
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)));116
}118
TEST(CheatahEd25519, RejectsMalformedSecret) {119
EXPECT_THROW(ed::public_key("abc"), std::invalid_argument); // odd length120
EXPECT_THROW(ed::public_key("zz"), std::invalid_argument); // non-hex121
EXPECT_THROW(ed::public_key("00"), std::invalid_argument); // wrong byte count122
EXPECT_THROW(ed::sign("00", "m"), std::invalid_argument);123
}