snarkos_node_network/noise.rs
1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Shared plumbing for handshakes based on the [Noise-XX] pattern.
17//!
18//! The Noise static keys are generated per connection: they exist solely to give the pattern a
19//! channel to bind to, and are never persisted. Node identity remains the Aleo account key, which
20//! is bound to the session by signing the running Noise handshake hash - see [`binding_message`].
21//!
22//! Note that the transport keys a completed session yields are only used to carry the last
23//! handshake message, and are then dropped.
24//!
25//! [Noise-XX]: https://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental
26
27use bytes::BytesMut;
28use snow::{Builder, HandshakeState, TransportState, params::NoiseParams};
29use std::io;
30use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
31use tokio_util::codec::Framed;
32
33/// The Noise handshake pattern used by snarkOS.
34pub const NOISE_PARAMS: &str = "Noise_XX_25519_ChaChaPoly_BLAKE2s";
35
36/// The prefix that marks a stream as speaking the Noise handshake.
37///
38/// A peer that only knows the legacy handshake reads these bytes as the little-endian `u32` length
39/// prefix of the first frame; the value is `0xFF00_1EAE` (~4.3 GiB), which is far beyond the 1 MiB
40/// frame limit its handshake codec allows, so it rejects the connection immediately rather than
41/// stalling or misparsing it.
42///
43/// The prefix travels ahead of the Noise stream and so is not a Noise message, but it is fed to the
44/// pattern as its prologue, which mixes it into the handshake hash on both sides. It is therefore
45/// covered by the signatures that bind the two identities to the session, exactly like the payloads
46/// are - if it is ever tampered with in flight, the two sides derive different hashes and the
47/// handshake fails. Nothing that precedes the pattern should be left out of the prologue: the moment
48/// the preamble carries anything negotiable, an unauthenticated one is a downgrade attack.
49pub const NOISE_MAGIC: [u8; 4] = [0xAE, 0x1E, 0x00, 0xFF];
50
51/// The maximum length of a single Noise message, as mandated by the specification.
52pub const MAX_NOISE_MSG_LEN: usize = 65535;
53
54/// The length of the little-endian `u32` that prefixes each Noise message on the wire.
55///
56/// The specification's own convention is a two-byte big-endian prefix; four little-endian bytes are
57/// used instead so that the marker preceding the stream can be chosen to look like an impossible
58/// frame length to a peer that speaks the legacy handshake. See [`NOISE_MAGIC`].
59const LENGTH_PREFIX_LEN: usize = 4;
60
61/// An upper bound on the number of bytes a Noise message can add on top of its payload: an
62/// ephemeral public key, an encrypted static public key with its AEAD tag, and a tag for the payload
63/// itself.
64///
65/// No single message of the pattern carries all of them - the largest is the second, at `e, ee, s,
66/// es` - so this is deliberately an upper bound rather than an exact figure. It is used both to
67/// reject payloads that could not fit in a message and to size the buffer a message is written into,
68/// so it must not be an underestimate for *any* message: the second message is the one that makes
69/// the ephemeral key term necessary.
70const NOISE_OVERHEAD: usize = DH_LEN + (DH_LEN + TAG_LEN) + TAG_LEN;
71
72/// The length of the AEAD tag of the cipher function in [`NOISE_PARAMS`].
73const TAG_LEN: usize = 16;
74
75/// The length of the handshake hash, i.e. the digest length of the hash function in [`NOISE_PARAMS`].
76pub const HANDSHAKE_HASH_LEN: usize = 32;
77
78/// The maximum length of a message exchanged during the handshake itself.
79///
80/// The pattern's largest message is the third, at 307 bytes with the payloads this crate's callers
81/// put in it, so this leaves room for a field or two to be added without a change to what peers
82/// accept from one another. It is far below [`MAX_NOISE_MSG_LEN`], and deliberately so: until a peer
83/// has been checked over, the length it declares is the size of a buffer it gets to make the
84/// responder allocate, and there is no reason for that to be 64 KiB when the protocol needs a
85/// fraction of it. The transport phase keeps the specification's limit, which is what a chunked
86/// payload would need.
87///
88/// Note that this is a soft parameter of the wire protocol: raising it later is harmless, lowering
89/// it is not, so it is set with headroom rather than as tightly as today's messages allow.
90const MAX_HANDSHAKE_MSG_LEN: usize = 1024;
91
92// The handshake limit only means anything if it is the tighter of the two; a change that inverted
93// them would otherwise widen what an unchecked peer can ask for, silently.
94const _: () = assert!(MAX_HANDSHAKE_MSG_LEN < MAX_NOISE_MSG_LEN);
95
96/// The length of a public or private key of the Diffie-Hellman function in [`NOISE_PARAMS`].
97const DH_LEN: usize = 32;
98
99/// The offset of the payload within the pattern's first message.
100///
101/// That message is `e`: the initiator's ephemeral public key followed by the payload, neither of
102/// them encrypted, since no key has been established yet. The payload can therefore be read - though
103/// emphatically not trusted - before any key is derived, which is what lets a responder turn a peer
104/// away cheaply; see [`PendingSession`].
105///
106/// A unit test pins this against a message the pattern actually produced, so that a change to
107/// [`NOISE_PARAMS`] cannot invalidate it silently.
108const FIRST_MESSAGE_PAYLOAD_OFFSET: usize = DH_LEN;
109
110/// The side of a Noise session; note that this is the side of the *handshake*, which for all
111/// current callers coincides with the side of the underlying TCP connection.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum Role {
114 Initiator,
115 Responder,
116}
117
118impl Role {
119 /// The role tag included in the signed binding message.
120 const fn tag(&self) -> &'static [u8] {
121 match self {
122 Self::Initiator => b"|initiator",
123 Self::Responder => b"|responder",
124 }
125 }
126}
127
128/// Returns the message that a party must sign with its Aleo account key in order to bind that
129/// identity to the Noise session identified by `handshake_hash`.
130///
131/// The handshake hash commits to every key and payload exchanged so far, so a signature over it is
132/// only valid for the one session it was produced in. This is what makes relaying impossible: an
133/// attacker that terminates two separate Noise sessions and forwards the payloads between them
134/// derives a different handshake hash on each side, so neither signature verifies.
135///
136/// `domain` separates the handshakes of different subprotocols (e.g. the BFT gateway and the
137/// router), which a validator runs concurrently under the same Aleo key.
138pub fn binding_message(domain: &[u8], role: Role, handshake_hash: &[u8]) -> Vec<u8> {
139 let mut message = Vec::with_capacity(domain.len() + role.tag().len() + handshake_hash.len());
140 message.extend_from_slice(domain);
141 message.extend_from_slice(role.tag());
142 message.extend_from_slice(handshake_hash);
143 message
144}
145
146/// The handshake protocol a connection is speaking.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum HandshakeProtocol {
149 /// The Noise-based handshake, identified by the [`NOISE_MAGIC`] prefix.
150 Noise,
151 /// The legacy challenge-response handshake.
152 Legacy,
153}
154
155/// Announces to the peer that this side is about to perform a Noise handshake.
156///
157/// The marker is also the pattern's prologue, so it does not need to be authenticated separately;
158/// see [`NOISE_MAGIC`].
159pub async fn write_noise_magic<S: AsyncWrite + Unpin>(stream: &mut S) -> io::Result<()> {
160 stream.write_all(&NOISE_MAGIC).await
161}
162
163/// Determines which handshake protocol the peer is speaking by consuming the first
164/// [`NOISE_MAGIC`]-sized chunk of the stream.
165///
166/// Alongside the protocol, this returns the bytes that need to be handed back to the caller's
167/// decoder: empty for [`HandshakeProtocol::Noise`], since the magic is not part of the Noise
168/// stream, and the consumed prefix for [`HandshakeProtocol::Legacy`], where it is the beginning of
169/// the peer's first frame. Use [`prepare_framed`] to feed it back into a codec.
170///
171/// Note: this reads rather than peeks, as a peek is free to return fewer bytes than requested.
172pub async fn detect_handshake_protocol<S: AsyncRead + Unpin>(
173 stream: &mut S,
174) -> io::Result<(HandshakeProtocol, BytesMut)> {
175 let mut prefix = [0u8; NOISE_MAGIC.len()];
176 stream.read_exact(&mut prefix).await?;
177
178 if prefix == NOISE_MAGIC {
179 Ok((HandshakeProtocol::Noise, BytesMut::new()))
180 } else {
181 Ok((HandshakeProtocol::Legacy, BytesMut::from(&prefix[..])))
182 }
183}
184
185/// Frames the given stream with the given codec, pre-populating the read buffer with bytes that
186/// were consumed from the stream before it was framed.
187///
188/// This exists only so that the legacy handshake can be handed back the prefix that
189/// [`detect_handshake_protocol`] took from it; the Noise handshake reads its messages exactly and has
190/// no codec to seed. It goes away with the legacy path.
191pub fn prepare_framed<S: AsyncRead + AsyncWrite, C>(stream: S, codec: C, read_buf: &[u8]) -> Framed<S, C> {
192 let mut framed = Framed::new(stream, codec);
193 framed.read_buffer_mut().extend_from_slice(read_buf);
194 framed
195}
196
197/// Reads one length-prefixed Noise message from the given stream.
198///
199/// Both reads are exact, so this consumes the message and not a single byte more. That is what lets
200/// the stream be handed on to a reader with a codec of its own once the handshake is done: there is
201/// never anything buffered here for that reader to miss. Note that a buffering codec could not offer
202/// the same guarantee, as it reads whatever the socket has available.
203async fn read_frame<S: AsyncRead + Unpin>(stream: &mut S, max_len: usize) -> io::Result<Vec<u8>> {
204 let mut length = [0u8; LENGTH_PREFIX_LEN];
205 stream.read_exact(&mut length).await?;
206
207 // Bound the length before it is used to allocate, against whichever limit applies to the phase
208 // the session is in; anything larger is a protocol violation rather than a big message.
209 let length = u32::from_le_bytes(length) as usize;
210 if length > max_len {
211 return Err(invalid_data(format!("the Noise message is too large ({length} bytes)")));
212 }
213
214 let mut message = vec![0u8; length];
215 stream.read_exact(&mut message).await?;
216
217 Ok(message)
218}
219
220/// Writes one length-prefixed Noise message to the given stream.
221///
222/// The prefix and the body go out in a single write, so that emitting a message costs one syscall
223/// rather than two and the peer sees the two halves arrive together.
224async fn write_frame<S: AsyncWrite + Unpin>(stream: &mut S, message: &[u8], max_len: usize) -> io::Result<()> {
225 // `NoiseSession::send` already bounds its payload so that the message the pattern produces cannot
226 // reach this; the check is repeated here so that the cast below cannot silently truncate.
227 if message.len() > max_len {
228 return Err(invalid_data(format!("the Noise message is too large ({} bytes)", message.len())));
229 }
230
231 let mut framed = Vec::with_capacity(LENGTH_PREFIX_LEN + message.len());
232 framed.extend_from_slice(&(message.len() as u32).to_le_bytes());
233 framed.extend_from_slice(message);
234
235 stream.write_all(&framed).await?;
236 stream.flush().await
237}
238
239/// Returns a builder for the pattern in [`NOISE_PARAMS`], with [`NOISE_MAGIC`] as its prologue.
240fn builder<'a>() -> io::Result<Builder<'a>> {
241 // This is a compile-time constant, so a parsing failure is a bug rather than a runtime condition.
242 let params: NoiseParams = NOISE_PARAMS.parse().expect("the Noise parameters should be valid");
243 // The prologue brings the marker that precedes the pattern under the handshake hash; see
244 // `NOISE_MAGIC`.
245 Builder::new(params).prologue(NOISE_MAGIC.as_slice()).map_err(invalid_data)
246}
247
248/// Builds the handshake state for the given role, with a fresh static keypair.
249fn build_state(role: Role) -> io::Result<HandshakeState> {
250 // Any 32 bytes are a valid X25519 private key, as the scalar is clamped where it is used, so the
251 // key is taken straight from the RNG. `Builder::generate_keypair` would derive the public key
252 // here and the builder would derive it again when the state is built, spending a scalar
253 // multiplication that nothing reads.
254 let private_key: [u8; DH_LEN] = rand::random();
255 let builder = builder()?.local_private_key(&private_key).map_err(invalid_data)?;
256
257 match role {
258 Role::Initiator => builder.build_initiator(),
259 Role::Responder => builder.build_responder(),
260 }
261 .map_err(invalid_data)
262}
263
264fn invalid_data<E: std::fmt::Display>(err: E) -> io::Error {
265 io::Error::new(io::ErrorKind::InvalidData, err.to_string())
266}
267
268enum SessionState {
269 Handshake(Box<HandshakeState>),
270 Transport(Box<TransportState>),
271}
272
273/// A stream whose first Noise message has been read but not yet processed.
274///
275/// This exists so that a responder can act on the initiator's cleartext payload *before* deriving
276/// any keys. Everything up to [`PendingSession::into_session`] is reading and parsing, so a peer that
277/// is going to be turned away - one that is not an authorized validator, say - costs no scalar
278/// multiplications. Under a distributed flood of connection attempts, that is the difference between
279/// paying for five of them per rejected peer and paying for none.
280pub struct PendingSession<S> {
281 stream: S,
282 first_message: Vec<u8>,
283}
284
285impl<S: AsyncRead + AsyncWrite + Unpin> PendingSession<S> {
286 /// Reads the pattern's first message from the given stream.
287 ///
288 /// The caller is responsible for the [`NOISE_MAGIC`] prefix, which must already have been
289 /// consumed with [`detect_handshake_protocol`].
290 pub async fn accept(mut stream: S) -> io::Result<Self> {
291 // The bound is stated rather than taken from a session, as there is not one yet; this is by
292 // definition the pattern's first message, so the handshake limit is the one that applies.
293 let first_message = read_frame(&mut stream, MAX_HANDSHAKE_MSG_LEN).await?;
294
295 Ok(Self { stream, first_message })
296 }
297
298 /// Returns the payload the initiator sent alongside its ephemeral key.
299 ///
300 /// It is neither encrypted nor authenticated, so it is a claim and not a fact: anything acted on
301 /// here has to be re-checked against the authenticated copy later in the handshake.
302 pub fn first_payload(&self) -> io::Result<&[u8]> {
303 self.first_message
304 .get(FIRST_MESSAGE_PAYLOAD_OFFSET..)
305 .ok_or_else(|| invalid_data("the first Noise message is too short to carry a payload"))
306 }
307
308 /// Derives the responder's keys and processes the first message.
309 pub fn into_session(self) -> io::Result<NoiseSession<S>> {
310 let Self { stream, first_message } = self;
311 let mut session =
312 NoiseSession { stream, state: SessionState::Handshake(Box::new(build_state(Role::Responder)?)) };
313 // The payload was already exposed by `first_payload`, so it is discarded here; processing the
314 // message is what advances the pattern.
315 session.decrypt(&first_message)?;
316
317 Ok(session)
318 }
319}
320
321/// A Noise-XX session over a stream.
322///
323/// Payloads are exchanged with [`NoiseSession::send`] and [`NoiseSession::recv`]; the session moves
324/// from the handshake phase to the transport phase via [`NoiseSession::into_transport_mode`], which
325/// may only be called once the pattern's three messages have been exchanged.
326pub struct NoiseSession<S> {
327 stream: S,
328 state: SessionState,
329}
330
331impl<S: AsyncRead + AsyncWrite + Unpin> NoiseSession<S> {
332 /// Creates a Noise session over the given stream.
333 ///
334 /// The caller is responsible for the [`NOISE_MAGIC`] prefix: the initiator must have sent it
335 /// with [`write_noise_magic`], and the responder must have consumed it with
336 /// [`detect_handshake_protocol`].
337 /// Note that a responder should generally use [`PendingSession::accept`] instead, so that it can
338 /// inspect the initiator's cleartext payload before committing to any key derivation.
339 pub fn new(stream: S, role: Role) -> io::Result<Self> {
340 Ok(Self { stream, state: SessionState::Handshake(Box::new(build_state(role)?)) })
341 }
342
343 /// Encrypts the given payload and sends it to the peer.
344 pub async fn send(&mut self, payload: &[u8]) -> io::Result<()> {
345 let max_len = self.max_msg_len();
346 if payload.len() + NOISE_OVERHEAD > max_len {
347 return Err(invalid_data(format!("the handshake payload is too large ({} bytes)", payload.len())));
348 }
349
350 // The message cannot exceed the payload plus what the pattern adds to it, so there is no
351 // reason to reach for the maximum message length here.
352 let mut buffer = vec![0u8; payload.len() + NOISE_OVERHEAD];
353 let len = match self.state {
354 SessionState::Handshake(ref mut state) => state.write_message(payload, &mut buffer),
355 SessionState::Transport(ref mut state) => state.write_message(payload, &mut buffer),
356 }
357 .map_err(invalid_data)?;
358 buffer.truncate(len);
359
360 write_frame(&mut self.stream, &buffer, max_len).await
361 }
362
363 /// Receives a message from the peer and returns its decrypted payload.
364 pub async fn recv(&mut self) -> io::Result<Vec<u8>> {
365 let max_len = self.max_msg_len();
366 let message = read_frame(&mut self.stream, max_len).await?;
367
368 self.decrypt(&message)
369 }
370
371 /// The largest message this session will accept or produce, which the handshake holds well below
372 /// what the specification allows; see [`MAX_HANDSHAKE_MSG_LEN`].
373 fn max_msg_len(&self) -> usize {
374 match self.state {
375 SessionState::Handshake(_) => MAX_HANDSHAKE_MSG_LEN,
376 SessionState::Transport(_) => MAX_NOISE_MSG_LEN,
377 }
378 }
379
380 /// Processes an already-received message and returns its decrypted payload.
381 fn decrypt(&mut self, message: &[u8]) -> io::Result<Vec<u8>> {
382 // A payload is never longer than the message that carried it, which was already bounded by
383 // whichever limit applies to the phase this session is in.
384 let mut buffer = vec![0u8; message.len()];
385 let len = match self.state {
386 SessionState::Handshake(ref mut state) => state.read_message(message, &mut buffer),
387 SessionState::Transport(ref mut state) => state.read_message(message, &mut buffer),
388 }
389 .map_err(invalid_data)?;
390 buffer.truncate(len);
391
392 Ok(buffer)
393 }
394
395 /// Returns the current handshake hash, which commits to every key and payload exchanged so
396 /// far; see [`binding_message`].
397 ///
398 /// Both sides derive the same value after processing the same number of messages. It can only
399 /// be read during the handshake phase, so callers that need it must capture it before calling
400 /// [`NoiseSession::into_transport_mode`].
401 pub fn handshake_hash(&self) -> io::Result<[u8; HANDSHAKE_HASH_LEN]> {
402 let SessionState::Handshake(ref state) = self.state else {
403 return Err(invalid_data("the Noise handshake hash is only available during the handshake"));
404 };
405
406 // The hash length is determined by the hash function in `NOISE_PARAMS`.
407 Ok(state.get_handshake_hash().try_into().expect("the Noise handshake hash should be 32 bytes long"))
408 }
409
410 /// Transitions the session from the handshake phase to the transport phase, which the pattern
411 /// only permits once its three messages have been exchanged.
412 pub fn into_transport_mode(self) -> io::Result<Self> {
413 let Self { stream, state } = self;
414
415 let SessionState::Handshake(handshake_state) = state else {
416 return Err(invalid_data("the Noise session is already in transport mode"));
417 };
418 let transport_state = handshake_state.into_transport_mode().map_err(invalid_data)?;
419
420 Ok(Self { stream, state: SessionState::Transport(Box::new(transport_state)) })
421 }
422
423 /// Recovers the underlying stream.
424 ///
425 /// Nothing is lost in the process: the session reads its messages exactly, so anything the peer
426 /// sent past the end of the handshake is still on the stream for whoever takes it next.
427 pub fn into_inner(self) -> S {
428 self.stream
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 use tokio::io::{DuplexStream, duplex};
437
438 /// Runs the three messages of the XX pattern, returning both sessions and the handshake hashes
439 /// each side derived after messages 2 and 3.
440 async fn perform_xx_handshake(
441 payloads: [&[u8]; 3],
442 ) -> (NoiseSession<DuplexStream>, NoiseSession<DuplexStream>, [[u8; 32]; 4]) {
443 let (initiator_stream, responder_stream) = duplex(1024);
444 let mut initiator = NoiseSession::new(initiator_stream, Role::Initiator).unwrap();
445
446 // -> e
447 initiator.send(payloads[0]).await.unwrap();
448 let pending = PendingSession::accept(responder_stream).await.unwrap();
449 assert_eq!(pending.first_payload().unwrap(), payloads[0]);
450 let mut responder = pending.into_session().unwrap();
451
452 // <- e, ee, s, es
453 responder.send(payloads[1]).await.unwrap();
454 assert_eq!(initiator.recv().await.unwrap(), payloads[1]);
455 let (initiator_h2, responder_h2) = (initiator.handshake_hash().unwrap(), responder.handshake_hash().unwrap());
456
457 // -> s, se
458 initiator.send(payloads[2]).await.unwrap();
459 assert_eq!(responder.recv().await.unwrap(), payloads[2]);
460 let (initiator_h3, responder_h3) = (initiator.handshake_hash().unwrap(), responder.handshake_hash().unwrap());
461
462 (initiator, responder, [initiator_h2, responder_h2, initiator_h3, responder_h3])
463 }
464
465 #[tokio::test]
466 async fn xx_handshake_completes_and_agrees_on_the_handshake_hash() {
467 let (initiator, responder, [initiator_h2, responder_h2, initiator_h3, responder_h3]) =
468 perform_xx_handshake([b"hint", b"responder info", b"initiator info"]).await;
469
470 // Both sides derive the same binding value at both points of the handshake.
471 assert_eq!(initiator_h2, responder_h2);
472 assert_eq!(initiator_h3, responder_h3);
473 // The hash keeps evolving, so the two binding values are distinct.
474 assert_ne!(initiator_h2, initiator_h3);
475
476 // The fourth message, carrying the responder's proof, is a transport message.
477 let mut initiator = initiator.into_transport_mode().unwrap();
478 let mut responder = responder.into_transport_mode().unwrap();
479 responder.send(b"responder proof").await.unwrap();
480 assert_eq!(initiator.recv().await.unwrap(), b"responder proof");
481 }
482
483 #[tokio::test]
484 async fn handshake_hashes_differ_between_sessions() {
485 let (_, _, first) = perform_xx_handshake([b"", b"", b""]).await;
486 let (_, _, second) = perform_xx_handshake([b"", b"", b""]).await;
487
488 // The ephemeral keys make every session's binding value unique, which is what prevents a
489 // signature over it from being relayed into another session.
490 assert_ne!(first, second);
491 }
492
493 #[tokio::test]
494 async fn tampering_with_a_handshake_message_is_detected() {
495 let (mut initiator_stream, mut responder_stream) = duplex(1024);
496 let mut initiator = NoiseSession::new(&mut initiator_stream, Role::Initiator).unwrap();
497
498 initiator.send(b"hint").await.unwrap();
499 let mut responder = PendingSession::accept(&mut responder_stream).await.unwrap().into_session().unwrap();
500
501 // Flip a bit in the encrypted payload of the second message.
502 let mut buffer = vec![0u8; MAX_NOISE_MSG_LEN];
503 let SessionState::Handshake(ref mut state) = responder.state else { unreachable!() };
504 let len = state.write_message(b"responder info", &mut buffer).unwrap();
505 buffer.truncate(len);
506 *buffer.last_mut().unwrap() ^= 1;
507 write_frame(&mut responder.stream, &buffer, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
508
509 assert!(initiator.recv().await.is_err());
510 }
511
512 #[tokio::test]
513 async fn a_first_message_payload_is_readable_before_any_keys_are_derived() {
514 let (initiator_stream, responder_stream) = duplex(1024);
515 let mut initiator = NoiseSession::new(initiator_stream, Role::Initiator).unwrap();
516 initiator.send(b"a cleartext hint").await.unwrap();
517
518 // This is what pins `FIRST_MESSAGE_PAYLOAD_OFFSET` to what the pattern actually produces: if
519 // `NOISE_PARAMS` ever changed its Diffie-Hellman function, or the pattern gained a
520 // pre-message, the offset would be wrong and this assertion would catch it.
521 let pending = PendingSession::accept(responder_stream).await.unwrap();
522 assert_eq!(pending.first_payload().unwrap(), b"a cleartext hint");
523
524 // The same message must still drive the handshake once the keys exist.
525 let mut responder = pending.into_session().unwrap();
526 responder.send(b"responder info").await.unwrap();
527 assert_eq!(initiator.recv().await.unwrap(), b"responder info");
528 }
529
530 #[tokio::test]
531 async fn tampering_with_the_cleartext_first_payload_is_detected() {
532 // The attacker sits on both wires, so that it can rewrite the first message in flight.
533 let (initiator_stream, mut initiator_wire) = duplex(1024);
534 let (mut responder_wire, responder_stream) = duplex(1024);
535
536 let mut initiator = NoiseSession::new(initiator_stream, Role::Initiator).unwrap();
537 initiator.send(b"the original hint").await.unwrap();
538
539 // Flip a bit in the payload, which the pattern's first message carries in the clear.
540 let mut message = read_frame(&mut initiator_wire, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
541 message[FIRST_MESSAGE_PAYLOAD_OFFSET] ^= 1;
542 write_frame(&mut responder_wire, &message, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
543
544 // The responder reads the rewritten payload quite happily - it is a claim, not a fact.
545 let pending = PendingSession::accept(responder_stream).await.unwrap();
546 assert_eq!(pending.first_payload().unwrap(), b"uhe original hint");
547 let mut responder = pending.into_session().unwrap();
548
549 // But the payload was mixed into its handshake hash, which is the associated data of every
550 // encryption that follows, so its reply cannot be decrypted by the initiator. This is what
551 // lets the responder act on the cleartext hint and have the result stand.
552 responder.send(b"responder info").await.unwrap();
553 let reply = read_frame(&mut responder_wire, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
554 write_frame(&mut initiator_wire, &reply, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
555
556 assert!(initiator.recv().await.is_err());
557 }
558
559 #[tokio::test]
560 async fn a_mismatched_prologue_fails_the_handshake() {
561 // A peer that folds a different marker into the pattern - which is what a marker tampered
562 // with in flight amounts to - cannot complete the handshake.
563 let params: NoiseParams = NOISE_PARAMS.parse().unwrap();
564 let mut odd_one_out = Builder::new(params)
565 .prologue(b"a different marker")
566 .unwrap()
567 .local_private_key(&[0u8; DH_LEN])
568 .unwrap()
569 .build_initiator()
570 .unwrap();
571
572 let (mut initiator_stream, responder_stream) = duplex(1024);
573
574 let mut buffer = vec![0u8; MAX_NOISE_MSG_LEN];
575 let len = odd_one_out.write_message(b"hint", &mut buffer).unwrap();
576 buffer.truncate(len);
577 write_frame(&mut initiator_stream, &buffer, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
578
579 // The responder accepts the first message, which carries nothing it could verify, and its
580 // reply is rejected in turn - exactly as with a tampered payload.
581 let mut responder = PendingSession::accept(responder_stream).await.unwrap().into_session().unwrap();
582 responder.send(b"responder info").await.unwrap();
583
584 let reply = read_frame(&mut initiator_stream, MAX_HANDSHAKE_MSG_LEN).await.unwrap();
585 assert!(odd_one_out.read_message(&reply, &mut vec![0u8; MAX_NOISE_MSG_LEN]).is_err());
586 }
587
588 #[tokio::test]
589 async fn a_session_leaves_bytes_that_follow_a_message_on_the_stream() {
590 let (mut initiator_stream, responder_stream) = duplex(1024);
591 let mut initiator = NoiseSession::new(&mut initiator_stream, Role::Initiator).unwrap();
592 initiator.send(b"hint").await.unwrap();
593
594 // Whatever the peer pipelines behind a handshake message. Dropping the session first is only
595 // to release the borrow; the bytes are written to the same stream either way.
596 drop(initiator);
597 initiator_stream.write_all(b"pipelined").await.unwrap();
598
599 let pending = PendingSession::accept(responder_stream).await.unwrap();
600 assert_eq!(pending.first_payload().unwrap(), b"hint");
601 let responder = pending.into_session().unwrap();
602
603 // The session read its message and not a byte further, so the trailing bytes are still there
604 // for whoever takes the stream next - which is what lets the handshake hand a bare stream to
605 // a reader that builds a codec of its own.
606 let mut stream = responder.into_inner();
607 let mut trailing = [0u8; 9];
608 stream.read_exact(&mut trailing).await.unwrap();
609 assert_eq!(&trailing, b"pipelined");
610 }
611
612 #[tokio::test]
613 async fn an_oversized_handshake_message_is_rejected_before_it_is_read() {
614 let (mut initiator_stream, mut responder_stream) = duplex(1024);
615
616 // Only the length is written, not a body to match it: the bound has to be applied to the
617 // declared length before anything is allocated or read, so this must fail regardless.
618 let length = (MAX_HANDSHAKE_MSG_LEN + 1) as u32;
619 initiator_stream.write_all(&length.to_le_bytes()).await.unwrap();
620
621 let error = read_frame(&mut responder_stream, MAX_HANDSHAKE_MSG_LEN).await.unwrap_err();
622 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
623 }
624
625 #[tokio::test]
626 async fn a_truncated_first_message_is_rejected() {
627 let (mut initiator_stream, responder_stream) = duplex(1024);
628
629 // Shorter than the ephemeral key the pattern's first message must begin with.
630 write_frame(&mut initiator_stream, &[0u8; DH_LEN - 1], MAX_HANDSHAKE_MSG_LEN).await.unwrap();
631
632 let pending = PendingSession::accept(responder_stream).await.unwrap();
633 assert!(pending.first_payload().is_err());
634 }
635
636 #[tokio::test]
637 async fn an_oversized_message_length_is_rejected_before_it_is_allocated() {
638 let (mut initiator_stream, mut responder_stream) = duplex(1024);
639
640 // A length prefix beyond what the specification permits, and no body behind it.
641 initiator_stream.write_all(&(MAX_NOISE_MSG_LEN as u32 + 1).to_le_bytes()).await.unwrap();
642
643 let error = read_frame(&mut responder_stream, MAX_HANDSHAKE_MSG_LEN).await.unwrap_err();
644 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
645 }
646
647 #[tokio::test]
648 async fn the_binding_message_is_domain_separated() {
649 let hash = [7u8; HANDSHAKE_HASH_LEN];
650
651 // A signature is only valid for one role of one subprotocol.
652 assert_ne!(binding_message(b"bft", Role::Initiator, &hash), binding_message(b"bft", Role::Responder, &hash));
653 assert_ne!(binding_message(b"bft", Role::Initiator, &hash), binding_message(b"router", Role::Initiator, &hash));
654 }
655
656 #[tokio::test]
657 async fn the_noise_magic_is_detected() {
658 let (mut initiator_stream, mut responder_stream) = duplex(1024);
659
660 write_noise_magic(&mut initiator_stream).await.unwrap();
661 let (protocol, leftover) = detect_handshake_protocol(&mut responder_stream).await.unwrap();
662 assert_eq!(protocol, HandshakeProtocol::Noise);
663 assert!(leftover.is_empty());
664 }
665
666 #[tokio::test]
667 async fn a_legacy_prefix_is_detected_and_returned() {
668 let (mut initiator_stream, mut responder_stream) = duplex(1024);
669
670 // The length prefix of a legacy `ChallengeRequest` frame.
671 let legacy_prefix = 87u32.to_le_bytes();
672 initiator_stream.write_all(&legacy_prefix).await.unwrap();
673
674 let (protocol, leftover) = detect_handshake_protocol(&mut responder_stream).await.unwrap();
675 assert_eq!(protocol, HandshakeProtocol::Legacy);
676 assert_eq!(&leftover[..], &legacy_prefix[..]);
677 }
678
679 #[test]
680 fn the_noise_magic_is_an_invalid_legacy_frame_length() {
681 // A legacy peer must reject the magic outright instead of waiting for a frame that will
682 // never arrive; its handshake codecs cap frames at 1 MiB.
683 const MAX_LEGACY_HANDSHAKE_FRAME_LEN: u32 = 1024 * 1024;
684 assert!(u32::from_le_bytes(NOISE_MAGIC) > MAX_LEGACY_HANDSHAKE_FRAME_LEN);
685 }
686}