From 5c5d6e3314ce254e173d31c74ff97e453fcb3760 Mon Sep 17 00:00:00 2001 From: JackDoan Date: Wed, 14 Oct 2026 13:58:40 -0500 Subject: [PATCH] Fold the 2026-10-14 crypto review into the spec: deterministic signing, which fingerprint to block, the skem oracle, the psk2 static key, pending responders, the sticky default and the fork's rollback gap --- CHANGELOG.md | 16 +++++++------ docs/pq-handshake.md | 57 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 927d022f..815b2906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Version 3 certificates and post-quantum hybrid handshakes (`ix_hybrid_mlkem1024`). A version 3 certificate is a - version 2 certificate extended with an ML-KEM-1024 key (hosts) or an ML-DSA-87 key (CAs) and an ML-DSA-87 CA - signature, and a node holding one still presents the version 2 certificate it extends to peers that need it. - `nebula-cert pq-extend` upgrades existing CAs and hosts in place. See `docs/pq-handshake.md`. + version 2 certificate extended with an ML-KEM-1024 key (hosts) or an ML-DSA-87 key (CAs) and a deterministic + ML-DSA-87 CA signature, and a node holding one still presents the version 2 certificate it extends to peers that + need it. `nebula-cert pq-extend` upgrades existing CAs and hosts in place, and extending a certificate twice gives + the same certificate. See `docs/pq-handshake.md`. ### Changed -- A version 3 certificate has two fingerprints: its own, and that of the version 2 certificate it extends. Each - identifies it in logs and to `pki.blocklist`. Blocking the v2 fingerprint blocks both forms, but blocking only the - v3 fingerprint leaves the v2 form usable in classical handshakes. **Operators must list and block both - fingerprints**; `nebula-cert print` shows them as `fingerprint` and `v2Fingerprint`. +- A version 3 certificate has two fingerprints: its own, and that of the version 2 certificate it extends. Both + identify it in logs and to `pki.blocklist`. **To block a version 3 certificate, block its v2 fingerprint**: the + pool checks it for the v3 certificate and for its v2 form alike, so that one entry covers both forms. Blocking only + the v3 fingerprint leaves the v2 form usable in classical handshakes. `nebula-cert print` leads with both + fingerprints and says which one to block; in JSON they are `fingerprint` and `v2Fingerprint`. - Firewall `ca_sha` rules match a version 3 peer by either fingerprint of its CA. ### Fixed diff --git a/docs/pq-handshake.md b/docs/pq-handshake.md index b54ee780..95898977 100644 --- a/docs/pq-handshake.md +++ b/docs/pq-handshake.md @@ -55,6 +55,11 @@ about 1.84 KB each, so each travels as 2 data and 2 repair fragments of about 93 A KEM key on the classical subtype, or a missing or malformed one on this subtype, is a fatal protocol error; a forged ciphertext only fails that packet. +The `psk` token is the last in msg2, so the responder's static key `s` is encrypted under `ee` and +`se` only, and a future quantum attacker can read it from a recording; the certificate and indexes +follow the psk and stay protected. Identity hiding is a non-goal, and phase 2's KEM-first ordering +closes this anyway. + ## Phase 2: hybrid IX with certificate v3 Phase 2 adds post-quantum authentication. Decisions were made on 2026-10-14; it is implemented on @@ -85,12 +90,18 @@ CertificateV3 ::= SEQUENCE { - **Signatures.** Both must verify. The ML-DSA-87 signature covers the classical signature too, so neither signature can be swapped out (which also makes P256 low/high-s alternates moot for v3). If ML-DSA fell classically and someone swapped `pqPublicKey`, the hybrid handshake still needs - the classical static key, which only the classical signature covers. + the classical static key, which only the classical signature covers. The ML-DSA-87 signature is + deterministic (FIPS 204 section 3.4), so extending the same v2 certificate with the same key twice + gives the same v3 certificate and fingerprint: `pq-extend` is idempotent. The hedged variant + guards signers an attacker can fault physically, which a CA key in a file is not, and the Ed25519 + signature beside it is deterministic too. - **Issuer and fingerprints.** The details are v2-shaped, so the issuer is the fingerprint of the CA's v2 form. A v3 certificate's own fingerprint is the SHA-256 of its whole encoding and differs - from its v2 form's. Both identify it: the blocklist checks both, and operators must list both, - since blocking only the v3 fingerprint leaves the v2 form usable in classical handshakes. Firewall - `ca_sha` rules match a v3 peer by either fingerprint of its CA. + from its v2 form's. Both identify it. To block a v3 certificate, block its v2 fingerprint: the + pool checks it for the v3 certificate and for its v2 form alike, so one entry covers both forms, + while blocking only the v3 fingerprint leaves the v2 form usable in classical handshakes. + `nebula-cert print` leads with both fingerprints and says which one to block. Firewall `ca_sha` + rules match a v3 peer by either fingerprint of its CA. - **CA pool.** A v3 CA verifies only v3 certificates. To accept v2 certificates as well the pool must also list the CA's v2 form. Dropping the v2 form is how a node requires post-quantum authentication; there is no separate setting. Two different v3 extensions of one v2 CA are @@ -143,6 +154,13 @@ design rationale and security argument), with ML-KEM-1024 supplied by nebula: first packet under the new keys decrypts (it depends on msg3's `skem`). Neither side needs a signature. Knowing only a peer's classical private key gets an impersonator nowhere: as an initiator it cannot read msg2, as a responder it cannot read msg3. +- **The two static ML-KEM keys see different ciphertexts.** msg2's `skem` ciphertext is encrypted + under a key derived from the `ekem` secret, which whoever sent msg2 chose, so anyone who can + address a pending initiator's index can have the initiator decapsulate a ciphertext of their + choosing with its static key. That is the IND-CCA setting ML-KEM is designed for, and + `crypto/mlkem` is constant time. The responder decrypts msg3's `skem` ciphertext under the keys of + a completed msg2 before decapsulating, so it only ever decapsulates ciphertexts the genuine + initiator sealed. - v3 certificates go with this subtype and nothing else: a v3 certificate on any other subtype, or a v2 one on this one, is a protocol error. @@ -193,6 +211,9 @@ flynn/noise) before this can merge. - Post-quantum authentication is required through the CA pool, by not listing the CA's v2 form. A sticky or `require` initiator resists active downgrade of the key exchange; only the CA pool resists downgrade of authentication, since a quantum attacker could forge a v2 form. +- `sticky` stays off by default for v3 holders too (decided 2026-10-14): a peer re-issued back to a + v2-only certificate would otherwise be unreachable until this process restarts. Deployments that + want downgrade resistance turn on `sticky` or `require`, or drop the CA's v2 form from the pool. ## Fragment transport @@ -337,6 +358,21 @@ WireGuard's mac2, QUIC Retry and the SCTP state cookie: A CA-keyed pre-filter MAC was considered and rejected: networks commonly trust many CAs. +#### Pending hybrid responders + +A hybrid responder that answered msg1 keeps a pending entry, keyed by its local index, until +msg3 arrives or the handshake timeout (5.5 s by default) passes. msg1 is cleartext and nothing +covers `e` or `Time`, so from one captured v3 msg1 an attacker can mint distinct valid msg1s +with a fresh `e` and a bumped `Time`. Each misses the answered-msg1 cache, costs the responder +the whole msg1 (two signature verifications, two encapsulations, four DH operations, the +certificate checks) and a pending entry, and the fragment table stays near empty because a +complete burst arrives together, so cookies are never demanded and spoofed sources work. +Pending entries are not bounded per peer or in total. Accepted as is (2026-10-14): the per-msg1 +work is the same class as classical IX's pre-authentication cost, the expiry bounds the state in +time and the fragment table bounds bytes in flight. If it needs tightening, cap pending +responders per certificate fingerprint and in total, and count them toward the cookie +threshold. + #### Injected symbols The decoder keeps the first K distinct symbols it sees, so one forged symbol in those K @@ -467,9 +503,20 @@ in every scenario above, and within three at least 99.99%. - Publish DefinedNet/noise and drop the `replace` in `go.mod`, once Nate says the fork is ready to ship. The classical-only responder `e` is on the local clone's `drop-responder-ekem-key` branch (b4a892b), not yet in DefinedNet/noise. -- Cryptographic review of the hybrid composition and of the v3 certificate signatures. +- DefinedNet/noise `ReadMessage` returns `ErrShortMessage` without rolling back the symmetric + state ([DefinedNet/noise#2](https://github.com/DefinedNet/noise/issues/2), inherited from + flynn/noise). A message that fails a length check after a key-mixing token leaves `ck` and `h` + advanced, so the genuine message can never be read: one spoofed short packet to a pending + initiator's index, which msg1 carries in the clear in every IX variant, kills that attempt until + it times out, and the Machine reports it as recoverable. Blocks publishing the module; the fix is + a deferred rollback that also restores `k`, `n` and `hasK`. Smaller pre-publish items are in + [DefinedNet/noise#3](https://github.com/DefinedNet/noise/issues/3). - Field test of hybrid handshakes on real hosts, as was done for phase 1. +The cryptographic review of 2026-10-14 found no break in the composition or the certificate +chain. Its outcomes are folded in above: deterministic ML-DSA signing, the fingerprint guidance, +the `skem` oracle note, the psk2 note, the pending-responder exposure and the `sticky` default. + Accepted as is: a tunnel that came up classical between two v3 holders (say, the hybrid attempt was dropped) stays classical until it rehandshakes for another reason. `require`, `sticky` and a CA pool without v2 forms cover deployments that need more. PKCS#11 keys do not support v3.