feat(crypto): ECDSA-P256 signing, RFC 6979 nonce (rv2 9 phase G1b)

- wo_ecdsa_p256_sha256_sign: deterministic nonce (RFC 6979 HMAC-DRBG over
  the key + message — no RNG, no nonce-reuse/bias risk), then r = (k*G).x
  mod n and s = k^-1 (z + r*d) mod n
- constant-time in the secret: jmul_ct (double-and-add-always + point
  cmov) for k*G, and bn_modexp_ct for k^-1 mod n and the affine inversion.
  Known residual (documented): the ladder leaks k's leading-zero count (a
  bit-length hint, not the key) — a complete-formula/Montgomery-ladder
  upgrade is the named follow-up
- KAT: byte-for-byte vs the RFC 6979 A.2.5 P-256/SHA-256 vectors ("sample"
  + "test"), our sign verifies with our verify, determinism checked.
  test_crypto 115, ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 1bc6d04f9e18180dc7d0a6e5e7021dbd588e499a)
This commit is contained in:
shoney.arickathil 2026-09-09 03:11:22 +02:00
parent 631506d36f
commit df5054b07d
4 changed files with 179 additions and 0 deletions

View file

@ -1511,6 +1511,103 @@ int wo_ecdsa_p256_sha256_verify(const uint8_t qx[32], const uint8_t qy[32],
return fp_eq(xr, rv) ? 1 : 0; return fp_eq(xr, rv) ? 1 : 0;
} }
/* ---- ECDSA-P256 signing (rv2 9 phase G1b) --------------------------------
* Deterministic nonce (RFC 6979) — no RNG, no nonce-reuse/bias risk, and
* KAT-able against the published vectors. The scalar multiply k*G and the
* inversions touch the secret k/d, so they use the constant-time modexp and a
* double-and-add-always ladder. Known residual: the ladder's point-at-infinity
* handling leaks k's leading-zero count (a bit-length hint, not the key); a
* complete-formula / Montgomery-ladder upgrade is a named follow-up. */
static void jpt_cmov(jpt *d, const jpt *s, uint64_t mask) {
bn_cmov(d->X, s->X, mask, 4);
bn_cmov(d->Y, s->Y, mask, 4);
bn_cmov(d->Z, s->Z, mask, 4);
}
/* R = k*pt, constant-time in k (double-and-add-always). */
static void jmul_ct(const modctx *P, jpt *o, const uint8_t k[32], const jpt *pt) {
jpt acc; for (int i = 0; i < 4; i++) { acc.X[i] = 0; acc.Y[i] = 0; acc.Z[i] = 0; }
for (int bit = 255; bit >= 0; bit--) {
jdouble(P, &acc, &acc);
jpt t; jadd(P, &t, &acc, pt);
uint64_t m = (uint64_t)0 - (uint64_t)((k[(255 - bit) / 8] >> (7 - ((255 - bit) & 7))) & 1);
jpt_cmov(&acc, &t, m);
}
*o = acc;
}
/* HMAC-SHA256 with a 32-byte key (RFC 6979's DRBG uses fixed-size keys). */
static void hmac32(const uint8_t key[32], const uint8_t *msg, size_t mlen,
uint8_t out[32]) {
wo_hmac_sha256(key, 32, msg, mlen, out);
}
/* ECDSA-P256 sign over SHA-256 with an RFC 6979 deterministic nonce. Private
* scalar d and 32-byte message hash in; (r,s) out, big-endian. 0 ok, -1 on
* failure (astronomically unlikely nonce exhaustion, or d out of range). */
int wo_ecdsa_p256_sha256_sign(const uint8_t d[32], const uint8_t hash[32],
uint8_t r_out[32], uint8_t s_out[32]) {
modctx P, N;
modctx_init(&P, P256_P);
modctx_init(&N, P256_N);
fp dfp; bn_from_be(dfp, d, 32);
if (fp_zero(dfp) || bn_ge(dfp, N.m, 4)) return -1; /* d in [1, n-1] */
/* z = hash mod n, and bits2octets(hash) = z as 32 bytes */
fp z; bn_from_be(z, hash, 32);
if (bn_ge(z, N.m, 4)) bn_sub(z, z, N.m, 4);
uint8_t h1o[32]; bn_to_be(h1o, 32, z, 4);
/* RFC 6979 §3.2 seeding */
uint8_t V[32], K[32], buf[32 + 1 + 32 + 32];
memset(V, 0x01, 32); memset(K, 0x00, 32);
memcpy(buf, V, 32); buf[32] = 0x00; memcpy(buf + 33, d, 32); memcpy(buf + 65, h1o, 32);
hmac32(K, buf, 97, K); hmac32(K, V, 32, V);
memcpy(buf, V, 32); buf[32] = 0x01; memcpy(buf + 33, d, 32); memcpy(buf + 65, h1o, 32);
hmac32(K, buf, 97, K); hmac32(K, V, 32, V);
/* pre-mont G */
jpt G; fp gx, gy;
bn_from_be(gx, P256_GX, 32); bn_from_be(gy, P256_GY, 32);
to_mont(&P, G.X, gx); to_mont(&P, G.Y, gy);
for (int i = 0; i < 4; i++) G.Z[i] = P.one_mont[i];
for (int tries = 0; tries < 64; tries++) {
hmac32(K, V, 32, V); /* T = V (qlen = 256) */
fp kfp; bn_from_be(kfp, V, 32);
if (!fp_zero(kfp) && !bn_ge(kfp, N.m, 4)) {
jpt R; jmul_ct(&P, &R, V, &G);
if (!fp_zero(R.Z)) {
/* affine x of R (Z^-2 * X, mod p, all constant-time) */
fp Xn, Zn, zinv, zinv2, tm, xaff, rr;
from_mont(&P, Xn, R.X); from_mont(&P, Zn, R.Z);
bn_modexp_ct(zinv, Zn, P.m, 4, P256_PM2, 32);
to_mont(&P, tm, zinv); fpmul(&P, zinv2, tm, zinv);
to_mont(&P, tm, Xn); fpmul(&P, xaff, tm, zinv2);
for (int i = 0; i < 4; i++) rr[i] = xaff[i];
if (bn_ge(rr, N.m, 4)) bn_sub(rr, rr, N.m, 4);
if (!fp_zero(rr)) {
/* s = k^-1 (z + r*d) mod n */
fp kinv, rd, zrd, ss;
bn_modexp_ct(kinv, kfp, N.m, 4, P256_NM2, 32);
to_mont(&N, tm, rr); fpmul(&N, rd, tm, dfp); /* r*d */
modadd(zrd, z, rd, N.m, 4); /* z + r*d */
to_mont(&N, tm, kinv); fpmul(&N, ss, tm, zrd); /* k^-1*(z+rd) */
if (!fp_zero(ss)) {
bn_to_be(r_out, 32, rr, 4);
bn_to_be(s_out, 32, ss, 4);
return 0;
}
}
}
}
/* reject: K = HMAC(K, V||0x00); V = HMAC(K, V) */
memcpy(buf, V, 32); buf[32] = 0x00;
hmac32(K, buf, 33, K); hmac32(K, V, 32, V);
}
return -1;
}
/* ---- X.509 / ASN.1 DER (rv2 9 phase E, core) ---------------------------- /* ---- X.509 / ASN.1 DER (rv2 9 phase E, core) ----------------------------
* A defensive DER reader and the certificate-field extraction TLS needs: * A defensive DER reader and the certificate-field extraction TLS needs:
* tbsCertificate (raw, for signature verification), the signature algorithm, * tbsCertificate (raw, for signature verification), the signature algorithm,

View file

@ -77,6 +77,12 @@ int wo_ecdsa_p256_sha256_verify(const uint8_t qx[32], const uint8_t qy[32],
const uint8_t r[32], const uint8_t s[32], const uint8_t r[32], const uint8_t s[32],
const uint8_t hash[32]); const uint8_t hash[32]);
/* ECDSA-P256 SIGN over SHA-256 with an RFC 6979 deterministic nonce (rv2 9
* phase G1b). Private scalar d + 32-byte hash in; (r,s) big-endian out. The
* secret-dependent scalar mult and inversions are constant-time. 0 ok, -1. */
int wo_ecdsa_p256_sha256_sign(const uint8_t d[32], const uint8_t hash[32],
uint8_t r_out[32], uint8_t s_out[32]);
/* X.509 / ASN.1 DER (rv2 9 phase E, core). Internal C consumed by the TLS /* X.509 / ASN.1 DER (rv2 9 phase E, core). Internal C consumed by the TLS
* handshake. key_alg / return values use the WO_X509_* enums in crypto.c * handshake. key_alg / return values use the WO_X509_* enums in crypto.c
* (RSA = 1, EC_P256 = 2). */ * (RSA = 1, EC_P256 = 2). */

