fix(crypto): branch-free EC signing ladder via complete addition (rv2 9 follow-up)
- pmul_ct: double-and-add-always over the Renes–Costello–Batina complete projective addition formula (Alg. 4, a=-3) — one exception-free formula for add and double, identity (0:1:0), so there is NO point-at-infinity branch. Closes the documented residual: the Jacobian jadd/jdouble ladder's fp_zero checks leaked k's leading-zero count (a bit-length hint) during ECDSA sign - wo_ecdsa_p256_sha256_sign uses it; affine x = X * Z^-1 (projective), the inversion via the constant-time modexp. Dead Jacobian jmul_ct/jpt_cmov removed - RFC 6979 A.2.5 vectors still byte-exact (test_crypto 130/0); server loopback (signs with this ladder) still green (test_tls 123/0); ASan/UBSan clean - docs: rv2 9 review_pending — close_notify + complete-formula ladder moved from deferred to landed; lang-41 decision 4 fixture marked landed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit fba30352b965e0f3b749168421a920a832df541b)
This commit is contained in:
parent
d9501ae8e3
commit
3b1a188d77
3 changed files with 96 additions and 22 deletions
|
|
@ -372,9 +372,12 @@ fixed.
|
|||
header so a NULL freelist link can never forge a valid class-0 object,
|
||||
trapping any stale drop rather than misreading it. Needs a language-track
|
||||
number when picked up.
|
||||
- **A minimal deterministic corpus fixture** (decision 4) — a cross-shard `send`
|
||||
of an object with an owned subtree, both sides dropping, under `WO_SHARDS>1` +
|
||||
ASan. Worth pinning; its own follow-up.
|
||||
- ~~**A minimal deterministic corpus fixture** (decision 4)~~ ✅ **landed
|
||||
2026-09-09** as `tests/regress/lang-41/cross-shard-marshal.wo` — a cross-shard
|
||||
`send` + `call` of an object carrying a `multi<Text>` subtree, both sides
|
||||
dropping, run 5× under `WO_SHARDS=4` + ASan by `scripts/db-actor-accept.sh`
|
||||
(`just db-actor`). It lives in `tests/regress` rather than the corpus because
|
||||
the corpus runner pins `WO_SHARDS=1`.
|
||||
- **The two smaller runtime defects found alongside** (above): the
|
||||
`try EXPR catch (e) nil` Int-0-vs-trap ambiguity and the `json.decode ... as T`
|
||||
cross-return-boundary corruption. Both worked around in the archived code;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ track: runtime-v2
|
|||
iteration: "9"
|
||||
status: done
|
||||
readiness: ready
|
||||
review_pending: "forks auto-approved 2026-09-08/09 for autonomous execution — developer second review before this ships. COMPLETE BOTH DIRECTIONS, live-gated (just tls 5/0 outbound, just tls-server 4/0 inbound EC+RSA). Outbound: A–E crypto, F1–F3c client (net.connect_tls/read_tls/write_tls, ids 115-117). Inbound: G1 constant-time RSA-PSS + ECDSA-P256 signing (RFC 6979), G2 server FSM, G3 net.accept_tls (id 118) + private-key parse. Deferred (named follow-ups, not blockers): park-based handshake, TlsConn language object, connection pooling, TLS close_notify on shutdown, complete-formula EC ladder. Doctrine docs (34/38/porch) corrected as part of this landing"
|
||||
review_pending: "forks auto-approved 2026-09-08/09 for autonomous execution — developer second review before this ships. COMPLETE BOTH DIRECTIONS, live-gated (just tls 5/0 outbound, just tls-server 4/0 inbound EC+RSA). Outbound: A–E crypto, F1–F3c client (net.connect_tls/read_tls/write_tls, ids 115-117). Inbound: G1 constant-time RSA-PSS + ECDSA-P256 signing (RFC 6979), G2 server FSM, G3 net.accept_tls (id 118) + private-key parse. Follow-ups landed 2026-09-09: TLS close_notify on shutdown (openssl clean EOF) and the complete-formula (RCB) constant-time EC ladder for signing (bit-length leak closed, RFC 6979 vectors unchanged). Still deferred (not blockers): park-based handshake, TlsConn language object, connection pooling. Doctrine docs (34/38/porch) corrected as part of this landing"
|
||||
---
|
||||
|
||||
# runtime-v2 9 — in-process TLS: retiring the proxy-termination doctrine
|
||||
|
|
|
|||
|
|
@ -1515,23 +1515,92 @@ int wo_ecdsa_p256_sha256_verify(const uint8_t qx[32], const uint8_t qy[32],
|
|||
* 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. */
|
||||
* branch-free double-and-add-always ladder over the Renes–Costello–Batina
|
||||
* complete addition formula (pmul_ct): no point-at-infinity special case, so
|
||||
* the op sequence is identical for every k. (The first cut used the Jacobian
|
||||
* jadd/jdouble, whose infinity branches leaked k's leading-zero count; replaced
|
||||
* 2026-09-09, RFC 6979 vectors unchanged.) */
|
||||
|
||||
static void jpt_cmov(jpt *d, const jpt *s, uint64_t mask) {
|
||||
/* (The earlier Jacobian double-and-add-always ladder was replaced by pmul_ct
|
||||
* below — its jadd/jdouble infinity branches leaked k's leading-zero count.) */
|
||||
|
||||
/* ---- complete projective addition (Renes–Costello–Batina 2016, Alg. 4) ---
|
||||
* For the SIGNING ladder. Standard projective coordinates (x = X/Z, y = Y/Z),
|
||||
* a = -3. "Complete" = exception-free: one formula is correct for P+Q, P+P
|
||||
* (doubling), P+O and P+(-P), so the ladder below has NO point-at-infinity
|
||||
* branch — which is exactly the leak jmul_ct's Jacobian jadd/jdouble carried
|
||||
* (their fp_zero checks exposed k's leading-zero count). Identity is (0:1:0).
|
||||
* ~12M+2m per step; plenty for one CertificateVerify per handshake. `bm` is the
|
||||
* curve constant b in Montgomery form. out must not alias the inputs (we copy). */
|
||||
typedef struct { fp X, Y, Z; } ppt;
|
||||
|
||||
static void padd_complete(const modctx *P, const fp bm, ppt *o, const ppt *p, const ppt *q) {
|
||||
const uint64_t *m = P->m;
|
||||
fp t0, t1, t2, t3, t4, X3, Y3, Z3;
|
||||
fpmul(P, t0, p->X, q->X); /* 1 */
|
||||
fpmul(P, t1, p->Y, q->Y); /* 2 */
|
||||
fpmul(P, t2, p->Z, q->Z); /* 3 */
|
||||
modadd(t3, p->X, p->Y, m, 4); /* 4 */
|
||||
modadd(t4, q->X, q->Y, m, 4); /* 5 */
|
||||
fpmul(P, t3, t3, t4); /* 6 */
|
||||
modadd(t4, t0, t1, m, 4); /* 7 */
|
||||
modsub(t3, t3, t4, m, 4); /* 8 */
|
||||
modadd(t4, p->Y, p->Z, m, 4); /* 9 */
|
||||
modadd(X3, q->Y, q->Z, m, 4); /* 10 */
|
||||
fpmul(P, t4, t4, X3); /* 11 */
|
||||
modadd(X3, t1, t2, m, 4); /* 12 */
|
||||
modsub(t4, t4, X3, m, 4); /* 13 */
|
||||
modadd(X3, p->X, p->Z, m, 4); /* 14 */
|
||||
modadd(Y3, q->X, q->Z, m, 4); /* 15 */
|
||||
fpmul(P, X3, X3, Y3); /* 16 */
|
||||
modadd(Y3, t0, t2, m, 4); /* 17 */
|
||||
modsub(Y3, X3, Y3, m, 4); /* 18 */
|
||||
fpmul(P, Z3, bm, t2); /* 19 */
|
||||
modsub(X3, Y3, Z3, m, 4); /* 20 */
|
||||
modadd(Z3, X3, X3, m, 4); /* 21 */
|
||||
modadd(X3, X3, Z3, m, 4); /* 22 */
|
||||
modsub(Z3, t1, X3, m, 4); /* 23 */
|
||||
modadd(X3, t1, X3, m, 4); /* 24 */
|
||||
fpmul(P, Y3, bm, Y3); /* 25 */
|
||||
modadd(t1, t2, t2, m, 4); /* 26 */
|
||||
modadd(t2, t1, t2, m, 4); /* 27 */
|
||||
modsub(Y3, Y3, t2, m, 4); /* 28 */
|
||||
modsub(Y3, Y3, t0, m, 4); /* 29 */
|
||||
modadd(t1, Y3, Y3, m, 4); /* 30 */
|
||||
modadd(Y3, t1, Y3, m, 4); /* 31 */
|
||||
modadd(t1, t0, t0, m, 4); /* 32 */
|
||||
modadd(t0, t1, t0, m, 4); /* 33 */
|
||||
modsub(t0, t0, t2, m, 4); /* 34 */
|
||||
fpmul(P, t1, t4, Y3); /* 35 */
|
||||
fpmul(P, t2, t0, Y3); /* 36 */
|
||||
fpmul(P, Y3, X3, Z3); /* 37 */
|
||||
modadd(Y3, Y3, t2, m, 4); /* 38 */
|
||||
fpmul(P, X3, t3, X3); /* 39 */
|
||||
modsub(X3, X3, t1, m, 4); /* 40 */
|
||||
fpmul(P, Z3, t4, Z3); /* 41 */
|
||||
fpmul(P, t1, t3, t0); /* 42 */
|
||||
modadd(Z3, Z3, t1, m, 4); /* 43 */
|
||||
for (int i = 0; i < 4; i++) { o->X[i] = X3[i]; o->Y[i] = Y3[i]; o->Z[i] = Z3[i]; }
|
||||
}
|
||||
|
||||
static void ppt_cmov(ppt *d, const ppt *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; }
|
||||
|
||||
/* R = k*pt, branch-free: double-and-add-always over the complete formula, so
|
||||
* the op sequence is identical for every k (no infinity special case). */
|
||||
static void pmul_ct(const modctx *P, const fp bm, ppt *o, const uint8_t k[32], const ppt *pt) {
|
||||
ppt acc; /* identity (0:1:0), mont */
|
||||
for (int i = 0; i < 4; i++) { acc.X[i] = 0; acc.Y[i] = P->one_mont[i]; 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);
|
||||
ppt d, t;
|
||||
padd_complete(P, bm, &d, &acc, &acc); /* double (complete) */
|
||||
padd_complete(P, bm, &t, &d, pt); /* always compute d+pt */
|
||||
uint64_t msk = (uint64_t)0 - (uint64_t)((k[(255 - bit) / 8] >> (7 - ((255 - bit) & 7))) & 1);
|
||||
acc = d;
|
||||
ppt_cmov(&acc, &t, msk); /* select on the bit */
|
||||
}
|
||||
*o = acc;
|
||||
}
|
||||
|
|
@ -1566,24 +1635,26 @@ int wo_ecdsa_p256_sha256_sign(const uint8_t d[32], const uint8_t hash[32],
|
|||
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;
|
||||
/* G and the curve b, projective + Montgomery, for the complete-formula ladder */
|
||||
ppt G; fp gx, gy, br, bm;
|
||||
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];
|
||||
bn_from_be(br, P256_B, 32); to_mont(&P, bm, br);
|
||||
|
||||
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);
|
||||
ppt R; pmul_ct(&P, bm, &R, V, &G); /* branch-free k*G */
|
||||
/* R.Z == 0 only if k*G = O, impossible for k in [1, n-1]; the check
|
||||
* is a never-taken sanity guard, not a secret-dependent branch. */
|
||||
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;
|
||||
/* affine x of R: projective x = X / Z (mod p), constant-time */
|
||||
fp Xn, Zn, zinv, 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);
|
||||
to_mont(&P, tm, zinv); fpmul(&P, xaff, tm, Xn); /* Xn * Z^-1 */
|
||||
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)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue