PCH-02-009-reject-zero-dh.patch.md

3.7 KB · raw

rendered · source

1# PCH-02-009 — Reject zero X25519 DH outputs in Noise XX and NK23**Severity:** Informational4**Target files:** `src/shared/noise-xx.ts` (6 DH sites),5`src/shared/noise-js.ts` (6 DH sites)6**Report:** peach-code-review.md, Finding PCH-02-00978## Implementation choice (simpler than the brief's sample)910`@stablelib/x25519`'s `sharedKey` already has the exact guard built in:11`sharedKey(mySecretKey, theirPublicKey, rejectZero = false)` — with12`rejectZero: true` it throws `"X25519: invalid shared key"` on an all-zero13output (verified in `node_modules/@stablelib/x25519/lib/x25519.js:606-624`).14Passing `true` at every call site is equivalent to the brief's manual15`every(b => b === 0)` check, applies to the raw 32-byte output before it16reaches `mixKey`/HMAC, and adds no new code path to maintain.1718**Not a wire change.** Honest peers never produce an all-zero shared secret19(the probability for random keys is negligible; only adversarially chosen20low-order/identity points do). Each side can adopt the check independently —21no Android coordination required, though mirroring it there is recommended.2223## Exact edits — all 12 DH call sites2425`noise-xx.ts` — add a rationale comment at the first site, `, true` at all six:2627```typescript28// Line 108 (initiator, ee):29// BEFORE30  const ee = sharedKey(e.secretKey, remoteEphemeral);31// AFTER32  // PCH-02-009: rejectZero — an all-zero DH output (low-order peer point)33  // aborts the handshake before the value can enter the key schedule. Applies34  // at every DH below; honest peers are unaffected.35  const ee = sharedKey(e.secretKey, remoteEphemeral, true);3637// Line 119 (initiator, es):   sharedKey(e.secretKey, remoteStaticPlain, true)38// Line 140 (initiator, se):   sharedKey(ourStaticPrivateKey, remoteEphemeral, true)39// Line 196 (responder, ee):   sharedKey(e.secretKey, remoteEphemeral, true)40// Line 204 (responder, es):   sharedKey(ourStaticPrivateKey, remoteEphemeral, true)41// Line 231 (responder, se):   sharedKey(e.secretKey, remoteStaticPlain, true)42```4344`noise-js.ts` — same pattern, comment at the first site:4546```typescript47// Line 84  (NK initiator, es):  sharedKey(ephemeral.secretKey, responderStaticPublicKey, true)48//   with the PCH-02-009 rationale comment above it (as in noise-xx.ts)49// Line 106 (NK initiator, ee):  sharedKey(ephemeral.secretKey, remoteEphemeralPublic, true)50// Line 175 (computeAuthProof):  sharedKey(initiatorPrivate, responderPublic, true)51// Line 248 (NK responder, es):  sharedKey(ourStaticPrivateKey, remoteEphemeralPublic, true)52// Line 263 (NK responder, ee):  sharedKey(ephemeral.secretKey, remoteEphemeralPublic, true)53// Line 362 (performResponderAuth): sharedKey(ourPrivateKey, initiatorPublicKey, true)54```5556The two auth-proof sites (175, 362) key an HMAC rather than `mixKey`; the57same contributory-behaviour argument applies — a zero MAC key derived from an58attacker-chosen point should abort, not authenticate.5960## KAT / existing-test impact6162None. The pinned vectors (`noise-js.test.ts` self-vectors,63`noise-blake2s-kat.test.ts` cross-repo vectors) all use honest keys whose DH64outputs are non-zero; `rejectZero` only changes behaviour on the degenerate65inputs the new tests inject.6667## Tests6869`noise.reject-zero-dh.test.ts` (in this folder) — 5 tests: NK initiator70aborts against an all-zero (low-order) responder static; NK responder aborts71on an all-zero msg-1 ephemeral; XX responder aborts on an all-zero msg-172ephemeral; full NK and XX handshakes over an in-memory transport still73succeed with honest keys (regression).74Destination: `src/shared/noise.reject-zero-dh.test.ts`.7576## Save message (integration)7778`fix: PCH-02-009 reject zero X25519 DH output at all 12 Noise XX/NK call sites via stablelib rejectZero`