Skip to main content

fips_core/noise/
mod.rs

1//! Noise Protocol Implementations for FIPS
2//!
3//! Implements Noise Protocol Framework patterns using secp256k1:
4//!
5//! - **IK pattern**: Used by FMP (link layer) for hop-by-hop peer authentication.
6//!   The initiator knows the responder's static key and sends its encrypted
7//!   static in msg1. Two-message handshake.
8//!
9//! - **XK pattern**: Used by FSP (session layer) for end-to-end sessions.
10//!   The initiator knows the responder's static key but defers revealing its
11//!   own identity until msg3, providing stronger identity hiding. Three-message
12//!   handshake.
13//!
14//! ## IK Handshake Pattern (Link Layer)
15//!
16//! ```text
17//!   <- s                    (pre-message: responder's static known)
18//!   -> e, es, s, ss         (msg1: ephemeral + encrypted static)
19//!   <- e, ee, se            (msg2: ephemeral)
20//! ```
21//!
22//! ## XK Handshake Pattern (Session Layer)
23//!
24//! ```text
25//!   <- s                    (pre-message: responder's static known)
26//!   -> e, es                (msg1: ephemeral + DH with responder's static)
27//!   <- e, ee                (msg2: ephemeral + DH)
28//!   -> s, se                (msg3: encrypted static + DH)
29//! ```
30//!
31//! ## Separation of Concerns
32//!
33//! The IK pattern handles **link-layer peer authentication** — securing the
34//! direct link between neighboring nodes. The XK pattern handles **session-layer
35//! end-to-end encryption** between arbitrary network addresses, with stronger
36//! initiator identity protection.
37
38mod handshake;
39mod replay;
40mod session;
41
42use ring::aead::{Aad, CHACHA20_POLY1305, LessSafeKey, Nonce, UnboundKey};
43use std::fmt;
44use std::sync::Arc;
45use thiserror::Error;
46
47pub use handshake::HandshakeState;
48pub use replay::ReplayWindow;
49pub use session::NoiseSession;
50
51/// Protocol name for Noise IK with secp256k1 (link layer).
52/// Format: Noise_IK_secp256k1_ChaChaPoly_SHA256
53pub(crate) const PROTOCOL_NAME_IK: &[u8] = b"Noise_IK_secp256k1_ChaChaPoly_SHA256";
54
55/// Protocol name for Noise XK with secp256k1 (session layer).
56/// Format: Noise_XK_secp256k1_ChaChaPoly_SHA256
57pub(crate) const PROTOCOL_NAME_XK: &[u8] = b"Noise_XK_secp256k1_ChaChaPoly_SHA256";
58
59/// Maximum message size for noise transport messages.
60pub const MAX_MESSAGE_SIZE: usize = 65535;
61
62/// Size of the AEAD tag.
63pub const TAG_SIZE: usize = 16;
64
65/// Size of a public key (compressed secp256k1).
66pub const PUBKEY_SIZE: usize = 33;
67
68/// Size of the startup epoch (random bytes for restart detection).
69pub const EPOCH_SIZE: usize = 8;
70
71/// Size of encrypted epoch (epoch + AEAD tag).
72pub const EPOCH_ENCRYPTED_SIZE: usize = EPOCH_SIZE + TAG_SIZE;
73
74/// Size of IK handshake message 1: ephemeral (33) + encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag).
75pub const HANDSHAKE_MSG1_SIZE: usize = PUBKEY_SIZE + PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE;
76
77/// Size of IK handshake message 2: ephemeral (33) + encrypted epoch (8 + 16 tag).
78pub const HANDSHAKE_MSG2_SIZE: usize = PUBKEY_SIZE + EPOCH_ENCRYPTED_SIZE;
79
80/// XK msg1: ephemeral only (33 bytes).
81pub const XK_HANDSHAKE_MSG1_SIZE: usize = PUBKEY_SIZE;
82
83/// XK msg2: ephemeral (33) + encrypted epoch (8 + 16 tag) = 57 bytes.
84pub const XK_HANDSHAKE_MSG2_SIZE: usize = PUBKEY_SIZE + EPOCH_ENCRYPTED_SIZE;
85
86/// XK msg3: encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag) = 73 bytes.
87pub const XK_HANDSHAKE_MSG3_SIZE: usize = PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE;
88
89/// Replay window size in packets (matching WireGuard).
90pub const REPLAY_WINDOW_SIZE: usize = 2048;
91
92/// Errors from Noise protocol operations.
93#[derive(Debug, Error)]
94pub enum NoiseError {
95    #[error("handshake not complete")]
96    HandshakeNotComplete,
97
98    #[error("handshake already complete")]
99    HandshakeAlreadyComplete,
100
101    #[error("wrong handshake state: expected {expected}, got {got}")]
102    WrongState { expected: String, got: String },
103
104    #[error("invalid public key")]
105    InvalidPublicKey,
106
107    #[error("decryption failed")]
108    DecryptionFailed,
109
110    #[error("encryption failed")]
111    EncryptionFailed,
112
113    #[error("message too large: {size} > {max}")]
114    MessageTooLarge { size: usize, max: usize },
115
116    #[error("message too short: expected at least {expected}, got {got}")]
117    MessageTooShort { expected: usize, got: usize },
118
119    #[error("nonce overflow")]
120    NonceOverflow,
121
122    #[error("replay detected: counter {0} already seen or too old")]
123    ReplayDetected(u64),
124
125    #[error("secp256k1 error: {0}")]
126    Secp256k1(#[from] secp256k1::Error),
127}
128
129/// Role in the handshake.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum HandshakeRole {
132    /// We initiated the connection.
133    Initiator,
134    /// They initiated the connection.
135    Responder,
136}
137
138impl fmt::Display for HandshakeRole {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            HandshakeRole::Initiator => write!(f, "initiator"),
142            HandshakeRole::Responder => write!(f, "responder"),
143        }
144    }
145}
146
147/// Which Noise pattern is being used for this handshake.
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum NoisePattern {
150    /// Noise IK: two-message handshake (link layer).
151    Ik,
152    /// Noise XK: three-message handshake (session layer).
153    Xk,
154}
155
156/// Handshake state machine states.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum HandshakeProgress {
159    /// Initial state, ready to send/receive message 1.
160    Initial,
161    /// Message 1 sent/received, ready for message 2.
162    Message1Done,
163    /// Message 2 sent/received, ready for message 3 (XK only).
164    Message2Done,
165    /// Handshake complete, ready for transport.
166    Complete,
167}
168
169impl fmt::Display for HandshakeProgress {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            HandshakeProgress::Initial => write!(f, "initial"),
173            HandshakeProgress::Message1Done => write!(f, "message1_done"),
174            HandshakeProgress::Message2Done => write!(f, "message2_done"),
175            HandshakeProgress::Complete => write!(f, "complete"),
176        }
177    }
178}
179
180/// Symmetric cipher state for post-handshake encryption.
181///
182/// AEAD is `ring`'s ChaCha20-Poly1305 (BoringSSL backend), which dispatches
183/// to NEON on aarch64 and AVX-512/AVX2 on x86_64. The `cipher` field caches
184/// a constructed `LessSafeKey` so we don't re-derive it per packet.
185/// Off-task send workers use a cheap `Arc` handle to that immutable key
186/// while the session state keeps counter assignment sequential.
187pub struct CipherState {
188    /// Encryption key (32 bytes). Retained so we can rebuild the keyed
189    /// AEAD for `cipher_clone()` (ring's `UnboundKey`/`LessSafeKey`
190    /// don't implement `Clone` deliberately for safety).
191    key: [u8; 32],
192    /// Cached keyed AEAD, valid iff `has_key`. None for an un-keyed state.
193    cipher: Option<Arc<LessSafeKey>>,
194    /// Nonce counter (8 bytes used, 4 bytes zero prefix).
195    pub(super) nonce: u64,
196    /// Whether this cipher has a valid key.
197    has_key: bool,
198}
199
200impl Clone for CipherState {
201    fn clone(&self) -> Self {
202        Self {
203            key: self.key,
204            cipher: self.cipher.clone(),
205            nonce: self.nonce,
206            has_key: self.has_key,
207        }
208    }
209}
210
211impl CipherState {
212    /// Create a new cipher state with the given key.
213    pub(crate) fn new(key: [u8; 32]) -> Self {
214        let cipher = Self::build_cipher(&key);
215        Self {
216            key,
217            cipher,
218            nonce: 0,
219            has_key: true,
220        }
221    }
222
223    /// Create an empty cipher state (no key yet).
224    pub(super) fn empty() -> Self {
225        Self {
226            key: [0u8; 32],
227            cipher: None,
228            nonce: 0,
229            has_key: false,
230        }
231    }
232
233    /// Initialize with a key.
234    pub(super) fn initialize_key(&mut self, key: [u8; 32]) {
235        self.key = key;
236        self.cipher = Self::build_cipher(&key);
237        self.nonce = 0;
238        self.has_key = true;
239    }
240
241    /// Build the cached ring `LessSafeKey` from raw key bytes.
242    fn build_cipher(key: &[u8; 32]) -> Option<Arc<LessSafeKey>> {
243        UnboundKey::new(&CHACHA20_POLY1305, key)
244            .ok()
245            .map(LessSafeKey::new)
246            .map(Arc::new)
247    }
248
249    /// Build an owned key for receive-side worker snapshots that intentionally
250    /// own their decrypt state instead of sharing it.
251    fn build_owned_cipher(key: &[u8; 32]) -> Option<LessSafeKey> {
252        UnboundKey::new(&CHACHA20_POLY1305, key)
253            .ok()
254            .map(LessSafeKey::new)
255    }
256
257    /// Encrypt plaintext, returning ciphertext with appended tag.
258    pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, NoiseError> {
259        if !self.has_key {
260            // No key means no encryption (shouldn't happen in transport phase)
261            return Ok(plaintext.to_vec());
262        }
263
264        if plaintext.len() > MAX_MESSAGE_SIZE - TAG_SIZE {
265            return Err(NoiseError::MessageTooLarge {
266                size: plaintext.len(),
267                max: MAX_MESSAGE_SIZE - TAG_SIZE,
268            });
269        }
270
271        let counter = self.advance_nonce()?;
272        seal(self.cipher.as_deref(), counter, &[], plaintext)
273    }
274
275    /// Decrypt ciphertext (with appended tag), returning plaintext.
276    ///
277    /// Uses the internal nonce counter. For transport phase with explicit
278    /// counters from the wire format, use `decrypt_with_counter` instead.
279    pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, NoiseError> {
280        if !self.has_key {
281            // No key means no encryption
282            return Ok(ciphertext.to_vec());
283        }
284
285        if ciphertext.len() < TAG_SIZE {
286            return Err(NoiseError::MessageTooShort {
287                expected: TAG_SIZE,
288                got: ciphertext.len(),
289            });
290        }
291
292        let counter = self.advance_nonce()?;
293        open(self.cipher.as_deref(), counter, &[], ciphertext)
294    }
295
296    /// Decrypt with an explicit counter value (for transport phase).
297    ///
298    /// This is used when the counter comes from the wire format rather than
299    /// an internal counter. The counter must be validated by a replay window
300    /// before calling this method.
301    pub fn decrypt_with_counter(
302        &self,
303        ciphertext: &[u8],
304        counter: u64,
305    ) -> Result<Vec<u8>, NoiseError> {
306        if !self.has_key {
307            return Ok(ciphertext.to_vec());
308        }
309
310        if ciphertext.len() < TAG_SIZE {
311            return Err(NoiseError::MessageTooShort {
312                expected: TAG_SIZE,
313                got: ciphertext.len(),
314            });
315        }
316
317        open(self.cipher.as_deref(), counter, &[], ciphertext)
318    }
319
320    /// Encrypt plaintext with Additional Authenticated Data (AAD).
321    ///
322    /// The AAD is authenticated but not encrypted. Used for the FMP
323    /// established frame format where the 16-byte outer header is
324    /// bound to the AEAD tag.
325    pub fn encrypt_with_aad(
326        &mut self,
327        plaintext: &[u8],
328        aad: &[u8],
329    ) -> Result<Vec<u8>, NoiseError> {
330        if !self.has_key {
331            return Ok(plaintext.to_vec());
332        }
333
334        if plaintext.len() > MAX_MESSAGE_SIZE - TAG_SIZE {
335            return Err(NoiseError::MessageTooLarge {
336                size: plaintext.len(),
337                max: MAX_MESSAGE_SIZE - TAG_SIZE,
338            });
339        }
340
341        let counter = self.advance_nonce()?;
342        seal(self.cipher.as_deref(), counter, aad, plaintext)
343    }
344
345    /// Encrypt plaintext with an explicit counter (no AAD).
346    ///
347    /// Symmetric to `decrypt_with_counter`: takes `&self` and a caller-
348    /// supplied counter rather than mutating the internal nonce. Intended
349    /// for pipelined encrypt paths where a dispatcher pre-assigns counters
350    /// and fans the AEAD work out across worker threads. Callers are
351    /// responsible for ensuring counter uniqueness — typically by holding
352    /// the cipher behind a lock or queue that hands out counters in order.
353    pub fn encrypt_with_counter(
354        &self,
355        plaintext: &[u8],
356        counter: u64,
357    ) -> Result<Vec<u8>, NoiseError> {
358        if !self.has_key {
359            return Ok(plaintext.to_vec());
360        }
361
362        if plaintext.len() > MAX_MESSAGE_SIZE - TAG_SIZE {
363            return Err(NoiseError::MessageTooLarge {
364                size: plaintext.len(),
365                max: MAX_MESSAGE_SIZE - TAG_SIZE,
366            });
367        }
368
369        seal(self.cipher.as_deref(), counter, &[], plaintext)
370    }
371
372    /// Encrypt plaintext with an explicit counter and AAD.
373    ///
374    /// Symmetric to `decrypt_with_counter_and_aad`: takes `&self` and a
375    /// caller-supplied counter rather than mutating the internal nonce.
376    /// Same uniqueness contract as `encrypt_with_counter`.
377    pub fn encrypt_with_counter_and_aad(
378        &self,
379        plaintext: &[u8],
380        counter: u64,
381        aad: &[u8],
382    ) -> Result<Vec<u8>, NoiseError> {
383        if !self.has_key {
384            return Ok(plaintext.to_vec());
385        }
386
387        if plaintext.len() > MAX_MESSAGE_SIZE - TAG_SIZE {
388            return Err(NoiseError::MessageTooLarge {
389                size: plaintext.len(),
390                max: MAX_MESSAGE_SIZE - TAG_SIZE,
391            });
392        }
393
394        seal(self.cipher.as_deref(), counter, aad, plaintext)
395    }
396
397    /// Construct an independent keyed AEAD pinned to this cipher's key.
398    ///
399    /// Returns `None` for an empty (un-keyed) state. The returned key is
400    /// freshly built from the retained 32-byte key material — ring's
401    /// `LessSafeKey` doesn't implement `Clone` deliberately. Rebuilding from
402    /// the retained bytes is fine for infrequent owned receive snapshots; hot
403    /// send workers use [`Self::cipher_handle`] to avoid this per packet.
404    pub fn cipher_clone(&self) -> Option<LessSafeKey> {
405        if self.has_key {
406            Self::build_owned_cipher(&self.key)
407        } else {
408            None
409        }
410    }
411
412    /// Cheaply clone a handle to the cached keyed AEAD.
413    ///
414    /// `LessSafeKey` is immutable for explicit-counter AEAD calls. Send
415    /// workers receive counters reserved by the owning session, so sharing the
416    /// key handle does not share nonce state.
417    pub(crate) fn cipher_handle(&self) -> Option<Arc<LessSafeKey>> {
418        self.cipher.clone()
419    }
420
421    /// Decrypt with an explicit counter and AAD (for transport phase).
422    ///
423    /// Combines explicit counter (from wire format) with AAD verification.
424    /// The AAD must match exactly what was used during encryption or the
425    /// AEAD tag verification will fail.
426    pub fn decrypt_with_counter_and_aad(
427        &self,
428        ciphertext: &[u8],
429        counter: u64,
430        aad: &[u8],
431    ) -> Result<Vec<u8>, NoiseError> {
432        if !self.has_key {
433            return Ok(ciphertext.to_vec());
434        }
435
436        if ciphertext.len() < TAG_SIZE {
437            return Err(NoiseError::MessageTooShort {
438                expected: TAG_SIZE,
439                got: ciphertext.len(),
440            });
441        }
442
443        open(self.cipher.as_deref(), counter, aad, ciphertext)
444    }
445
446    /// In-place variant of [`Self::decrypt_with_counter_and_aad`].
447    ///
448    /// On entry, `buf` holds `ciphertext + 16-byte AEAD tag`. On
449    /// successful return, `buf[..returned_len]` holds the plaintext.
450    /// Saves one heap alloc + memcpy per packet versus the by-value
451    /// variant — at multi-Gbps that's a real chunk of the rx_loop's
452    /// per-packet cost.
453    ///
454    /// If the cipher has no key (handshake-not-yet-complete fallback),
455    /// `buf` is treated as already-plaintext and the full length is
456    /// returned unchanged.
457    pub fn decrypt_with_counter_and_aad_in_place(
458        &self,
459        buf: &mut [u8],
460        counter: u64,
461        aad: &[u8],
462    ) -> Result<usize, NoiseError> {
463        if !self.has_key {
464            return Ok(buf.len());
465        }
466        open_in_place(self.cipher.as_deref(), counter, aad, buf)
467    }
468
469    /// Build a ring `Nonce` from a counter value (8-byte LE counter, with
470    /// 4-byte zero prefix to match the Noise/WireGuard wire format).
471    /// Public-in-crate helper so the off-task encrypt/decrypt path on
472    /// callers (e.g. `recv_cipher_clone`) can produce a matching nonce.
473    pub(crate) fn counter_to_nonce(counter: u64) -> Nonce {
474        let mut nonce_bytes = [0u8; 12];
475        nonce_bytes[4..12].copy_from_slice(&counter.to_le_bytes());
476        Nonce::assume_unique_for_key(nonce_bytes)
477    }
478
479    /// Reserve and return the next nonce, advancing the internal counter.
480    fn advance_nonce(&mut self) -> Result<u64, NoiseError> {
481        if self.nonce == u64::MAX {
482            return Err(NoiseError::NonceOverflow);
483        }
484        let n = self.nonce;
485        self.nonce += 1;
486        Ok(n)
487    }
488
489    /// Get the current nonce value (for debugging/testing).
490    pub fn nonce(&self) -> u64 {
491        self.nonce
492    }
493
494    /// Check if cipher has a key.
495    pub fn has_key(&self) -> bool {
496        self.has_key
497    }
498}
499
500impl fmt::Debug for CipherState {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        f.debug_struct("CipherState")
503            .field("nonce", &self.nonce)
504            .field("has_key", &self.has_key)
505            .field("key", &"[redacted]")
506            .finish()
507    }
508}
509
510/// Encrypt `plaintext` with the given keyed AEAD, counter, and AAD,
511/// returning a `Vec<u8>` of `plaintext.len() + TAG_SIZE` bytes (ring's
512/// `seal_in_place_append_tag` works on a single buffer; we own it here
513/// to keep the public Vec-returning API of `CipherState`).
514///
515/// Module-private so explicit-counter paths inside `noise` can reuse the
516/// exact same allocation + AEAD pattern.
517pub(crate) fn seal(
518    cipher: Option<&LessSafeKey>,
519    counter: u64,
520    aad: &[u8],
521    plaintext: &[u8],
522) -> Result<Vec<u8>, NoiseError> {
523    let cipher = cipher.ok_or(NoiseError::EncryptionFailed)?;
524    let mut buf = Vec::with_capacity(plaintext.len() + TAG_SIZE);
525    buf.extend_from_slice(plaintext);
526    let nonce = CipherState::counter_to_nonce(counter);
527    cipher
528        .seal_in_place_append_tag(nonce, Aad::from(aad), &mut buf)
529        .map_err(|_| NoiseError::EncryptionFailed)?;
530    Ok(buf)
531}
532
533/// Decrypt `ciphertext` (with appended tag) with the given keyed AEAD,
534/// counter, and AAD, returning the plaintext as a `Vec<u8>`. Truncates
535/// in place to drop the AEAD tag.
536pub(crate) fn open(
537    cipher: Option<&LessSafeKey>,
538    counter: u64,
539    aad: &[u8],
540    ciphertext: &[u8],
541) -> Result<Vec<u8>, NoiseError> {
542    let cipher = cipher.ok_or(NoiseError::DecryptionFailed)?;
543    let mut buf = ciphertext.to_vec();
544    let nonce = CipherState::counter_to_nonce(counter);
545    let plaintext_len = cipher
546        .open_in_place(nonce, Aad::from(aad), &mut buf)
547        .map_err(|_| NoiseError::DecryptionFailed)?
548        .len();
549    buf.truncate(plaintext_len);
550    Ok(buf)
551}
552
553/// In-place variant of [`open`] — decrypts `buf` (which on entry holds
554/// `ciphertext + 16-byte AEAD tag`) into the same buffer, returning the
555/// plaintext length. The caller can then slice `&buf[..plaintext_len]`
556/// without any heap allocation.
557///
558/// Saves one ~1.4 KB heap alloc + memcpy per packet on the FMP / FSP
559/// receive hot path versus the by-value [`open`] variant (which
560/// internally does `ciphertext.to_vec()` before calling
561/// `open_in_place`). At 113 kpps that's ~150 MB/s of memory traffic
562/// dropped per AEAD step, and a meaningful chunk of the rx_loop's
563/// per-packet cost.
564///
565/// Returns `NoiseError::DecryptionFailed` if the AEAD tag check fails,
566/// the cipher has no key, or the buffer is shorter than the tag.
567pub(crate) fn open_in_place(
568    cipher: Option<&LessSafeKey>,
569    counter: u64,
570    aad: &[u8],
571    buf: &mut [u8],
572) -> Result<usize, NoiseError> {
573    let cipher = cipher.ok_or(NoiseError::DecryptionFailed)?;
574    if buf.len() < TAG_SIZE {
575        return Err(NoiseError::MessageTooShort {
576            expected: TAG_SIZE,
577            got: buf.len(),
578        });
579    }
580    let nonce = CipherState::counter_to_nonce(counter);
581    let plaintext = cipher
582        .open_in_place(nonce, Aad::from(aad), buf)
583        .map_err(|_| NoiseError::DecryptionFailed)?;
584    Ok(plaintext.len())
585}
586
587#[cfg(test)]
588mod tests;