Source
stdlib/tests/p384_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
// p384_test — NIST P-384 ECDSA verification correctness against the RFC 69794
// Appendix A.2.6 test vectors (message "sample", SHA-384 AND SHA-256 — the SHA-2565
// one pins the bits2int semantics for a hash SHORTER than the 48-byte scalar).6
// p384 is verify-only (TLS certificate validation), so unlike p256 there is no7
// signing path to round-trip; the deterministic RFC vectors stand in for it.9
#include <string>11
#include <gtest/gtest.h>13
#include "hashlib.hpp"14
#include "p384.hpp"16
namespace p384 = cheatah::p384;18
namespace {20
// hex (big-endian) -> raw bytes21
std::string unhex(const std::string& h) {22
auto nib = [](char c) -> int {23
if (c >= '0' && c <= '9') return c - '0';24
if (c >= 'a' && c <= 'f') return c - 'a' + 10;25
return c - 'A' + 10;26
};27
std::string out;28
out.reserve(h.size() / 2);29
for (std::size_t i = 0; i + 1 < h.size(); i += 2)30
out.push_back(static_cast<char>((nib(h[i]) << 4) | nib(h[i + 1])));31
return out;32
}34
// RFC 6979 A.2.6 — the P-384 key pair and the "sample" signatures.35
const std::string kUx =36
"EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64DEF8F0EA9055866064A254515480BC13";37
const std::string kUy =38
"8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1288B231C3AE0D4FE7344FD2533264720";39
// With SHA-384:40
const std::string kR384 =41
"94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA73D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46";42
const std::string kS384 =43
"99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8";44
// With SHA-256 (a 32-byte hash < the 48-byte scalar — the whole-value bits2int case):45
const std::string kR256 =46
"21B13D1E013C7FA1392D03C5F99AF8B30C570C6F98D4EA8E354B63A21D3DAA33BDE1E888E63355D92FA2B3C36D8FB2CD";47
const std::string kS256 =48
"F3AA443FB107745BF4BD77CB3891674632068A10CA67E3D45DB2266FA7D1FEEBEFDC63ECCD1AC42EC0CB8668A4FA0AB0";50
// The P-384 field prime p and group order n (big-endian), and the SEC 2 base point.51
const std::string kP =52
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF";53
const std::string kN =54
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973";55
const std::string kGx =56
"AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7";57
const std::string kGy =58
"3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F";60
// Subtract two 48-byte big-endian values (a - b), assuming a >= b.61
std::string be_sub(const std::string& a, const std::string& b) {62
std::string r(48, '\0');63
int borrow = 0;64
for (int i = 47; i >= 0; --i) {65
int av = static_cast<unsigned char>(a[i]);66
int bv = static_cast<unsigned char>(b[i]) + borrow;67
int d = av - bv;68
if (d < 0) {69
d += 256;70
borrow = 1;71
} else {72
borrow = 0;73
}74
r[i] = static_cast<char>(d);75
}76
return r;77
}79
} // namespace81
TEST(CheatahP384, VerifyKnownVector) {82
const std::string hash = cheatah::hashlib::sha384_digest("sample");83
const std::string pub = unhex(kUx) + unhex(kUy);84
const std::string sig = unhex(kR384) + unhex(kS384);85
EXPECT_TRUE(p384::verify_raw(pub, hash, sig));87
// A tampered signature must fail.88
std::string bad = sig;89
bad[95] ^= 0x01;90
EXPECT_FALSE(p384::verify_raw(pub, hash, bad));91
// A different message must fail.92
EXPECT_FALSE(p384::verify_raw(pub, cheatah::hashlib::sha384_digest("test"), sig));93
}95
// The RFC's SHA-256 vector: a 32-byte hash under the 48-byte curve. bits2int takes the96
// WHOLE hash as the scalar (right-aligned) — left-aligning would fail every real97
// ecdsa-with-SHA256 signature made by a P-384 key (openssl's default self-cert shape).98
TEST(CheatahP384, VerifyKnownVectorSha256) {99
const std::string hash = cheatah::hashlib::sha256_digest("sample");100
const std::string pub = unhex(kUx) + unhex(kUy);101
EXPECT_TRUE(p384::verify_raw(pub, hash, unhex(kR256) + unhex(kS256)));102
// Cross-pairing hash/signature must fail.103
EXPECT_FALSE(p384::verify_raw(pub, hash, unhex(kR384) + unhex(kS384)));104
}106
TEST(CheatahP384, VerifyDerWithLeadingZeroIntegers) {107
// DER-encode the RFC 6979 (r, s): both start with a high bit set (0x94 / 0x99),108
// so DER requires a 0x00 sign byte — exercising der_to_rs's leading-zero strip.109
const std::string r = unhex(kR384), s = unhex(kS384);110
std::string der;111
der.push_back(0x30);112
der.push_back(0x66); // SEQUENCE, length 102113
der.push_back(0x02);114
der.push_back(0x31); // INTEGER, length 49 (48 + sign byte)115
der.push_back(0x00);116
der += r;117
der.push_back(0x02);118
der.push_back(0x31);119
der.push_back(0x00);120
der += s;121
const std::string pub = unhex(kUx) + unhex(kUy);122
const std::string hash = cheatah::hashlib::sha384_digest("sample");123
EXPECT_TRUE(p384::verify_der(pub, hash, der));125
// Malformed DER must be rejected: wrong sequence length, wrong outer tag, a126
// long-form length, truncation, and a non-INTEGER first element.127
std::string bad = der;128
bad[1] = 0x60;129
EXPECT_FALSE(p384::verify_der(pub, hash, bad));130
bad = der;131
bad[0] = 0x31;132
EXPECT_FALSE(p384::verify_der(pub, hash, bad));133
bad = der;134
bad[1] = static_cast<char>(0x81); // long-form length — refused (short-form only)135
EXPECT_FALSE(p384::verify_der(pub, hash, bad));136
EXPECT_FALSE(p384::verify_der(pub, hash, der.substr(0, der.size() / 2)));137
bad = der;138
bad[2] = 0x03;139
EXPECT_FALSE(p384::verify_der(pub, hash, bad));140
EXPECT_FALSE(p384::verify_der(pub, hash, ""));141
}143
// SECURITY (invalid-curve point validation, SP 800-56A): a public key whose coordinates144
// are in range but NOT on y^2 = x^3 - 3x + b is rejected before it enters the group law.145
TEST(CheatahP384, RejectsOffCurvePublicKey) {146
const std::string hash = cheatah::hashlib::sha384_digest("sample");147
const std::string sig = unhex(kR384) + unhex(kS384);148
const std::string good = unhex(kUx) + unhex(kUy);149
ASSERT_TRUE(p384::verify_raw(good, hash, sig)); // the genuine (on-curve) key verifies151
std::string off = good;152
off[95] ^= 0x01; // flip the low bit of y: still < p, but no longer on the curve153
EXPECT_FALSE(p384::verify_raw(off, hash, sig));155
// A point with y = 0 (never on this curve) is also refused.156
std::string y_zero = good;157
for (int i = 48; i < 96; ++i) y_zero[i] = 0;158
EXPECT_FALSE(p384::verify_raw(y_zero, hash, sig));159
}161
TEST(CheatahP384, VerifyRejectsOutOfRangeAndWrongSizes) {162
const std::string pub = unhex(kUx) + unhex(kUy);163
const std::string hash = cheatah::hashlib::sha384_digest("sample");164
// r or s == 0 -> reject.165
EXPECT_FALSE(p384::verify_raw(pub, hash, std::string(48, '\0') + unhex(kS384)));166
EXPECT_FALSE(p384::verify_raw(pub, hash, unhex(kR384) + std::string(48, '\0')));167
// r or s >= n -> reject (n itself, and all-0xFF).168
EXPECT_FALSE(p384::verify_raw(pub, hash, unhex(kN) + unhex(kS384)));169
EXPECT_FALSE(p384::verify_raw(pub, hash, unhex(kR384) + std::string(48, '\xff')));170
// Public coordinate >= field prime p -> reject.171
EXPECT_FALSE(p384::verify_raw(std::string(48, '\xff') + unhex(kUy), hash,172
unhex(kR384) + unhex(kS384)));173
// Wrong-size inputs -> reject (64-byte P-256 shapes included).174
EXPECT_FALSE(p384::verify_raw("short", hash, unhex(kR384) + unhex(kS384)));175
EXPECT_FALSE(p384::verify_raw(pub, hash, "short"));176
EXPECT_FALSE(p384::verify_raw(std::string(64, '\x01'), hash, unhex(kR384) + unhex(kS384)));177
EXPECT_FALSE(p384::verify_raw(pub, hash, std::string(64, '\x01')));178
}180
// A 48-byte "hash" >= n exercises the FIPS 186-4 reduction in hash_to_scalar for the181
// 6-limb instantiation (n itself drives geq's all-limbs-equal path; all-0xFF the subtract).182
TEST(CheatahP384, HashToScalarReducesWhenGreaterThanOrder) {183
const std::string pub = unhex(kUx) + unhex(kUy);184
const std::string sig = unhex(kR384) + unhex(kS384);185
EXPECT_FALSE(p384::verify_raw(pub, unhex(kN), sig));186
EXPECT_FALSE(p384::verify_raw(pub, std::string(48, '\xff'), sig));187
}189
// Verifying against a pubkey equal to G, and to -G, forces the Strauss-Shamir190
// precompute table to add a point to ITSELF (doubling branch) and to its NEGATION191
// (point-at-infinity branch) — the two jac_add group-law special cases, driven on192
// the 6-limb instantiation. The signatures need only be range-valid.193
TEST(CheatahP384, VerifyHitsGroupLawSpecialCases) {194
const std::string sig = unhex(kR384) + unhex(kS384);195
const std::string hash = cheatah::hashlib::sha384_digest("sample");196
// pubkey == G : tbl[1][1] = G + G (doubling special case)197
(void)p384::verify_raw(unhex(kGx) + unhex(kGy), hash, sig);198
// pubkey == -G = (Gx, p - Gy) : tbl[1][1] = G + (-G) (infinity special case)199
(void)p384::verify_raw(unhex(kGx) + be_sub(unhex(kP), unhex(kGy)), hash, sig);200
}202
TEST(CheatahP384, SpkiExtractsPoint) {203
// A SubjectPublicKeyInfo carrying the RFC 6979 public point: id-ecPublicKey +204
// secp384r1, then BIT STRING 00 04 X Y.205
const std::string point = std::string("\x04", 1) + unhex(kUx) + unhex(kUy); // 97 bytes206
// Algorithm OIDs (id-ecPublicKey 1.2.840.10045.2.1 and secp384r1 1.3.132.0.34)207
const std::string alg = unhex("301006072A8648CE3D020106052B81040022");208
std::string bitstr;209
bitstr.push_back(0x03);210
bitstr.push_back(0x62); // 98 bytes: 00 unused + 97-byte point211
bitstr.push_back(0x00);212
bitstr += point;213
std::string inner = alg + bitstr;214
std::string spki;215
spki.push_back(0x30);216
spki.push_back(static_cast<char>(0x81)); // inner is 117 bytes -> long-form length217
spki.push_back(static_cast<char>(inner.size()));218
spki += inner;219
const std::string got = p384::spki_ec_point(spki);220
ASSERT_EQ(got.size(), 96u);221
EXPECT_EQ(got, unhex(kUx) + unhex(kUy));223
// A DER with no uncompressed EC point returns "".224
EXPECT_TRUE(p384::spki_ec_point(std::string("\x30\x03\x02\x01\x00", 5)).empty());225
// A P-256-sized BIT STRING (length 66) is NOT a P-384 point — and vice versa: the226
// two extractors are length-anchored and mutually exclusive on real SPKIs.227
const std::string p256_bitstr = std::string("\x03\x42\x00\x04", 4) + std::string(64, '\x05');228
EXPECT_TRUE(p384::spki_ec_point(p256_bitstr).empty());229
}