libp2p_noise/io/
handshake.rs

1// Copyright 2019 Parity Technologies (UK) Ltd.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation
6// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7// and/or sell copies of the Software, and to permit persons to whom the
8// Software is furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19// DEALINGS IN THE SOFTWARE.
20
21//! Noise protocol handshake I/O.
22
23pub(super) mod proto {
24    #![allow(unreachable_pub)]
25    include!("../generated/mod.rs");
26    pub use self::payload::proto::NoiseExtensions;
27    pub use self::payload::proto::NoiseHandshakePayload;
28}
29
30use super::framed::Codec;
31use crate::io::Output;
32use crate::protocol::{KeypairIdentity, PublicKey, STATIC_KEY_DOMAIN};
33use crate::Error;
34use asynchronous_codec::Framed;
35use futures::prelude::*;
36use libp2p_identity as identity;
37use multihash::Multihash;
38use quick_protobuf::MessageWrite;
39use std::collections::HashSet;
40use std::{io, mem};
41
42//////////////////////////////////////////////////////////////////////////////
43// Internal
44
45/// Handshake state.
46pub(crate) struct State<T> {
47    /// The underlying I/O resource.
48    io: Framed<T, Codec<snow::HandshakeState>>,
49    /// The associated public identity of the local node's static DH keypair,
50    /// which can be sent to the remote as part of an authenticated handshake.
51    identity: KeypairIdentity,
52    /// The received signature over the remote's static DH public key, if any.
53    dh_remote_pubkey_sig: Option<Vec<u8>>,
54    /// The known or received public identity key of the remote, if any.
55    id_remote_pubkey: Option<identity::PublicKey>,
56    /// The WebTransport certhashes of the responder, if any.
57    responder_webtransport_certhashes: Option<HashSet<Multihash<64>>>,
58    /// The received extensions of the remote, if any.
59    remote_extensions: Option<Extensions>,
60}
61
62/// Extensions
63struct Extensions {
64    webtransport_certhashes: HashSet<Multihash<64>>,
65}
66
67impl<T> State<T>
68where
69    T: AsyncRead + AsyncWrite,
70{
71    /// Initializes the state for a new Noise handshake, using the given local
72    /// identity keypair and local DH static public key. The handshake messages
73    /// will be sent and received on the given I/O resource and using the
74    /// provided session for cryptographic operations according to the chosen
75    /// Noise handshake pattern.
76
77    pub(crate) fn new(
78        io: T,
79        session: snow::HandshakeState,
80        identity: KeypairIdentity,
81        expected_remote_key: Option<identity::PublicKey>,
82        responder_webtransport_certhashes: Option<HashSet<Multihash<64>>>,
83    ) -> Self {
84        Self {
85            identity,
86            io: Framed::new(io, Codec::new(session)),
87            dh_remote_pubkey_sig: None,
88            id_remote_pubkey: expected_remote_key,
89            responder_webtransport_certhashes,
90            remote_extensions: None,
91        }
92    }
93}
94
95impl<T> State<T>
96where
97    T: AsyncRead + AsyncWrite,
98{
99    /// Finish a handshake, yielding the established remote identity and the
100    /// [`Output`] for communicating on the encrypted channel.
101    pub(crate) fn finish(self) -> Result<(identity::PublicKey, Output<T>), Error> {
102        let is_initiator = self.io.codec().is_initiator();
103
104        let (pubkey, framed) = map_into_transport(self.io)?;
105
106        let id_pk = self
107            .id_remote_pubkey
108            .ok_or_else(|| Error::AuthenticationFailed)?;
109
110        let is_valid_signature = self.dh_remote_pubkey_sig.as_ref().map_or(false, |s| {
111            id_pk.verify(&[STATIC_KEY_DOMAIN.as_bytes(), pubkey.as_ref()].concat(), s)
112        });
113
114        if !is_valid_signature {
115            return Err(Error::BadSignature);
116        }
117
118        // Check WebTransport certhashes that responder reported back to us.
119        if is_initiator {
120            // We check only if we care (i.e. Config::with_webtransport_certhashes was used).
121            if let Some(expected_certhashes) = self.responder_webtransport_certhashes {
122                let ext = self.remote_extensions.ok_or_else(|| {
123                    Error::UnknownWebTransportCerthashes(
124                        expected_certhashes.to_owned(),
125                        HashSet::new(),
126                    )
127                })?;
128
129                let received_certhashes = ext.webtransport_certhashes;
130
131                // Expected WebTransport certhashes must be a strict subset
132                // of the reported ones.
133                if !expected_certhashes.is_subset(&received_certhashes) {
134                    return Err(Error::UnknownWebTransportCerthashes(
135                        expected_certhashes,
136                        received_certhashes,
137                    ));
138                }
139            }
140        }
141
142        Ok((id_pk, Output::new(framed)))
143    }
144}
145
146/// Maps the provided [`Framed`] from the [`snow::HandshakeState`] into the [`snow::TransportState`].
147///
148/// This is a bit tricky because [`Framed`] cannot just be de-composed but only into its [`FramedParts`](asynchronous_codec::FramedParts).
149/// However, we need to retain the original [`FramedParts`](asynchronous_codec::FramedParts) because they contain the active read & write buffers.
150///
151/// Those are likely **not** empty because the remote may directly write to the stream again after the noise handshake finishes.
152fn map_into_transport<T>(
153    framed: Framed<T, Codec<snow::HandshakeState>>,
154) -> Result<(PublicKey, Framed<T, Codec<snow::TransportState>>), Error>
155where
156    T: AsyncRead + AsyncWrite,
157{
158    let mut parts = framed.into_parts().map_codec(Some);
159
160    let (pubkey, codec) = mem::take(&mut parts.codec)
161        .expect("We just set it to `Some`")
162        .into_transport()?;
163
164    let parts = parts.map_codec(|_| codec);
165    let framed = Framed::from_parts(parts);
166
167    Ok((pubkey, framed))
168}
169
170impl From<proto::NoiseExtensions> for Extensions {
171    fn from(value: proto::NoiseExtensions) -> Self {
172        Extensions {
173            webtransport_certhashes: value
174                .webtransport_certhashes
175                .into_iter()
176                .filter_map(|bytes| Multihash::read(&bytes[..]).ok())
177                .collect(),
178        }
179    }
180}
181
182//////////////////////////////////////////////////////////////////////////////
183// Handshake Message Futures
184
185/// A future for receiving a Noise handshake message.
186async fn recv<T>(state: &mut State<T>) -> Result<proto::NoiseHandshakePayload, Error>
187where
188    T: AsyncRead + Unpin,
189{
190    match state.io.next().await {
191        None => Err(io::Error::new(io::ErrorKind::UnexpectedEof, "eof").into()),
192        Some(Err(e)) => Err(e.into()),
193        Some(Ok(p)) => Ok(p),
194    }
195}
196
197/// A future for receiving a Noise handshake message with an empty payload.
198pub(crate) async fn recv_empty<T>(state: &mut State<T>) -> Result<(), Error>
199where
200    T: AsyncRead + Unpin,
201{
202    let payload = recv(state).await?;
203    if payload.get_size() != 0 {
204        return Err(io::Error::new(io::ErrorKind::InvalidData, "Expected empty payload.").into());
205    }
206
207    Ok(())
208}
209
210/// A future for sending a Noise handshake message with an empty payload.
211pub(crate) async fn send_empty<T>(state: &mut State<T>) -> Result<(), Error>
212where
213    T: AsyncWrite + Unpin,
214{
215    state
216        .io
217        .send(&proto::NoiseHandshakePayload::default())
218        .await?;
219    Ok(())
220}
221
222/// A future for receiving a Noise handshake message with a payload identifying the remote.
223pub(crate) async fn recv_identity<T>(state: &mut State<T>) -> Result<(), Error>
224where
225    T: AsyncRead + Unpin,
226{
227    let pb = recv(state).await?;
228    state.id_remote_pubkey = Some(identity::PublicKey::try_decode_protobuf(&pb.identity_key)?);
229
230    if !pb.identity_sig.is_empty() {
231        state.dh_remote_pubkey_sig = Some(pb.identity_sig);
232    }
233
234    if let Some(extensions) = pb.extensions {
235        state.remote_extensions = Some(extensions.into());
236    }
237
238    Ok(())
239}
240
241/// Send a Noise handshake message with a payload identifying the local node to the remote.
242pub(crate) async fn send_identity<T>(state: &mut State<T>) -> Result<(), Error>
243where
244    T: AsyncRead + AsyncWrite + Unpin,
245{
246    let mut pb = proto::NoiseHandshakePayload {
247        identity_key: state.identity.public.encode_protobuf(),
248        ..Default::default()
249    };
250
251    pb.identity_sig = state.identity.signature.clone();
252
253    // If this is the responder then send WebTransport certhashes to initiator, if any.
254    if state.io.codec().is_responder() {
255        if let Some(ref certhashes) = state.responder_webtransport_certhashes {
256            let ext = pb
257                .extensions
258                .get_or_insert_with(proto::NoiseExtensions::default);
259
260            ext.webtransport_certhashes = certhashes.iter().map(|hash| hash.to_bytes()).collect();
261        }
262    }
263
264    state.io.send(&pb).await?;
265
266    Ok(())
267}