iroh_relay/protos/
handshake.rs

1//! Implements the handshake protocol that authenticates and authorizes clients connecting to the relays.
2//!
3//! The purpose of the handshake is to
4//! 1. Inform the relay of the client's EndpointId
5//! 2. Check that the connecting client owns the secret key for its EndpointId ("is authentic"/"authentication")
6//! 3. Possibly check that the client has access to this relay, if the relay requires authorization.
7//!
8//! Additional complexity comes from the fact that there's two ways that clients can authenticate with
9//! relays.
10//!
11//! One way is via an explicitly sent challenge:
12//!
13//! 1. Once a websocket connection is opened, a client receives a challenge (the `ServerChallenge` frame)
14//! 2. The client sends back what is essentially a signature of that challenge with their secret key
15//!    that matches the EndpointId they have, as well as the EndpointId (the `ClientAuth` frame)
16//!
17//! The second way is very similar to the [Concealed HTTP Auth RFC], and involves send a header that
18//! contains a signature of some shared keying material extracted from TLS ([RFC 5705]).
19//!
20//! The second way can save a full round trip, because the challenge doesn't have to be sent to the client
21//! first, however, it won't always work, as it relies on the keying material extraction feature of TLS,
22//! which is not available in browsers (but might be in the future?) and might break when there's an
23//! HTTPS proxy that doesn't properly deal with this TLS feature.
24//!
25//! [Concealed HTTP Auth RFC]: https://datatracker.ietf.org/doc/rfc9729/
26//! [RFC 5705]: https://datatracker.ietf.org/doc/html/rfc5705
27#[cfg(feature = "server")]
28use std::sync::Arc;
29
30use bytes::{BufMut, Bytes, BytesMut};
31use data_encoding::BASE32HEX_NOPAD as HEX;
32#[cfg(not(wasm_browser))]
33use http::HeaderValue;
34#[cfg(feature = "server")]
35use iroh_base::Signature;
36use iroh_base::{PublicKey, SecretKey};
37use n0_error::{AnyError, anyerr, e, ensure, stack_error};
38use n0_future::{SinkExt, TryStreamExt};
39#[cfg(feature = "server")]
40use rand::CryptoRng;
41use tracing::trace;
42
43use super::{
44    common::{FrameType, FrameTypeError},
45    streams::BytesStreamSink,
46};
47use crate::ExportKeyingMaterial;
48#[cfg(feature = "server")]
49use crate::server::{Access, ClientRequest, DynAccessControl, OnDisconnectGuard};
50
51/// Domain separation string for the [`ServerChallenge`] signature
52const DOMAIN_SEP_CHALLENGE: &str = "iroh-relay handshake v1 challenge signature";
53
54/// Domain separation label for [`KeyMaterialClientAuth`]'s use of [`ExportKeyingMaterial`]
55#[cfg(not(wasm_browser))]
56const DOMAIN_SEP_TLS_EXPORT_LABEL: &[u8] = b"iroh-relay handshake v1";
57
58/// Authentication message from the client.
59#[derive(derive_more::Debug, serde::Serialize, serde::Deserialize)]
60#[cfg_attr(wasm_browser, allow(unused))]
61pub(crate) struct KeyMaterialClientAuth {
62    /// The client's public key
63    pub(crate) public_key: PublicKey,
64    /// A signature of (a hash of) extracted key material.
65    #[serde(with = "serde_bytes")]
66    #[debug("{}", HEX.encode(signature))]
67    pub(crate) signature: [u8; 64],
68    /// Part of the extracted key material.
69    ///
70    /// Allows making sure we have the same underlying key material.
71    #[debug("{}", HEX.encode(key_material_suffix))]
72    pub(crate) key_material_suffix: [u8; 16],
73}
74
75/// A challenge for the client to sign with their secret key for EndpointId authentication.
76#[derive(derive_more::Debug, serde::Serialize, serde::Deserialize)]
77pub(crate) struct ServerChallenge {
78    /// The challenge to sign.
79    /// Must be randomly generated with an RNG that is safe to use for crypto.
80    #[debug("{}", HEX.encode(challenge))]
81    pub(crate) challenge: [u8; 16],
82}
83
84/// Authentication message from the client.
85///
86/// Used when authentication via [`KeyMaterialClientAuth`] didn't work.
87#[derive(derive_more::Debug, serde::Serialize, serde::Deserialize)]
88pub(crate) struct ClientAuth {
89    /// The client's public key, a.k.a. the `EndpointId`
90    pub(crate) public_key: PublicKey,
91    /// A signature of (a hash of) the [`ServerChallenge`].
92    ///
93    /// This is what provides the authentication.
94    #[serde(with = "serde_bytes")]
95    #[debug("{}", HEX.encode(signature))]
96    pub(crate) signature: [u8; 64],
97}
98
99/// Confirmation of successful connection.
100#[derive(derive_more::Debug, serde::Serialize, serde::Deserialize)]
101pub(crate) struct ServerConfirmsAuth;
102
103/// Denial of connection. The client couldn't be verified as authentic.
104#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
105pub(crate) struct ServerDeniesAuth {
106    reason: String,
107}
108
109/// Trait for getting the frame type tag for a frame.
110///
111/// Used only in the handshake, as the frame we expect next
112/// is fairly stateful.
113/// Not used in the send/recv protocol, as any frame is
114/// allowed to happen at any time there.
115trait Frame {
116    /// The frame type this frame is identified by and prefixed with
117    const TAG: FrameType;
118}
119
120impl<T: Frame> Frame for &T {
121    const TAG: FrameType = T::TAG;
122}
123
124impl Frame for ServerChallenge {
125    const TAG: FrameType = FrameType::ServerChallenge;
126}
127
128impl Frame for ClientAuth {
129    const TAG: FrameType = FrameType::ClientAuth;
130}
131
132impl Frame for ServerConfirmsAuth {
133    const TAG: FrameType = FrameType::ServerConfirmsAuth;
134}
135
136impl Frame for ServerDeniesAuth {
137    const TAG: FrameType = FrameType::ServerDeniesAuth;
138}
139
140#[stack_error(derive, add_meta)]
141#[allow(missing_docs)]
142#[non_exhaustive]
143pub enum Error {
144    /// Error returned from the underlying WebSocket stream during the handshake.
145    ///
146    /// The concrete error type is `tokio_websockets::Error` on native targets and
147    /// `ws_stream_wasm::WsErr` on `wasm_browser` targets. Use [`AnyError::downcast_ref`] to
148    /// recover it. Note that the concrete downcast type is not covered by any semver
149    /// guarantees and may change between releases.
150    #[error("WebSocket error")]
151    Websocket { source: AnyError },
152    #[error("Handshake stream ended prematurely")]
153    UnexpectedEnd {},
154    #[error(transparent)]
155    FrameTypeError {
156        #[error(from)]
157        source: FrameTypeError,
158    },
159    #[error("The relay denied our authentication ({reason})")]
160    ServerDeniedAuth { reason: String },
161    #[error("Unexpected tag, got {frame_type:?}, but expected one of {expected_types:?}")]
162    UnexpectedFrameType {
163        frame_type: FrameType,
164        expected_types: Vec<FrameType>,
165    },
166    #[error("Handshake failed while deserializing {frame_type:?} frame")]
167    DeserializationError {
168        frame_type: FrameType,
169        source: AnyError,
170    },
171    #[cfg(feature = "server")]
172    /// Failed to deserialize client auth header
173    ClientAuthHeaderInvalid { value: HeaderValue },
174}
175
176#[cfg(feature = "server")]
177#[stack_error(derive, add_meta)]
178#[non_exhaustive]
179pub(crate) enum VerificationError {
180    #[error("Couldn't export TLS keying material on our end")]
181    NoKeyingMaterial,
182    #[error(
183        "Client didn't extract the same keying material, the suffix mismatched: expected {expected:X?} but got {actual:X?}"
184    )]
185    MismatchedSuffix {
186        expected: [u8; 16],
187        actual: [u8; 16],
188    },
189    #[error(
190        "Client signature {signature:X?} for message {message:X?} invalid for public key {public_key}"
191    )]
192    SignatureInvalid {
193        source: iroh_base::SignatureError,
194        message: Vec<u8>,
195        signature: [u8; 64],
196        public_key: PublicKey,
197    },
198}
199
200impl ServerChallenge {
201    /// Generates a new challenge.
202    #[cfg(feature = "server")]
203    pub(crate) fn new<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
204        let mut challenge = [0u8; 16];
205        rng.fill_bytes(&mut challenge);
206        Self { challenge }
207    }
208
209    /// The actual message bytes to sign (and verify against) for this challenge.
210    fn message_to_sign(&self) -> [u8; 32] {
211        // We're signing a key instead of the direct challenge.
212        // This gives us domain separation protecting from multiple possible attacks,
213        // but especially this one:
214        // Assume a malicious relay. If the protocol required the client to sign the
215        // challenge directly, this would allow the relay to obtain an arbitrary 16-byte
216        // signature, if it maliciously choses the challenge instead of generating it
217        // randomly.
218        // Deriving a key to sign instead mitigates this attack.
219        blake3::derive_key(DOMAIN_SEP_CHALLENGE, &self.challenge)
220    }
221}
222
223impl ClientAuth {
224    /// Generates a signature for the given challenge from the server.
225    pub(crate) fn new(secret_key: &SecretKey, challenge: &ServerChallenge) -> Self {
226        Self {
227            public_key: secret_key.public(),
228            signature: secret_key.sign(&challenge.message_to_sign()).to_bytes(),
229        }
230    }
231
232    /// Verifies this client's authentication given the challenge this was sent in response to.
233    #[cfg(feature = "server")]
234    pub(crate) fn verify(&self, challenge: &ServerChallenge) -> Result<(), Box<VerificationError>> {
235        let message = challenge.message_to_sign();
236        self.public_key
237            .verify(&message, &Signature::from_bytes(&self.signature))
238            .map_err(|err| {
239                e!(VerificationError::SignatureInvalid {
240                    source: err,
241                    message: message.to_vec(),
242                    signature: self.signature,
243                    public_key: self.public_key
244                })
245            })
246            .map_err(Box::new)
247    }
248}
249
250#[cfg(not(wasm_browser))]
251impl KeyMaterialClientAuth {
252    /// Generates a client's authentication, similar to [`ClientAuth`], but by using TLS keying material
253    /// instead of a received challenge.
254    pub(crate) fn new(secret_key: &SecretKey, io: &impl ExportKeyingMaterial) -> Option<Self> {
255        let public_key = secret_key.public();
256        let key_material = io.export_keying_material(
257            [0u8; 32],
258            DOMAIN_SEP_TLS_EXPORT_LABEL,
259            Some(secret_key.public().as_bytes()),
260        )?;
261        // We split the export and only sign the first 16 bytes, and
262        // pass through the last 16 bytes. See also the note in [Self::verify].
263        let (message, suffix) = key_material.split_at(16);
264        Some(Self {
265            public_key,
266            signature: secret_key.sign(message).to_bytes(),
267            key_material_suffix: suffix.try_into().expect("hardcoded length"),
268        })
269    }
270
271    /// Generate the base64url-nopad-encoded header value.
272    pub(crate) fn into_header_value(self) -> HeaderValue {
273        HeaderValue::from_str(
274            &data_encoding::BASE64URL_NOPAD
275                .encode(&postcard::to_allocvec(&self).expect("encoding never fails")),
276        )
277        .expect("BASE64URL_NOPAD encoding contained invisible ascii characters")
278    }
279
280    /// Verifies this client auth on the server side using the same key material.
281    ///
282    /// This might return false for a couple of reasons:
283    /// 1. The exported keying material might not be the same between both ends of the TLS session
284    ///    (e.g. there's an HTTPS proxy in between that doesn't think/care about the TLS keying material exporter).
285    ///    This situation is detected when the key material suffix mismatches.
286    /// 2. The signature itself doesn't verify.
287    #[cfg(feature = "server")]
288    pub(crate) fn verify(
289        &self,
290        io: &impl ExportKeyingMaterial,
291    ) -> Result<(), Box<VerificationError>> {
292        let key_material = io
293            .export_keying_material(
294                [0u8; 32],
295                DOMAIN_SEP_TLS_EXPORT_LABEL,
296                Some(self.public_key.as_bytes()),
297            )
298            .ok_or_else(|| e!(VerificationError::NoKeyingMaterial))?;
299        // We split the export and only sign the first 16 bytes, and
300        // pass through the last 16 bytes.
301        // Passing on the suffix helps the verifying end figure out what
302        // went wrong: If there's a suffix mismatch, then the exported keying
303        // material on both ends wasn't the same - so perhaps there was a
304        // TLS proxy in between or similar.
305        // If the suffix does match, but the signature doesn't verify, then
306        // there must be something wrong with the client's secret key or signature.
307        let (message, suffix) = key_material.split_at(16);
308        let suffix: [u8; 16] = suffix.try_into().expect("hardcoded length");
309        ensure!(
310            suffix == self.key_material_suffix,
311            VerificationError::MismatchedSuffix {
312                expected: self.key_material_suffix,
313                actual: suffix
314            }
315        );
316        // NOTE: We don't blake3-hash here as we do it in [`ServerChallenge::message_to_sign`],
317        // because we already have a domain separation string and keyed hashing step in
318        // the TLS export keying material above.
319        self.public_key
320            .verify(message, &Signature::from_bytes(&self.signature))
321            .map_err(|err| {
322                e!(VerificationError::SignatureInvalid {
323                    source: err,
324                    message: message.to_vec(),
325                    public_key: self.public_key,
326                    signature: self.signature
327                })
328            })
329            .map_err(Box::new)
330    }
331}
332
333/// Runs the client side of the handshake protocol.
334///
335/// See the module docs for details on the protocol.
336/// This is already after having potentially transferred a [`KeyMaterialClientAuth`],
337/// but before having received a response for whether that worked or not.
338///
339/// This requires access to the client's secret key to sign a challenge.
340pub(crate) async fn clientside(
341    io: &mut (impl BytesStreamSink + ExportKeyingMaterial),
342    secret_key: &SecretKey,
343) -> Result<ServerConfirmsAuth, Error> {
344    let (tag, frame) = read_frame(
345        io,
346        &[
347            ServerChallenge::TAG,
348            ServerConfirmsAuth::TAG,
349            ServerDeniesAuth::TAG,
350        ],
351    )
352    .await?;
353
354    let (tag, frame) = if tag == ServerChallenge::TAG {
355        let challenge: ServerChallenge = deserialize_frame(frame)?;
356
357        let client_info = ClientAuth::new(secret_key, &challenge);
358        write_frame(io, client_info).await?;
359
360        read_frame(io, &[ServerConfirmsAuth::TAG, ServerDeniesAuth::TAG]).await?
361    } else {
362        (tag, frame)
363    };
364
365    match tag {
366        FrameType::ServerConfirmsAuth => {
367            let confirmation: ServerConfirmsAuth = deserialize_frame(frame)?;
368            Ok(confirmation)
369        }
370        FrameType::ServerDeniesAuth => {
371            let denial: ServerDeniesAuth = deserialize_frame(frame)?;
372            Err(e!(Error::ServerDeniedAuth {
373                reason: denial.reason
374            }))
375        }
376        _ => unreachable!(),
377    }
378}
379
380/// Result of a successful authentication handshake.
381///
382/// This struct represents a client that has successfully authenticated itself to the relay
383/// server. The authorization must still be confirmed by calling [`Self::authorize_if`] to
384/// complete the protocol and notify the client of success or failure.
385#[cfg(feature = "server")]
386#[derive(Debug)]
387#[must_use = "the protocol is not finished unless `authorize_if` is called"]
388pub struct SuccessfulAuthentication {
389    /// The authenticated client's public key.
390    pub client_key: PublicKey,
391    /// The authentication mechanism that was used.
392    pub mechanism: Mechanism,
393}
394
395/// The mechanism that was used for authentication.
396#[cfg(feature = "server")]
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398#[non_exhaustive]
399pub enum Mechanism {
400    /// Authentication was performed by verifying a signature of a challenge we sent
401    SignedChallenge,
402    /// Authentication was performed by verifying a signature of shared extracted TLS keying material
403    SignedKeyMaterial,
404}
405
406/// Runs the server side of the handshaking protocol.
407///
408/// See the module documentation for an overview of the handshaking protocol.
409///
410/// This takes `rng` to generate cryptographic randomness for the authentication challenge.
411///
412/// This also takes the `client_auth_header`, if present, to perform authentication without
413/// requiring sending a challenge, saving a round-trip, if possible.
414///
415/// If this fails, the protocol falls back to doing a normal extra round trip with a challenge.
416///
417/// The return value [`SuccessfulAuthentication`] still needs to be resolved by calling
418/// [`SuccessfulAuthentication::authorize_if`] to finish the whole authorization protocol
419/// (otherwise the client won't be notified about auth success or failure).
420#[cfg(feature = "server")]
421pub async fn serverside(
422    io: &mut (impl BytesStreamSink + ExportKeyingMaterial),
423    client_auth_header: Option<HeaderValue>,
424) -> Result<SuccessfulAuthentication, Error> {
425    if let Some(client_auth_header) = client_auth_header {
426        let client_auth_bytes = data_encoding::BASE64URL_NOPAD
427            .decode(client_auth_header.as_ref())
428            .map_err(|_| {
429                e!(Error::ClientAuthHeaderInvalid {
430                    value: client_auth_header.clone()
431                })
432            })?;
433
434        let client_auth: KeyMaterialClientAuth =
435            postcard::from_bytes(&client_auth_bytes).map_err(|_| {
436                e!(Error::ClientAuthHeaderInvalid {
437                    value: client_auth_header.clone()
438                })
439            })?;
440
441        if client_auth.verify(io).is_ok() {
442            trace!(?client_auth.public_key, "authentication succeeded via keying material");
443            return Ok(SuccessfulAuthentication {
444                client_key: client_auth.public_key,
445                mechanism: Mechanism::SignedKeyMaterial,
446            });
447        }
448        // Verification not succeeding is part of normal operation: The TLS exporter isn't required to match.
449        // We'll fall back to verification that takes another round trip more time.
450    }
451
452    let challenge = ServerChallenge::new(&mut rand::rng());
453    write_frame(io, &challenge).await?;
454
455    let (_, frame) = read_frame(io, &[ClientAuth::TAG]).await?;
456    let client_auth: ClientAuth = deserialize_frame(frame)?;
457
458    if let Err(err) = client_auth.verify(&challenge) {
459        trace!(?client_auth.public_key, ?err, "authentication failed");
460        let denial = ServerDeniesAuth {
461            reason: "signature invalid".into(),
462        };
463        write_frame(io, denial.clone()).await?;
464        Err(e!(Error::ServerDeniedAuth {
465            reason: denial.reason
466        }))
467    } else {
468        trace!(?client_auth.public_key, "authentication succeeded via challenge");
469        Ok(SuccessfulAuthentication {
470            client_key: client_auth.public_key,
471            mechanism: Mechanism::SignedChallenge,
472        })
473    }
474}
475
476#[cfg(feature = "server")]
477impl SuccessfulAuthentication {
478    /// Authorizes a [`ClientRequest`] and completes the authorization protocol.
479    ///
480    /// This invokes [`AccessControl::on_connect`] with the [`ClientRequest`] and informs the client about the
481    /// authorization decision.
482    ///
483    /// Returns [`OnDisconnectGuard`] in case the authorization was successful and the confirmation
484    /// message was sent to the client. The handshake protocol is now complete.
485    ///
486    /// Returns an error if the authorization failed or if the confirmation message could not be sent.
487    ///
488    /// [`AccessControl::on_connect`]: crate::server::AccessControl::on_connect
489    pub async fn authorize_with(
490        self,
491        request: &ClientRequest,
492        access_control: &Arc<dyn DynAccessControl>,
493        io: &mut (impl BytesStreamSink + ExportKeyingMaterial),
494    ) -> Result<OnDisconnectGuard, Error> {
495        match access_control.on_connect(request).await {
496            Access::Allow => {
497                let guard = OnDisconnectGuard::for_access_control(access_control.clone(), request);
498                self.accept(io).await?;
499                Ok(guard)
500            }
501            Access::Deny { reason } => Err(self.deny(reason, io).await),
502        }
503    }
504    /// Completes the authorization protocol by notifying the client of success or failure.
505    ///
506    /// After a client has been successfully authenticated via [`serverside`], the server must
507    /// decide whether to authorize the client (allow access) or deny it. This method sends
508    /// the authorization decision to the client and completes the handshake protocol.
509    ///
510    /// # Arguments
511    /// * `access` - Whether to grant access to the authenticated client
512    /// * `io` - The WebSocket stream to send the authorization response on
513    ///
514    /// # Returns
515    /// * `Ok(PublicKey)` - The client's public key if authorization was granted
516    /// * `Err(Error)` - If authorization was denied or communication failed
517    pub async fn authorize_if(
518        self,
519        access: Access,
520        io: &mut (impl BytesStreamSink + ExportKeyingMaterial),
521    ) -> Result<PublicKey, Error> {
522        match access {
523            Access::Allow => self.accept(io).await,
524            Access::Deny { reason } => Err(self.deny(reason, io).await),
525        }
526    }
527
528    async fn accept(
529        self,
530        io: &mut (impl BytesStreamSink + ExportKeyingMaterial),
531    ) -> Result<PublicKey, Error> {
532        trace!("authorizing client");
533        write_frame(io, ServerConfirmsAuth).await?;
534        Ok(self.client_key)
535    }
536
537    async fn deny(
538        self,
539        reason: Option<String>,
540        io: &mut (impl BytesStreamSink + ExportKeyingMaterial),
541    ) -> Error {
542        trace!("denying client auth");
543        let reason = reason.unwrap_or_else(|| "not authorized".into());
544        let denial = ServerDeniesAuth { reason };
545        match write_frame(io, denial.clone()).await {
546            Err(err) => err,
547            Ok(()) => e!(Error::ServerDeniedAuth {
548                reason: denial.reason
549            }),
550        }
551    }
552}
553
554async fn write_frame<F: serde::Serialize + Frame>(
555    io: &mut impl BytesStreamSink,
556    frame: F,
557) -> Result<(), Error> {
558    let mut bytes = BytesMut::new();
559    trace!(frame_type = ?F::TAG, "Writing frame");
560    F::TAG.write_to(&mut bytes);
561    let bytes = postcard::to_io(&frame, bytes.writer())
562        .expect("serialization failed") // buffer can't become "full" without being a critical failure, datastructures shouldn't ever fail serialization
563        .into_inner()
564        .freeze();
565    io.send(bytes)
566        .await
567        .map_err(|err| e!(Error::Websocket, anyerr!(err)))?;
568    io.flush()
569        .await
570        .map_err(|err| e!(Error::Websocket, anyerr!(err)))?;
571    Ok(())
572}
573
574async fn read_frame(
575    io: &mut impl BytesStreamSink,
576    expected_types: &[FrameType],
577) -> Result<(FrameType, Bytes), Error> {
578    let mut payload = io
579        .try_next()
580        .await
581        .map_err(|err| e!(Error::Websocket, anyerr!(err)))?
582        .ok_or_else(|| e!(Error::UnexpectedEnd))?;
583
584    let frame_type = FrameType::from_bytes(&mut payload)?;
585    trace!(?frame_type, "Reading frame");
586    ensure!(
587        expected_types.contains(&frame_type),
588        Error::UnexpectedFrameType {
589            frame_type,
590            expected_types: expected_types.to_vec()
591        }
592    );
593
594    Ok((frame_type, payload))
595}
596
597fn deserialize_frame<F: Frame + serde::de::DeserializeOwned>(frame: Bytes) -> Result<F, Error> {
598    postcard::from_bytes(&frame).map_err(|err| {
599        e!(Error::DeserializationError {
600            frame_type: F::TAG,
601            source: anyerr!(err)
602        })
603    })
604}
605
606#[cfg(all(test, feature = "server"))]
607mod tests {
608    use bytes::BytesMut;
609    use iroh_base::{PublicKey, SecretKey};
610    use n0_error::{AnyError, Result, StackResultExt, StdResultExt};
611    use n0_future::{Sink, SinkExt, Stream, TryStreamExt};
612    use n0_tracing_test::traced_test;
613    use rand::{RngExt, SeedableRng};
614    use tokio_util::codec::{Framed, LengthDelimitedCodec};
615    use tracing::{Instrument, info_span};
616
617    use super::{
618        ClientAuth, KeyMaterialClientAuth, Mechanism, ServerChallenge, ServerConfirmsAuth,
619    };
620    use crate::{ExportKeyingMaterial, server::Access};
621
622    struct TestKeyingMaterial<IO> {
623        shared_secret: Option<u64>,
624        inner: IO,
625    }
626
627    trait WithTlsSharedSecret: Sized {
628        fn with_shared_secret(self, shared_secret: Option<u64>) -> TestKeyingMaterial<Self>;
629    }
630
631    impl<T: Sized> WithTlsSharedSecret for T {
632        fn with_shared_secret(self, shared_secret: Option<u64>) -> TestKeyingMaterial<Self> {
633            TestKeyingMaterial {
634                shared_secret,
635                inner: self,
636            }
637        }
638    }
639
640    impl<IO> ExportKeyingMaterial for TestKeyingMaterial<IO> {
641        fn export_keying_material<T: AsMut<[u8]>>(
642            &self,
643            mut output: T,
644            label: &[u8],
645            context: Option<&[u8]>,
646        ) -> Option<T> {
647            // we simulate something like exporting keying material using blake3
648
649            let label_key = blake3::hash(label);
650            let context_key = blake3::keyed_hash(label_key.as_bytes(), context.unwrap_or(&[]));
651            let mut hasher = blake3::Hasher::new_keyed(context_key.as_bytes());
652            hasher.update(&self.shared_secret?.to_le_bytes());
653            hasher.finalize_xof().fill(output.as_mut());
654
655            Some(output)
656        }
657    }
658
659    impl<V, IO: Stream<Item = V> + Unpin> Stream for TestKeyingMaterial<IO> {
660        type Item = V;
661
662        fn poll_next(
663            mut self: std::pin::Pin<&mut Self>,
664            cx: &mut std::task::Context<'_>,
665        ) -> std::task::Poll<Option<Self::Item>> {
666            std::pin::Pin::new(&mut self.inner).poll_next(cx)
667        }
668    }
669
670    impl<V, E, IO: Sink<V, Error = E> + Unpin> Sink<V> for TestKeyingMaterial<IO> {
671        type Error = E;
672
673        fn poll_ready(
674            mut self: std::pin::Pin<&mut Self>,
675            cx: &mut std::task::Context<'_>,
676        ) -> std::task::Poll<Result<(), Self::Error>> {
677            std::pin::Pin::new(&mut self.inner).poll_ready(cx)
678        }
679
680        fn start_send(mut self: std::pin::Pin<&mut Self>, item: V) -> Result<(), Self::Error> {
681            std::pin::Pin::new(&mut self.inner).start_send(item)
682        }
683
684        fn poll_flush(
685            mut self: std::pin::Pin<&mut Self>,
686            cx: &mut std::task::Context<'_>,
687        ) -> std::task::Poll<Result<(), Self::Error>> {
688            std::pin::Pin::new(&mut self.inner).poll_flush(cx)
689        }
690
691        fn poll_close(
692            mut self: std::pin::Pin<&mut Self>,
693            cx: &mut std::task::Context<'_>,
694        ) -> std::task::Poll<Result<(), Self::Error>> {
695            std::pin::Pin::new(&mut self.inner).poll_close(cx)
696        }
697    }
698
699    async fn simulate_handshake(
700        secret_key: &SecretKey,
701        client_shared_secret: Option<u64>,
702        server_shared_secret: Option<u64>,
703        restricted_to: Option<PublicKey>,
704    ) -> (Result<ServerConfirmsAuth>, Result<(PublicKey, Mechanism)>) {
705        let (client, server) = tokio::io::duplex(1024);
706
707        let mut client_io = Framed::new(client, LengthDelimitedCodec::new())
708            .map_ok(BytesMut::freeze)
709            .map_err(AnyError::from_std)
710            .sink_map_err(AnyError::from_std)
711            .with_shared_secret(client_shared_secret);
712        let mut server_io = Framed::new(server, LengthDelimitedCodec::new())
713            .map_ok(BytesMut::freeze)
714            .map_err(AnyError::from_std)
715            .sink_map_err(AnyError::from_std)
716            .with_shared_secret(server_shared_secret);
717
718        let client_auth_header = KeyMaterialClientAuth::new(secret_key, &client_io)
719            .map(KeyMaterialClientAuth::into_header_value);
720
721        n0_future::future::zip(
722            async {
723                super::clientside(&mut client_io, secret_key)
724                    .await
725                    .context("clientside")
726            }
727            .instrument(info_span!("clientside")),
728            async {
729                let auth_n = super::serverside(&mut server_io, client_auth_header)
730                    .await
731                    .context("serverside")?;
732                let mechanism = auth_n.mechanism;
733                let is_authorized = match restricted_to.is_none_or(|key| key == auth_n.client_key) {
734                    true => Access::Allow,
735                    false => Access::Deny { reason: None },
736                };
737                let key = auth_n.authorize_if(is_authorized, &mut server_io).await?;
738                Ok((key, mechanism))
739            }
740            .instrument(info_span!("serverside")),
741        )
742        .await
743    }
744
745    #[tokio::test]
746    #[traced_test]
747    async fn test_handshake_via_shared_secrets() -> Result {
748        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
749
750        let secret_key = SecretKey::from_bytes(&rng.random());
751        let (client, server) = simulate_handshake(&secret_key, Some(42), Some(42), None).await;
752        client?;
753        let (public_key, auth) = server?;
754        assert_eq!(public_key, secret_key.public());
755        assert_eq!(auth, Mechanism::SignedKeyMaterial); // it got verified via shared key material
756        Ok(())
757    }
758
759    #[tokio::test]
760    #[traced_test]
761    async fn test_handshake_via_challenge() -> Result {
762        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
763
764        let secret_key = SecretKey::from_bytes(&rng.random());
765        let (client, server) = simulate_handshake(&secret_key, None, None, None).await;
766        client?;
767        let (public_key, auth) = server?;
768        assert_eq!(public_key, secret_key.public());
769        assert_eq!(auth, Mechanism::SignedChallenge);
770        Ok(())
771    }
772
773    #[tokio::test]
774    #[traced_test]
775    async fn test_handshake_mismatching_shared_secrets() -> Result {
776        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
777
778        let secret_key = SecretKey::from_bytes(&rng.random());
779        // mismatching shared secrets *might* happen with HTTPS proxies that don't also middle-man the shared secret
780        let (client, server) = simulate_handshake(&secret_key, Some(10), Some(99), None).await;
781        client?;
782        let (public_key, auth) = server?;
783        assert_eq!(public_key, secret_key.public());
784        assert_eq!(auth, Mechanism::SignedChallenge);
785        Ok(())
786    }
787
788    #[tokio::test]
789    #[traced_test]
790    async fn test_handshake_challenge_fallback() -> Result {
791        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
792        let secret_key = SecretKey::from_bytes(&rng.random());
793        // clients might not have access to shared secrets
794        let (client, server) = simulate_handshake(&secret_key, None, Some(99), None).await;
795        client?;
796        let (public_key, auth) = server?;
797        assert_eq!(public_key, secret_key.public());
798        assert_eq!(auth, Mechanism::SignedChallenge);
799        Ok(())
800    }
801
802    #[tokio::test]
803    #[traced_test]
804    async fn test_handshake_with_auth_positive() -> Result {
805        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
806        let secret_key = SecretKey::from_bytes(&rng.random());
807        let public_key = secret_key.public();
808        let (client, server) = simulate_handshake(&secret_key, None, None, Some(public_key)).await;
809        client?;
810        let (public_key, _) = server?;
811        assert_eq!(public_key, secret_key.public());
812        Ok(())
813    }
814
815    #[tokio::test]
816    #[traced_test]
817    async fn test_handshake_with_auth_negative() -> Result {
818        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
819        let secret_key = SecretKey::from_bytes(&rng.random());
820        let public_key = secret_key.public();
821        let wrong_secret_key = SecretKey::from_bytes(&rng.random());
822        let (client, server) =
823            simulate_handshake(&wrong_secret_key, None, None, Some(public_key)).await;
824        assert!(client.is_err());
825        assert!(server.is_err());
826        Ok(())
827    }
828
829    #[tokio::test]
830    #[traced_test]
831    async fn test_handshake_via_shared_secret_with_auth_negative() -> Result {
832        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
833        let secret_key = SecretKey::from_bytes(&rng.random());
834        let public_key = secret_key.public();
835        let wrong_secret_key = SecretKey::from_bytes(&rng.random());
836        let (client, server) =
837            simulate_handshake(&wrong_secret_key, Some(42), Some(42), Some(public_key)).await;
838        assert!(client.is_err());
839        assert!(server.is_err());
840        Ok(())
841    }
842
843    #[test]
844    fn test_client_auth_roundtrip() -> Result {
845        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
846        let secret_key = SecretKey::from_bytes(&rng.random());
847        let challenge = ServerChallenge::new(&mut rng);
848        let client_auth = ClientAuth::new(&secret_key, &challenge);
849
850        let bytes = postcard::to_allocvec(&client_auth).anyerr()?;
851        let decoded: ClientAuth = postcard::from_bytes(&bytes).anyerr()?;
852
853        assert_eq!(client_auth.public_key, decoded.public_key);
854        assert_eq!(client_auth.signature, decoded.signature);
855
856        Ok(())
857    }
858
859    #[test]
860    fn test_km_client_auth_roundtrip() -> Result {
861        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
862        let secret_key = SecretKey::from_bytes(&rng.random());
863        let client_auth = KeyMaterialClientAuth::new(
864            &secret_key,
865            &TestKeyingMaterial {
866                inner: (),
867                shared_secret: Some(42),
868            },
869        )
870        .anyerr()?;
871
872        let bytes = postcard::to_allocvec(&client_auth).anyerr()?;
873        let decoded: KeyMaterialClientAuth = postcard::from_bytes(&bytes).anyerr()?;
874
875        assert_eq!(client_auth.public_key, decoded.public_key);
876        assert_eq!(client_auth.signature, decoded.signature);
877
878        Ok(())
879    }
880
881    #[test]
882    fn test_challenge_verification() -> Result {
883        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
884        let secret_key = SecretKey::from_bytes(&rng.random());
885        let challenge = ServerChallenge::new(&mut rng);
886        let client_auth = ClientAuth::new(&secret_key, &challenge);
887        assert!(client_auth.verify(&challenge).is_ok());
888
889        Ok(())
890    }
891
892    #[test]
893    fn test_key_material_verification() -> Result {
894        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
895        let secret_key = SecretKey::from_bytes(&rng.random());
896        let io = TestKeyingMaterial {
897            inner: (),
898            shared_secret: Some(42),
899        };
900        let client_auth = KeyMaterialClientAuth::new(&secret_key, &io).anyerr()?;
901        assert!(client_auth.verify(&io).is_ok());
902
903        Ok(())
904    }
905}