Skip to main content

commonware_cryptography/
handshake.rs

1//! This module provides an authenticated key exchange protocol, or handshake.
2//!
3//! # Design
4//!
5//! The **dialer** and the **listener** both have a public identity, known to each other in advance.
6//! The goal of the handshake is to establish a shared, encrypted, and authenticated communication
7//! channel between these two parties. No third party should be able to read messages, or send
8//! messates along the channel.
9//!
10//! A three-message handshake is used to authenticate peers and establish a shared secret. The
11//! **dialer** initiates the connection, and the **listener** responds.
12//!
13//! [Syn] The dialer starts by sending a signed message with their ephemeral key.
14//!
15//! [SynAck] The listener responds by sending back their ephemeral key, along with a signature over the
16//! protocol transcript thus far. They can also derive a shared secret, which they use to generate
17//! a confirmation tag, also sent to the dialer.
18//!
19//! [Ack] The dialer verifies the signed message, then derives the same secret, and uses
20//! that to send their own confirmation back to the listener.
21//!
22//! The listener then verifies this confirmation.
23//!
24//! The shared secret can then be used to derive to AEAD keys, for the sending data ([SendCipher])
25//! and receiving data ([RecvCipher]). These use ChaCha20-Poly1305 as the AEAD. Each direction has
26//! a 12 byte counter to used as a nonce, with every call to [SendCipher::send] on one end,
27//! or [RecvCipher::recv] on the other end incrementing this counter. This guarantees that if
28//! a message is successfully received, then it was delivered in order. Re-ordering messages on
29//! the wire will have the effect of producing errors on the receiving end, but not of producing
30//! successful messages in a different order.
31//!
32//! # Security Features
33//!
34//! The protocol includes timestamp validation to protect against replay attacks and clock skew:
35//! - Messages with timestamps too old are rejected to prevent replay attacks
36//! - Messages with timestamps too far in the future are rejected to safeguard against clock skew
37use crate::{
38    PublicKey, Signature, Signer, Verifier,
39    transcript::{Summary, Transcript, Version},
40};
41use commonware_codec::{Encode, FixedSize, Read, ReadExt, Write};
42use core::ops::Range;
43use rand_core::CryptoRng;
44
45mod error;
46pub use error::Error;
47
48mod key_exchange;
49use key_exchange::{EphemeralPublicKey, SecretKey};
50
51mod cipher;
52pub use cipher::{RecvCipher, SendCipher, TAG_SIZE};
53
54#[cfg(all(test, feature = "arbitrary"))]
55mod conformance;
56
57const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_HANDSHAKE";
58const LABEL_CIPHER_L2D: &[u8] = b"cipher_l2d";
59const LABEL_CIPHER_D2L: &[u8] = b"cipher_d2l";
60const LABEL_CONFIRMATION_L2D: &[u8] = b"confirmation_l2d";
61const LABEL_CONFIRMATION_D2L: &[u8] = b"confirmation_d2l";
62
63// V0 is safe because the application namespace is summarized as a single packet before the
64// handshake commits a fixed sequence of canonical encodings at fixed positions.
65const TRANSCRIPT_VERSION: Version = Version::V0;
66
67/// First handshake message sent by the dialer.
68/// Contains dialer's ephemeral key and timestamp signature.
69#[cfg_attr(test, derive(Debug, PartialEq))]
70pub struct Syn<S: Signature> {
71    time_ms: u64,
72    epk: EphemeralPublicKey,
73    sig: S,
74}
75
76impl<S: Signature> FixedSize for Syn<S> {
77    const SIZE: usize = u64::SIZE + EphemeralPublicKey::SIZE + S::SIZE;
78}
79
80impl<S: Signature + Write> Write for Syn<S> {
81    fn write(&self, buf: &mut impl bytes::BufMut) {
82        self.time_ms.write(buf);
83        self.epk.write(buf);
84        self.sig.write(buf);
85    }
86}
87
88impl<S: Signature + Read> Read for Syn<S> {
89    type Cfg = S::Cfg;
90
91    fn read_cfg(
92        buf: &mut impl bytes::Buf,
93        cfg: &Self::Cfg,
94    ) -> Result<Self, commonware_codec::Error> {
95        Ok(Self {
96            time_ms: ReadExt::read(buf)?,
97            epk: ReadExt::read(buf)?,
98            sig: Read::read_cfg(buf, cfg)?,
99        })
100    }
101}
102
103#[cfg(feature = "arbitrary")]
104impl<S: Signature> arbitrary::Arbitrary<'_> for Syn<S>
105where
106    S: for<'a> arbitrary::Arbitrary<'a>,
107{
108    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
109        Ok(Self {
110            time_ms: u.arbitrary()?,
111            epk: u.arbitrary()?,
112            sig: u.arbitrary()?,
113        })
114    }
115}
116
117/// Second handshake message sent by the listener.
118/// Contains listener's ephemeral key, signature, and confirmation tag.
119#[cfg_attr(test, derive(Debug, PartialEq))]
120pub struct SynAck<S: Signature> {
121    time_ms: u64,
122    epk: EphemeralPublicKey,
123    sig: S,
124    confirmation: Summary,
125}
126
127impl<S: Signature> FixedSize for SynAck<S> {
128    const SIZE: usize = u64::SIZE + EphemeralPublicKey::SIZE + S::SIZE + Summary::SIZE;
129}
130
131impl<S: Signature + Write> Write for SynAck<S> {
132    fn write(&self, buf: &mut impl bytes::BufMut) {
133        self.time_ms.write(buf);
134        self.epk.write(buf);
135        self.sig.write(buf);
136        self.confirmation.write(buf);
137    }
138}
139
140impl<S: Signature + Read> Read for SynAck<S> {
141    type Cfg = S::Cfg;
142
143    fn read_cfg(
144        buf: &mut impl bytes::Buf,
145        cfg: &Self::Cfg,
146    ) -> Result<Self, commonware_codec::Error> {
147        Ok(Self {
148            time_ms: ReadExt::read(buf)?,
149            epk: ReadExt::read(buf)?,
150            sig: Read::read_cfg(buf, cfg)?,
151            confirmation: ReadExt::read(buf)?,
152        })
153    }
154}
155
156#[cfg(feature = "arbitrary")]
157impl<S: Signature> arbitrary::Arbitrary<'_> for SynAck<S>
158where
159    S: for<'a> arbitrary::Arbitrary<'a>,
160{
161    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
162        Ok(Self {
163            time_ms: u.arbitrary()?,
164            epk: u.arbitrary()?,
165            sig: u.arbitrary()?,
166            confirmation: u.arbitrary()?,
167        })
168    }
169}
170
171/// Third handshake message sent by the dialer.
172/// Contains dialer's confirmation tag to complete the handshake.
173#[cfg_attr(test, derive(PartialEq))]
174#[cfg_attr(feature = "arbitrary", derive(Debug, arbitrary::Arbitrary))]
175pub struct Ack {
176    confirmation: Summary,
177}
178
179impl FixedSize for Ack {
180    const SIZE: usize = Summary::SIZE;
181}
182
183impl Write for Ack {
184    fn write(&self, buf: &mut impl bytes::BufMut) {
185        self.confirmation.write(buf);
186    }
187}
188
189impl Read for Ack {
190    type Cfg = ();
191
192    fn read_cfg(
193        buf: &mut impl bytes::Buf,
194        _cfg: &Self::Cfg,
195    ) -> Result<Self, commonware_codec::Error> {
196        Ok(Self {
197            confirmation: ReadExt::read(buf)?,
198        })
199    }
200}
201
202/// State maintained by the dialer during handshake.
203/// Tracks ephemeral secret, peer identity, and protocol transcript.
204pub struct DialState<P> {
205    esk: SecretKey,
206    peer_identity: P,
207    transcript: Transcript,
208    ok_timestamps: Range<u64>,
209}
210
211/// State maintained by the listener during handshake.
212/// Tracks expected confirmation and derived ciphers.
213pub struct ListenState {
214    confirmation: Summary,
215    send: SendCipher,
216    recv: RecvCipher,
217}
218
219/// Handshake context containing timing and identity information.
220/// Used by both dialer and listener to initialize handshake state.
221pub struct Context<S, P> {
222    transcript: Transcript,
223    current_time: u64,
224    ok_timestamps: Range<u64>,
225    my_identity: S,
226    peer_identity: P,
227}
228
229impl<S, P> Context<S, P> {
230    /// Creates a new handshake context.
231    pub fn new(
232        namespace: &[u8],
233        current_time_ms: u64,
234        ok_timestamps: Range<u64>,
235        my_identity: S,
236        peer_identity: P,
237    ) -> Self {
238        let transcript = Transcript::new(namespace, TRANSCRIPT_VERSION).fork(NAMESPACE);
239        Self {
240            transcript,
241            current_time: current_time_ms,
242            ok_timestamps,
243            my_identity,
244            peer_identity,
245        }
246    }
247}
248
249/// Initiates a handshake as the dialer.
250/// Returns the dialer state and the first message to send.
251pub fn dial_start<S: Signer, P: PublicKey>(
252    rng: impl CryptoRng,
253    ctx: Context<S, P>,
254) -> (DialState<P>, Syn<<S as Signer>::Signature>) {
255    let Context {
256        current_time,
257        ok_timestamps,
258        my_identity,
259        peer_identity,
260        mut transcript,
261    } = ctx;
262    let esk = SecretKey::new(rng);
263    let epk = esk.public();
264    let sig = transcript
265        .commit(current_time.encode())
266        .commit(peer_identity.encode())
267        .commit(epk.encode())
268        .sign(&my_identity);
269    transcript.commit(my_identity.public_key().encode());
270    (
271        DialState {
272            esk,
273            peer_identity,
274            transcript,
275            ok_timestamps,
276        },
277        Syn {
278            time_ms: current_time,
279            epk,
280            sig,
281        },
282    )
283}
284
285/// Completes a handshake as the dialer.
286/// Verifies the listener's response and returns final message and ciphers.
287pub fn dial_end<P: PublicKey>(
288    state: DialState<P>,
289    msg: SynAck<<P as Verifier>::Signature>,
290) -> Result<(Ack, SendCipher, RecvCipher), Error> {
291    let DialState {
292        esk,
293        peer_identity,
294        mut transcript,
295        ok_timestamps,
296    } = state;
297    if !ok_timestamps.contains(&msg.time_ms) {
298        return Err(Error::InvalidTimestamp(msg.time_ms, ok_timestamps));
299    }
300    if !transcript
301        .commit(msg.time_ms.encode())
302        .commit(msg.epk.encode())
303        .verify(&peer_identity, &msg.sig)
304    {
305        return Err(Error::HandshakeFailed);
306    }
307    let Some(shared) = esk.exchange(&msg.epk) else {
308        return Err(Error::HandshakeFailed);
309    };
310    shared
311        .secret
312        .expose(|secret| transcript.commit(secret.as_ref()));
313    let recv = RecvCipher::new(transcript.noise(LABEL_CIPHER_L2D));
314    let send = SendCipher::new(transcript.noise(LABEL_CIPHER_D2L));
315    let confirmation_l2d = transcript.fork(LABEL_CONFIRMATION_L2D).summarize();
316    let confirmation_d2l = transcript.fork(LABEL_CONFIRMATION_D2L).summarize();
317    if msg.confirmation != confirmation_l2d {
318        return Err(Error::HandshakeFailed);
319    }
320
321    Ok((
322        Ack {
323            confirmation: confirmation_d2l,
324        },
325        send,
326        recv,
327    ))
328}
329
330/// Processes the first handshake message as the listener.
331/// Verifies the dialer's message and returns state and response.
332pub fn listen_start<S: Signer, P: PublicKey>(
333    rng: impl CryptoRng,
334    ctx: Context<S, P>,
335    msg: Syn<<P as Verifier>::Signature>,
336) -> Result<(ListenState, SynAck<<S as Signer>::Signature>), Error> {
337    let Context {
338        current_time,
339        my_identity,
340        peer_identity,
341        ok_timestamps,
342        mut transcript,
343    } = ctx;
344    if !ok_timestamps.contains(&msg.time_ms) {
345        return Err(Error::InvalidTimestamp(msg.time_ms, ok_timestamps));
346    }
347    if !transcript
348        .commit(msg.time_ms.encode())
349        .commit(my_identity.public_key().encode())
350        .commit(msg.epk.encode())
351        .verify(&peer_identity, &msg.sig)
352    {
353        return Err(Error::HandshakeFailed);
354    }
355    let esk = SecretKey::new(rng);
356    let epk = esk.public();
357    let sig = transcript
358        .commit(peer_identity.encode())
359        .commit(current_time.encode())
360        .commit(epk.encode())
361        .sign(&my_identity);
362    let Some(shared) = esk.exchange(&msg.epk) else {
363        return Err(Error::HandshakeFailed);
364    };
365    shared
366        .secret
367        .expose(|secret| transcript.commit(secret.as_ref()));
368    let send = SendCipher::new(transcript.noise(LABEL_CIPHER_L2D));
369    let recv = RecvCipher::new(transcript.noise(LABEL_CIPHER_D2L));
370    let confirmation_l2d = transcript.fork(LABEL_CONFIRMATION_L2D).summarize();
371    let confirmation_d2l = transcript.fork(LABEL_CONFIRMATION_D2L).summarize();
372
373    Ok((
374        ListenState {
375            confirmation: confirmation_d2l,
376            send,
377            recv,
378        },
379        SynAck {
380            time_ms: current_time,
381            epk,
382            sig,
383            confirmation: confirmation_l2d,
384        },
385    ))
386}
387
388/// Completes the handshake as the listener.
389/// Verifies the dialer's confirmation and returns established ciphers.
390pub fn listen_end(state: ListenState, msg: Ack) -> Result<(SendCipher, RecvCipher), Error> {
391    if msg.confirmation != state.confirmation {
392        return Err(Error::HandshakeFailed);
393    }
394    Ok((state.send, state.recv))
395}
396
397#[cfg(test)]
398mod test {
399    use super::*;
400    use crate::{Signer, ed25519::PrivateKey};
401    use commonware_codec::{Codec, DecodeExt};
402    use commonware_math::algebra::Random;
403    use commonware_utils::test_rng;
404
405    fn test_encode_roundtrip<T: Codec<Cfg = ()> + PartialEq>(value: &T) {
406        assert!(value == &<T as DecodeExt<_>>::decode(value.encode()).unwrap());
407    }
408
409    #[test]
410    fn test_can_setup_and_send_messages() -> Result<(), Error> {
411        let mut rng = test_rng();
412        let dialer_crypto = PrivateKey::random(&mut rng);
413        let listener_crypto = PrivateKey::random(&mut rng);
414
415        let (d_state, msg1) = dial_start(
416            &mut rng,
417            Context::new(
418                b"test_namespace",
419                0,
420                0..1,
421                dialer_crypto.clone(),
422                listener_crypto.public_key(),
423            ),
424        );
425        test_encode_roundtrip(&msg1);
426        let (l_state, msg2) = listen_start(
427            &mut rng,
428            Context::new(
429                b"test_namespace",
430                0,
431                0..1,
432                listener_crypto,
433                dialer_crypto.public_key(),
434            ),
435            msg1,
436        )?;
437        test_encode_roundtrip(&msg2);
438        let (msg3, mut d_send, mut d_recv) = dial_end(d_state, msg2)?;
439        test_encode_roundtrip(&msg3);
440        let (mut l_send, mut l_recv) = listen_end(l_state, msg3)?;
441
442        let m1: &'static [u8] = b"message 1";
443
444        let c1 = d_send.send(m1)?;
445        let m1_prime = l_recv.recv(&c1)?;
446        assert_eq!(m1, &m1_prime);
447
448        let m2: &'static [u8] = b"message 2";
449        let c2 = l_send.send(m2)?;
450        let m2_prime = d_recv.recv(&c2)?;
451        assert_eq!(m2, &m2_prime);
452
453        Ok(())
454    }
455
456    #[test]
457    fn test_mismatched_namespace_fails() {
458        let mut rng = test_rng();
459        let dialer_crypto = PrivateKey::random(&mut rng);
460        let listener_crypto = PrivateKey::random(&mut rng);
461
462        let (_, msg1) = dial_start(
463            &mut rng,
464            Context::new(
465                b"namespace_a",
466                0,
467                0..1,
468                dialer_crypto.clone(),
469                listener_crypto.public_key(),
470            ),
471        );
472
473        let result = listen_start(
474            &mut rng,
475            Context::new(
476                b"namespace_b",
477                0,
478                0..1,
479                listener_crypto,
480                dialer_crypto.public_key(),
481            ),
482            msg1,
483        );
484
485        assert!(matches!(result, Err(Error::HandshakeFailed)));
486    }
487
488    #[cfg(feature = "arbitrary")]
489    mod conformance {
490        use super::*;
491        use commonware_codec::conformance::CodecConformance;
492
493        commonware_conformance::conformance_tests! {
494            CodecConformance<Syn<crate::ed25519::Signature>>,
495            CodecConformance<SynAck<crate::ed25519::Signature>>,
496            CodecConformance<Ack>,
497        }
498    }
499}