View file

@ -0,0 +1,56 @@
/* RFC 6979 A.2.5 ECDSA P-256/SHA-256 KAT (deterministic nonce). */
static const unsigned char ecs_d[] = {
0xc9,0xaf,0xa9,0xd8,0x45,0xba,0x75,0x16,0x6b,0x5c,0x21,0x57,
0x67,0xb1,0xd6,0x93,0x4e,0x50,0xc3,0xdb,0x36,0xe8,0x9b,0x12,
0x7b,0x8a,0x62,0x2b,0x12,0x0f,0x67,0x21,
};
static const unsigned char ecs_qx[] = {
0x60,0xfe,0xd4,0xba,0x25,0x5a,0x9d,0x31,0xc9,0x61,0xeb,0x74,
0xc6,0x35,0x6d,0x68,0xc0,0x49,0xb8,0x92,0x3b,0x61,0xfa,0x6c,
0xe6,0x69,0x62,0x2e,0x60,0xf2,0x9f,0xb6,
};
static const unsigned char ecs_qy[] = {
0x79,0x03,0xfe,0x10,0x08,0xb8,0xbc,0x99,0xa4,0x1a,0xe9,0xe9,
0x56,0x28,0xbc,0x64,0xf2,0xf1,0xb2,0x0c,0x2d,0x7e,0x9f,0x51,
0x77,0xa3,0xc2,0x94,0xd4,0x46,0x22,0x99,
};
static const unsigned char ecs_sample_mhash[] = {
0xaf,0x2b,0xdb,0xe1,0xaa,0x9b,0x6e,0xc1,0xe2,0xad,0xe1,0xd6,
0x94,0xf4,0x1f,0xc7,0x1a,0x83,0x1d,0x02,0x68,0xe9,0x89,0x15,
0x62,0x11,0x3d,0x8a,0x62,0xad,0xd1,0xbf,
};
static const unsigned char ecs_sample_r[] = {
0xef,0xd4,0x8b,0x2a,0xac,0xb6,0xa8,0xfd,0x11,0x40,0xdd,0x9c,
0xd4,0x5e,0x81,0xd6,0x9d,0x2c,0x87,0x7b,0x56,0xaa,0xf9,0x91,
0xc3,0x4d,0x0e,0xa8,0x4e,0xaf,0x37,0x16,
};
static const unsigned char ecs_sample_s[] = {
0xf7,0xcb,0x1c,0x94,0x2d,0x65,0x7c,0x41,0xd4,0x36,0xc7,0xa1,
0xb6,0xe2,0x9f,0x65,0xf3,0xe9,0x00,0xdb,0xb9,0xaf,0xf4,0x06,
0x4d,0xc4,0xab,0x2f,0x84,0x3a,0xcd,0xa8,
};
static const unsigned char ecs_test_mhash[] = {
0x9f,0x86,0xd0,0x81,0x88,0x4c,0x7d,0x65,0x9a,0x2f,0xea,0xa0,
0xc5,0x5a,0xd0,0x15,0xa3,0xbf,0x4f,0x1b,0x2b,0x0b,0x82,0x2c,
0xd1,0x5d,0x6c,0x15,0xb0,0xf0,0x0a,0x08,
};
static const unsigned char ecs_test_r[] = {
0xf1,0xab,0xb0,0x23,0x51,0x83,0x51,0xcd,0x71,0xd8,0x81,0x56,
0x7b,0x1e,0xa6,0x63,0xed,0x3e,0xfc,0xf6,0xc5,0x13,0x2b,0x35,
0x4f,0x28,0xd3,0xb0,0xb7,0xd3,0x83,0x67,
};
static const unsigned char ecs_test_s[] = {
0x01,0x9f,0x41,0x13,0x74,0x2a,0x2b,0x14,0xbd,0x25,0x92,0x6b,
0x49,0xc6,0x49,0x15,0x5f,0x26,0x7e,0x60,0xd3,0x81,0x4b,0x4c,
0x0c,0xc8,0x42,0x50,0xe4,0x6f,0x00,0x83,
};
/* (published r,s cross-verified against Q by python) */

