Skip to content

Commit 7cc472f

Browse files
fix(crypto): real Noise ee+es+se key agreement (forward secrecy) — part 1 of N3
The handshake computed only ee + ss and passed `ss` in BOTH the es and se slots, so it had no forward secrecy and did not bind ephemerals into the session. Fix: NoiseXX.ephemeral becomes a reusable StaticSecret (EphemeralSecret is consumed by its first diffie_hellman; we need two), and complete_initiator/ responder compute the real ee/es/se so both sides derive the same key. Verified: handshake_and_roundtrip + independent_handshakes pass, full suite 103 passed. SCOPE: forward-secrecy half of N3 only. Identity auth (allow-list gating -> the actual MITM defense), N4 (HELLO MAC), N5 (keystore) remain open per RFC #63 and need owner sign-off. Based on the pipeline-guard branch (needs #60's build fix). Draft — needs crypto review. Refs #58, #63. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> phi^2 + phi^-2 = 3
1 parent 4adf850 commit 7cc472f

1 file changed

Lines changed: 21 additions & 22 deletions

File tree

src/crypto.rs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,11 @@ pub struct NoiseXX {
9292
/// Our static identity key (long-term)
9393
static_secret: StaticSecret,
9494
static_public: PublicKey,
95-
/// Our ephemeral key for this handshake
96-
ephemeral: EphemeralSecret,
95+
/// Our ephemeral key for this handshake. A reusable `StaticSecret` (not a
96+
/// one-shot `EphemeralSecret`) because the Noise key agreement needs it for
97+
/// TWO DH operations (ee and es/se); it is still freshly random per handshake
98+
/// and dropped when the handshake completes.
99+
ephemeral: StaticSecret,
97100
ephemeral_public: PublicKey,
98101
/// True if we're the initiator (first to send). Retained for future
99102
/// role-aware rekey/anti-replay logic; not yet read by current handlers.
@@ -105,7 +108,7 @@ impl NoiseXX {
105108
/// Start a new Noise-XX handshake with a static identity key.
106109
pub fn new(static_secret: StaticSecret, initiator: bool) -> Self {
107110
let static_public = PublicKey::from(&static_secret);
108-
let ephemeral = EphemeralSecret::random_from_rng(OsRng);
111+
let ephemeral = StaticSecret::random_from_rng(OsRng);
109112
let ephemeral_public = PublicKey::from(&ephemeral);
110113

111114
Self {
@@ -130,33 +133,29 @@ impl NoiseXX {
130133
/// Complete as initiator: receive responder's message, derive session.
131134
/// Input: (responder_ephemeral_pub, responder_static_pub)
132135
pub fn complete_initiator(self, peer_ephemeral: PublicKey, peer_static: PublicKey) -> Session {
133-
// SIMPLIFIED Noise-XX: Use only ee (ephemeral-ephemeral) + ss (static-static)
134-
// Proper Noise-XX would use ee, es, se but that requires multiple ephemeral DH ops
135-
136-
// ee = ephemeral × peer_ephemeral
136+
// Real Noise key agreement: ee + es + se (was ee + ss with `ss` wrongly
137+
// passed in both the es and se slots, giving no forward secrecy). The
138+
// initiator's shares:
139+
// ee = e_i . e_r es = e_i . s_r se = s_i . e_r
137140
let ee = self.ephemeral.diffie_hellman(&peer_ephemeral);
138-
let ee_bytes = *ee.as_bytes();
139-
140-
// ss = static × peer_static (both sides compute this, gets same result)
141-
let ss = self.static_secret.diffie_hellman(&peer_static);
142-
let ss_bytes = *ss.as_bytes();
143-
144-
// Combine ee + ss (both sides get same result)
145-
let combined = combine_dh_shares(&ee_bytes, &ss_bytes, &ss_bytes);
141+
let es = self.ephemeral.diffie_hellman(&peer_static);
142+
let se = self.static_secret.diffie_hellman(&peer_ephemeral);
143+
let combined =
144+
combine_dh_shares(ee.as_bytes(), es.as_bytes(), se.as_bytes());
146145
Session::from_shared(&combined, true)
147146
}
148147

149148
/// Complete as responder: receive initiator's static, derive session.
150149
/// Input: (initiator_ephemeral_pub, initiator_static_pub)
151150
pub fn complete_responder(self, peer_ephemeral: PublicKey, peer_static: PublicKey) -> Session {
152-
// Same as initiator: ee + ss (both sides compute same)
151+
// Mirror of the initiator so both derive the SAME (ee, es, se). Here the
152+
// peer is the initiator, so `peer_ephemeral = e_i`, `peer_static = s_i`:
153+
// ee = e_r . e_i es = s_r . e_i (== e_i . s_r) se = e_r . s_i (== s_i . e_r)
153154
let ee = self.ephemeral.diffie_hellman(&peer_ephemeral);
154-
let ee_bytes = *ee.as_bytes();
155-
156-
let ss = self.static_secret.diffie_hellman(&peer_static);
157-
let ss_bytes = *ss.as_bytes();
158-
159-
let combined = combine_dh_shares(&ee_bytes, &ss_bytes, &ss_bytes);
155+
let es = self.static_secret.diffie_hellman(&peer_ephemeral);
156+
let se = self.ephemeral.diffie_hellman(&peer_static);
157+
let combined =
158+
combine_dh_shares(ee.as_bytes(), es.as_bytes(), se.as_bytes());
160159
Session::from_shared(&combined, false)
161160
}
162161
}

0 commit comments

Comments
 (0)