Skip to main content

pim_crypto/
handshake.rs

1//! Authenticated peer handshake and session-key derivation.
2
3use ed25519_dalek::{Signer, Verifier};
4use hkdf::Hkdf;
5use hmac::{Hmac, Mac};
6use rand::rngs::OsRng;
7use sha2::Sha256;
8use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
9
10use crate::identity::Identity;
11
12type HmacSha256 = Hmac<Sha256>;
13
14/// The result of a successful handshake: a shared session key.
15#[derive(Clone)]
16pub struct SessionKey {
17    key: [u8; 32],
18}
19
20impl SessionKey {
21    /// Return the raw 32-byte session key material.
22    pub fn as_bytes(&self) -> &[u8; 32] {
23        &self.key
24    }
25}
26
27/// Data sent by the initiator to start a handshake.
28pub struct HandshakeInit {
29    /// Initiator's long-term Ed25519 public key.
30    pub sender_pub: [u8; 32],
31    /// Ephemeral X25519 public key for this handshake.
32    pub ephemeral_pub: [u8; 32],
33    /// Random nonce.
34    pub nonce: [u8; 32],
35    /// Ed25519 signature over (ephemeral_pub || nonce).
36    pub signature: [u8; 64],
37}
38
39/// Data sent by the responder.
40pub struct HandshakeResponse {
41    /// Responder's long-term Ed25519 public key.
42    pub sender_pub: [u8; 32],
43    /// Ephemeral X25519 public key.
44    pub ephemeral_pub: [u8; 32],
45    /// Random nonce.
46    pub nonce: [u8; 32],
47    /// Ed25519 signature over (ephemeral_pub || nonce).
48    pub signature: [u8; 64],
49}
50
51/// Confirmation message containing HMAC of the handshake transcript.
52pub struct HandshakeConfirm {
53    /// Transcript MAC proving both sides derived the same session key.
54    pub hmac: [u8; 32],
55}
56
57/// Manages one side of the handshake protocol.
58///
59/// Usage:
60/// - Initiator: `initiate()` → send Init → receive Response → `finalize_initiator()` → send Confirm
61/// - Responder: receive Init → `respond()` → send Response → receive Confirm → `verify_confirm()`
62///
63/// **Private-mesh binding.** If both sides call
64/// [`Handshaker::with_mesh_handshake_key`] with the same 32-byte secret
65/// (typically [`crate::MeshSecret::handshake_key`]), the secret is mixed
66/// into the HKDF IKM and both sides derive the same session key. If the
67/// secrets differ — or if one side sets it and the other doesn't — the
68/// derived session keys diverge, the transcript HMAC fails, and the
69/// handshake is rejected. This is how the open-mesh ↔ private-mesh
70/// boundary is enforced: the divergence is silent on the wire (no new
71/// frame fields) but cryptographically definitive.
72pub struct Handshaker {
73    identity: HandshakeIdentity,
74    ephemeral_secret: Option<StaticSecret>,
75    ephemeral_pub: Option<[u8; 32]>,
76    session_key: Option<SessionKey>,
77    transcript: Vec<u8>,
78    mesh_handshake_key: Option<[u8; 32]>,
79}
80
81/// Subset of Identity needed for handshake (avoids lifetime issues).
82struct HandshakeIdentity {
83    signing_key_bytes: [u8; 32],
84    verifying_key_bytes: [u8; 32],
85}
86
87impl Handshaker {
88    /// Create a handshake state machine bound to the local node identity.
89    pub fn new(identity: &Identity) -> Self {
90        Self {
91            identity: HandshakeIdentity {
92                signing_key_bytes: identity.signing_key().to_bytes(),
93                verifying_key_bytes: identity.public_key_bytes(),
94            },
95            ephemeral_secret: None,
96            ephemeral_pub: None,
97            session_key: None,
98            transcript: Vec::new(),
99            mesh_handshake_key: None,
100        }
101    }
102
103    /// Bind this handshake to a private mesh by mixing the supplied
104    /// 32-byte secret into the session-key derivation IKM. Must be
105    /// called before [`Self::respond`] or [`Self::finalize_initiator`].
106    /// See struct docs for the open ↔ private rejection semantics.
107    pub fn with_mesh_handshake_key(mut self, key: [u8; 32]) -> Self {
108        self.mesh_handshake_key = Some(key);
109        self
110    }
111
112    /// Build the HKDF input keying material: X25519 shared secret
113    /// optionally followed by the mesh handshake key. Empty mesh key →
114    /// open-mesh derivation; populated → private-mesh derivation that
115    /// won't match an open peer's HKDF output.
116    fn ikm_with_mesh(&self, shared_secret: &[u8]) -> Vec<u8> {
117        let mut ikm = Vec::with_capacity(shared_secret.len() + 32);
118        ikm.extend_from_slice(shared_secret);
119        if let Some(mesh_key) = &self.mesh_handshake_key {
120            ikm.extend_from_slice(mesh_key);
121        }
122        ikm
123    }
124
125    /// Initiator step 1: produce a HandshakeInit.
126    pub fn initiate(&mut self) -> HandshakeInit {
127        let ephemeral_secret = StaticSecret::random_from_rng(OsRng);
128        let ephemeral_pub = X25519PublicKey::from(&ephemeral_secret);
129        let ephemeral_pub_bytes = ephemeral_pub.to_bytes();
130
131        let mut nonce = [0u8; 32];
132        rand::RngCore::fill_bytes(&mut OsRng, &mut nonce);
133
134        // Sign (ephemeral_pub || nonce)
135        let mut msg = Vec::with_capacity(64);
136        msg.extend_from_slice(&ephemeral_pub_bytes);
137        msg.extend_from_slice(&nonce);
138
139        let signing_key = ed25519_dalek::SigningKey::from_bytes(&self.identity.signing_key_bytes);
140        let signature = signing_key.sign(&msg);
141
142        // Store state
143        self.ephemeral_secret = Some(ephemeral_secret);
144        self.ephemeral_pub = Some(ephemeral_pub_bytes);
145
146        let init = HandshakeInit {
147            sender_pub: self.identity.verifying_key_bytes,
148            ephemeral_pub: ephemeral_pub_bytes,
149            nonce,
150            signature: signature.to_bytes(),
151        };
152
153        // Append to transcript
154        self.transcript.extend_from_slice(&init.sender_pub);
155        self.transcript.extend_from_slice(&init.ephemeral_pub);
156        self.transcript.extend_from_slice(&init.nonce);
157
158        init
159    }
160
161    /// Responder step: verify the init and produce a response + derive session key.
162    pub fn respond(&mut self, init: &HandshakeInit) -> Result<HandshakeResponse, HandshakeError> {
163        // Verify initiator's signature
164        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&init.sender_pub)
165            .map_err(|_| HandshakeError::InvalidPublicKey)?;
166
167        let mut msg = Vec::with_capacity(64);
168        msg.extend_from_slice(&init.ephemeral_pub);
169        msg.extend_from_slice(&init.nonce);
170
171        let signature = ed25519_dalek::Signature::from_bytes(&init.signature);
172        verifying_key
173            .verify(&msg, &signature)
174            .map_err(|_| HandshakeError::InvalidSignature)?;
175
176        // Generate our ephemeral key
177        let ephemeral_secret = StaticSecret::random_from_rng(OsRng);
178        let ephemeral_pub = X25519PublicKey::from(&ephemeral_secret);
179        let ephemeral_pub_bytes = ephemeral_pub.to_bytes();
180
181        let mut nonce = [0u8; 32];
182        rand::RngCore::fill_bytes(&mut OsRng, &mut nonce);
183
184        // Sign our (ephemeral_pub || nonce)
185        let mut sign_msg = Vec::with_capacity(64);
186        sign_msg.extend_from_slice(&ephemeral_pub_bytes);
187        sign_msg.extend_from_slice(&nonce);
188
189        let signing_key = ed25519_dalek::SigningKey::from_bytes(&self.identity.signing_key_bytes);
190        let signature = signing_key.sign(&sign_msg);
191
192        // Derive shared secret: X25519(our_ephemeral, their_ephemeral_pub)
193        let their_ephemeral = X25519PublicKey::from(init.ephemeral_pub);
194        let shared_secret = ephemeral_secret.diffie_hellman(&their_ephemeral);
195
196        // Build transcript
197        self.transcript.extend_from_slice(&init.sender_pub);
198        self.transcript.extend_from_slice(&init.ephemeral_pub);
199        self.transcript.extend_from_slice(&init.nonce);
200        self.transcript
201            .extend_from_slice(&self.identity.verifying_key_bytes);
202        self.transcript.extend_from_slice(&ephemeral_pub_bytes);
203        self.transcript.extend_from_slice(&nonce);
204
205        // Derive session key via HKDF. IKM = X25519 || optional mesh key
206        // (see struct docs for the open ↔ private rejection semantics).
207        let mut salt = Vec::with_capacity(64);
208        salt.extend_from_slice(&init.nonce);
209        salt.extend_from_slice(&nonce);
210
211        let ikm = self.ikm_with_mesh(shared_secret.as_bytes());
212        let hk = Hkdf::<Sha256>::new(Some(&salt), &ikm);
213        let mut key = [0u8; 32];
214        hk.expand(b"pim-session-v1", &mut key)
215            .expect("32 bytes is valid for HKDF-SHA256");
216
217        self.session_key = Some(SessionKey { key });
218        self.ephemeral_pub = Some(ephemeral_pub_bytes);
219
220        Ok(HandshakeResponse {
221            sender_pub: self.identity.verifying_key_bytes,
222            ephemeral_pub: ephemeral_pub_bytes,
223            nonce,
224            signature: signature.to_bytes(),
225        })
226    }
227
228    /// Initiator step 2: verify response and derive session key.
229    pub fn finalize_initiator(
230        &mut self,
231        response: &HandshakeResponse,
232    ) -> Result<(), HandshakeError> {
233        // Verify responder's signature
234        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&response.sender_pub)
235            .map_err(|_| HandshakeError::InvalidPublicKey)?;
236
237        let mut msg = Vec::with_capacity(64);
238        msg.extend_from_slice(&response.ephemeral_pub);
239        msg.extend_from_slice(&response.nonce);
240
241        let signature = ed25519_dalek::Signature::from_bytes(&response.signature);
242        verifying_key
243            .verify(&msg, &signature)
244            .map_err(|_| HandshakeError::InvalidSignature)?;
245
246        // Derive shared secret
247        let ephemeral_secret = self
248            .ephemeral_secret
249            .take()
250            .ok_or(HandshakeError::InvalidState)?;
251        let their_ephemeral = X25519PublicKey::from(response.ephemeral_pub);
252        let shared_secret = ephemeral_secret.diffie_hellman(&their_ephemeral);
253
254        // Extend transcript
255        self.transcript.extend_from_slice(&response.sender_pub);
256        self.transcript.extend_from_slice(&response.ephemeral_pub);
257        self.transcript.extend_from_slice(&response.nonce);
258
259        // Derive session key via HKDF (same salt + IKM construction as responder).
260        // The init nonce is the first 32 bytes of our existing transcript after sender_pub + ephemeral_pub
261        // We stored: init.sender_pub(32) + init.ephemeral_pub(32) + init.nonce(32) = bytes 64..96
262        let init_nonce = &self.transcript[64..96];
263        let mut salt = Vec::with_capacity(64);
264        salt.extend_from_slice(init_nonce);
265        salt.extend_from_slice(&response.nonce);
266
267        let ikm = self.ikm_with_mesh(shared_secret.as_bytes());
268        let hk = Hkdf::<Sha256>::new(Some(&salt), &ikm);
269        let mut key = [0u8; 32];
270        hk.expand(b"pim-session-v1", &mut key)
271            .expect("32 bytes is valid for HKDF-SHA256");
272
273        self.session_key = Some(SessionKey { key });
274        Ok(())
275    }
276
277    /// Produce a confirm message (HMAC of transcript with session key).
278    pub fn make_confirm(&self) -> Result<HandshakeConfirm, HandshakeError> {
279        let session_key = self
280            .session_key
281            .as_ref()
282            .ok_or(HandshakeError::InvalidState)?;
283        let mut mac =
284            HmacSha256::new_from_slice(session_key.as_bytes()).expect("HMAC accepts any key size");
285        mac.update(&self.transcript);
286        let result = mac.finalize().into_bytes();
287        let mut hmac = [0u8; 32];
288        hmac.copy_from_slice(&result);
289        Ok(HandshakeConfirm { hmac })
290    }
291
292    /// Verify a confirm message from the peer.
293    pub fn verify_confirm(&self, confirm: &HandshakeConfirm) -> Result<(), HandshakeError> {
294        let session_key = self
295            .session_key
296            .as_ref()
297            .ok_or(HandshakeError::InvalidState)?;
298        let mut mac =
299            HmacSha256::new_from_slice(session_key.as_bytes()).expect("HMAC accepts any key size");
300        mac.update(&self.transcript);
301        mac.verify_slice(&confirm.hmac)
302            .map_err(|_| HandshakeError::ConfirmMismatch)
303    }
304
305    /// Get the derived session key (only available after finalization).
306    pub fn session_key(&self) -> Option<&SessionKey> {
307        self.session_key.as_ref()
308    }
309}
310
311#[derive(Debug, thiserror::Error)]
312/// Errors returned when the authenticated handshake fails.
313pub enum HandshakeError {
314    /// The peer supplied an invalid Ed25519 public key.
315    #[error("invalid public key")]
316    InvalidPublicKey,
317    /// The peer signature on handshake material did not verify.
318    #[error("invalid signature")]
319    InvalidSignature,
320    /// The transcript confirmation MAC did not match.
321    #[error("handshake confirm mismatch")]
322    ConfirmMismatch,
323    /// The handshake API was called in an invalid order.
324    #[error("invalid handshake state")]
325    InvalidState,
326}
327
328#[cfg(test)]
329mod tests;