View file

@ -9,6 +9,7 @@
#include "t.h" #include "t.h"
#include "x509_vectors.h" #include "x509_vectors.h"
#include "rsa_sign_vectors.h" #include "rsa_sign_vectors.h"
#include "ecdsa_sign_vectors.h"
static void hex(const uint8_t *d, size_t n, char *out) { static void hex(const uint8_t *d, size_t n, char *out) {
static const char *h = "0123456789abcdef"; static const char *h = "0123456789abcdef";
@ -472,5 +473,24 @@ int main(void) {
sizeof rsasig_e, sig, 256, rsasig_mhash, 32) == 0); sizeof rsasig_e, sig, 256, rsasig_mhash, 32) == 0);
} }
/* ECDSA-P256 SIGN with RFC 6979 nonce (rv2 9 phase G1b) — byte-for-byte vs
* the RFC 6979 A.2.5 vectors, and our sign verifies with our verify. */
{
uint8_t r[32], s[32];
/* "sample" */
T_CHECK(wo_ecdsa_p256_sha256_sign(ecs_d, ecs_sample_mhash, r, s) == 0);
T_CHECK(memcmp(r, ecs_sample_r, 32) == 0 && memcmp(s, ecs_sample_s, 32) == 0);
T_CHECK(wo_ecdsa_p256_sha256_verify(ecs_qx, ecs_qy, r, s, ecs_sample_mhash) == 1);
/* "test" */
T_CHECK(wo_ecdsa_p256_sha256_sign(ecs_d, ecs_test_mhash, r, s) == 0);
T_CHECK(memcmp(r, ecs_test_r, 32) == 0 && memcmp(s, ecs_test_s, 32) == 0);
T_CHECK(wo_ecdsa_p256_sha256_verify(ecs_qx, ecs_qy, r, s, ecs_test_mhash) == 1);
/* determinism: same input, same signature */
uint8_t r2[32], s2[32];
wo_ecdsa_p256_sha256_sign(ecs_d, ecs_sample_mhash, r2, s2);
wo_ecdsa_p256_sha256_sign(ecs_d, ecs_sample_mhash, r, s);
T_CHECK(memcmp(r, r2, 32) == 0 && memcmp(s, s2, 32) == 0);
}
return t_report("test_crypto"); return t_report("test_crypto");
} }