cheatah
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 6979
4// Appendix A.2.6 test vectors (message "sample", SHA-384 AND SHA-256 — the SHA-256
5// 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 no
7// 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"
16namespace p384 = cheatah::p384;
18namespace {
20// hex (big-endian) -> raw bytes
21std::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;
34// RFC 6979 A.2.6 — the P-384 key pair and the "sample" signatures.
35const std::string kUx =
36 "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64DEF8F0EA9055866064A254515480BC13";
37const std::string kUy =
38 "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1288B231C3AE0D4FE7344FD2533264720";
39// With SHA-384:
40const std::string kR384 =
41 "94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA73D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46";
42const std::string kS384 =
43 "99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8";
44// With SHA-256 (a 32-byte hash < the 48-byte scalar — the whole-value bits2int case):
45const std::string kR256 =
46 "21B13D1E013C7FA1392D03C5F99AF8B30C570C6F98D4EA8E354B63A21D3DAA33BDE1E888E63355D92FA2B3C36D8FB2CD";
47const std::string kS256 =
48 "F3AA443FB107745BF4BD77CB3891674632068A10CA67E3D45DB2266FA7D1FEEBEFDC63ECCD1AC42EC0CB8668A4FA0AB0";
50// The P-384 field prime p and group order n (big-endian), and the SEC 2 base point.
51const std::string kP =
52 "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF";
53const std::string kN =
54 "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973";
55const std::string kGx =
56 "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7";
57const std::string kGy =
58 "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F";
60// Subtract two 48-byte big-endian values (a - b), assuming a >= b.
61std::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;
79} // namespace
81TEST(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));
95// The RFC's SHA-256 vector: a 32-byte hash under the 48-byte curve. bits2int takes the
96// WHOLE hash as the scalar (right-aligned) — left-aligning would fail every real
97// ecdsa-with-SHA256 signature made by a P-384 key (openssl's default self-cert shape).
98TEST(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)));
106TEST(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 102
113 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, a
126 // 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, ""));
143// SECURITY (invalid-curve point validation, SP 800-56A): a public key whose coordinates
144// are in range but NOT on y^2 = x^3 - 3x + b is rejected before it enters the group law.
145TEST(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 verifies
151 std::string off = good;
152 off[95] ^= 0x01; // flip the low bit of y: still < p, but no longer on the curve
153 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));
161TEST(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')));
180// A 48-byte "hash" >= n exercises the FIPS 186-4 reduction in hash_to_scalar for the
181// 6-limb instantiation (n itself drives geq's all-limbs-equal path; all-0xFF the subtract).
182TEST(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));
189// Verifying against a pubkey equal to G, and to -G, forces the Strauss-Shamir
190// precompute table to add a point to ITSELF (doubling branch) and to its NEGATION
191// (point-at-infinity branch) — the two jac_add group-law special cases, driven on
192// the 6-limb instantiation. The signatures need only be range-valid.
193TEST(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);
202TEST(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 bytes
206 // 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 point
211 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 length
217 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: the
226 // 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());