diff --git a/CHANGELOG.md b/CHANGELOG.md index 678b4c33..b585911a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,22 @@ # Version ??? +- `libp2p-noise`: Added the `X25519Spec` protocol suite which uses + libp2p-noise-spec compliant signatures on static keys as well as the + `/noise` protocol upgrade, hence providing a libp2p-noise-spec compliant + `XX` handshake. `IK` and `IX` are still supported with `X25519Spec` + though not guaranteed to be interoperable with other libp2p + implementations as these handshake patterns are not currently + included in the libp2p-noise-spec. The `X25519Spec` implementation + will eventually replace the current `X25519` implementation, with + the former being removed. To upgrade without interruptions, you may + temporarily include `NoiseConfig`s for both implementations as + alternatives in your transport upgrade pipeline. + - `libp2p-kad`: Consider fixed (K_VALUE) amount of peers at closest query initialization. Unless `KademliaConfig::set_replication_factor` is used change has no effect. [PR 1536](https://github.com/libp2p/rust-libp2p/pull/1536) + - `libp2p-tcp`: On listeners started with an IPv6 multi-address the socket option `IPV6_V6ONLY` is set to true. Instead of relying on IPv4-mapped IPv6 address support, two listeners can be started if IPv4 and IPv6 should both diff --git a/protocols/noise/src/io/handshake.rs b/protocols/noise/src/io/handshake.rs index a8982354..047951ed 100644 --- a/protocols/noise/src/io/handshake.rs +++ b/protocols/noise/src/io/handshake.rs @@ -365,10 +365,10 @@ where let mut payload_buf = vec![0; len]; state.io.read_exact(&mut payload_buf).await?; - let pb = payload_proto::Identity::decode(&payload_buf[..])?; + let pb = payload_proto::NoiseHandshakePayload::decode(&payload_buf[..])?; - if !pb.pubkey.is_empty() { - let pk = identity::PublicKey::from_protobuf_encoding(&pb.pubkey) + if !pb.identity_key.is_empty() { + let pk = identity::PublicKey::from_protobuf_encoding(&pb.identity_key) .map_err(|_| NoiseError::InvalidKey)?; if let Some(ref k) = state.id_remote_pubkey { if k != &pk { @@ -377,8 +377,8 @@ where } state.id_remote_pubkey = Some(pk); } - if !pb.signature.is_empty() { - state.dh_remote_pubkey_sig = Some(pb.signature); + if !pb.identity_sig.is_empty() { + state.dh_remote_pubkey_sig = Some(pb.identity_sig); } Ok(()) @@ -389,12 +389,12 @@ async fn send_identity(state: &mut State) -> Result<(), NoiseError> where T: AsyncWrite + Unpin, { - let mut pb = payload_proto::Identity::default(); + let mut pb = payload_proto::NoiseHandshakePayload::default(); if state.send_identity { - pb.pubkey = state.identity.public.clone().into_protobuf_encoding() + pb.identity_key = state.identity.public.clone().into_protobuf_encoding() } if let Some(ref sig) = state.identity.signature { - pb.signature = sig.clone() + pb.identity_sig = sig.clone() } let mut buf = Vec::with_capacity(pb.encoded_len()); pb.encode(&mut buf).expect("Vec provides capacity as needed"); diff --git a/protocols/noise/src/io/handshake/payload.proto b/protocols/noise/src/io/handshake/payload.proto index 51b79645..1893dc55 100644 --- a/protocols/noise/src/io/handshake/payload.proto +++ b/protocols/noise/src/io/handshake/payload.proto @@ -4,8 +4,8 @@ package payload.proto; // Payloads for Noise handshake messages. -message Identity { - bytes pubkey = 1; - bytes signature = 2; +message NoiseHandshakePayload { + bytes identity_key = 1; + bytes identity_sig = 2; + bytes data = 3; } - diff --git a/protocols/noise/src/lib.rs b/protocols/noise/src/lib.rs index da9794fc..f73a9e9a 100644 --- a/protocols/noise/src/lib.rs +++ b/protocols/noise/src/lib.rs @@ -27,6 +27,9 @@ //! implementations for various noise handshake patterns (currently `IK`, `IX`, and `XX`) //! over a particular choice of Diffie–Hellman key agreement (currently only X25519). //! +//! > **Note**: Only the `XX` handshake pattern is currently guaranteed to provide +//! > interoperability with other libp2p implementations. +//! //! All upgrades produce as output a pair, consisting of the remote's static public key //! and a `NoiseOutput` which represents the established cryptographic session with the //! remote, implementing `futures::io::AsyncRead` and `futures::io::AsyncWrite`. @@ -38,11 +41,11 @@ //! ``` //! use libp2p_core::{identity, Transport, upgrade}; //! use libp2p_tcp::TcpConfig; -//! use libp2p_noise::{Keypair, X25519, NoiseConfig}; +//! use libp2p_noise::{Keypair, X25519Spec, NoiseConfig}; //! //! # fn main() { //! let id_keys = identity::Keypair::generate_ed25519(); -//! let dh_keys = Keypair::::new().into_authentic(&id_keys).unwrap(); +//! let dh_keys = Keypair::::new().into_authentic(&id_keys).unwrap(); //! let noise = NoiseConfig::xx(dh_keys).into_authenticated(); //! let builder = TcpConfig::new().upgrade(upgrade::Version::V1).authenticate(noise); //! // let transport = builder.multiplex(...); @@ -60,7 +63,8 @@ pub use io::NoiseOutput; pub use io::handshake; pub use io::handshake::{Handshake, RemoteIdentity, IdentityExchange}; pub use protocol::{Keypair, AuthenticKeypair, KeypairIdentity, PublicKey, SecretKey}; -pub use protocol::{Protocol, ProtocolParams, x25519::X25519, IX, IK, XX}; +pub use protocol::{Protocol, ProtocolParams, IX, IK, XX}; +pub use protocol::{x25519::X25519, x25519_spec::X25519Spec}; use futures::prelude::*; use libp2p_core::{identity, PeerId, UpgradeInfo, InboundUpgrade, OutboundUpgrade}; diff --git a/protocols/noise/src/protocol.rs b/protocols/noise/src/protocol.rs index 1fd9fa03..5844dbc8 100644 --- a/protocols/noise/src/protocol.rs +++ b/protocols/noise/src/protocol.rs @@ -21,6 +21,7 @@ //! Components of a Noise protocol. pub mod x25519; +pub mod x25519_spec; use crate::NoiseError; use libp2p_core::identity; @@ -71,6 +72,7 @@ pub trait Protocol { /// /// The trivial case is when the keys are byte for byte identical. #[allow(unused_variables)] + #[deprecated] fn linked(id_pk: &identity::PublicKey, dh_pk: &PublicKey) -> bool { false } @@ -87,6 +89,7 @@ pub trait Protocol { /// without a signature, otherwise a signature over the static DH public key /// must be given and is verified with the public identity key, establishing /// the authenticity of the static DH public key w.r.t. the public identity key. + #[allow(deprecated)] fn verify(id_pk: &identity::PublicKey, dh_pk: &PublicKey, sig: &Option>) -> bool where C: AsRef<[u8]> @@ -95,6 +98,13 @@ pub trait Protocol { || sig.as_ref().map_or(false, |s| id_pk.verify(dh_pk.as_ref(), s)) } + + fn sign(id_keys: &identity::Keypair, dh_pk: &PublicKey) -> Result, NoiseError> + where + C: AsRef<[u8]> + { + Ok(id_keys.sign(dh_pk.as_ref())?) + } } /// DH keypair. @@ -151,9 +161,10 @@ impl Keypair { /// is authentic w.r.t. the given identity keypair, by signing the DH public key. pub fn into_authentic(self, id_keys: &identity::Keypair) -> Result, NoiseError> where - T: AsRef<[u8]> + T: AsRef<[u8]>, + T: Protocol { - let sig = id_keys.sign(self.public.as_ref())?; + let sig = T::sign(id_keys, &self.public)?; let identity = KeypairIdentity { public: id_keys.public(), diff --git a/protocols/noise/src/protocol/x25519.rs b/protocols/noise/src/protocol/x25519.rs index 8a19971d..80bba174 100644 --- a/protocols/noise/src/protocol/x25519.rs +++ b/protocols/noise/src/protocol/x25519.rs @@ -18,7 +18,10 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -//! Noise protocols based on X25519. +//! Legacy Noise protocols based on X25519. +//! +//! **Note**: This set of protocols is not interoperable with other +//! libp2p implementations. use crate::{NoiseConfig, NoiseError, Protocol, ProtocolParams}; use curve25519_dalek::edwards::CompressedEdwardsY; @@ -92,7 +95,11 @@ impl UpgradeInfo for NoiseConfig { } } -/// Noise protocols for X25519. +/// Legacy Noise protocol for X25519. +/// +/// **Note**: This `Protocol` provides no configuration that +/// is interoperable with other libp2p implementations. +/// See [`crate::X25519Spec`] instead. impl Protocol for X25519 { fn params_ik() -> ProtocolParams { PARAMS_IK.clone() diff --git a/protocols/noise/src/protocol/x25519_spec.rs b/protocols/noise/src/protocol/x25519_spec.rs new file mode 100644 index 00000000..446ff7cc --- /dev/null +++ b/protocols/noise/src/protocol/x25519_spec.rs @@ -0,0 +1,150 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! [libp2p-noise-spec] compliant Noise protocols based on X25519. +//! +//! [libp2p-noise-spec]: https://github.com/libp2p/specs/tree/master/noise + +use crate::{NoiseConfig, NoiseError, Protocol, ProtocolParams}; +use libp2p_core::UpgradeInfo; +use libp2p_core::identity; +use rand::Rng; +use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519}; +use zeroize::Zeroize; + +use super::{*, x25519::X25519}; + +/// Prefix of static key signatures for domain separation. +const STATIC_KEY_DOMAIN: &str = "noise-libp2p-static-key:"; + +/// A X25519 key. +#[derive(Clone)] +pub struct X25519Spec([u8; 32]); + +impl AsRef<[u8]> for X25519Spec { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Zeroize for X25519Spec { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +impl Keypair { + /// Create a new X25519 keypair. + pub fn new() -> Keypair { + let mut sk_bytes = [0u8; 32]; + rand::thread_rng().fill(&mut sk_bytes); + let sk = SecretKey(X25519Spec(sk_bytes)); // Copy + sk_bytes.zeroize(); + Self::from(sk) + } +} + +/// Promote a X25519 secret key into a keypair. +impl From> for Keypair { + fn from(secret: SecretKey) -> Keypair { + let public = PublicKey(X25519Spec(x25519((secret.0).0, X25519_BASEPOINT_BYTES))); + Keypair { secret, public } + } +} + +impl UpgradeInfo for NoiseConfig { + type Info = &'static [u8]; + type InfoIter = std::iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + std::iter::once(b"/noise") + } +} + +/// Noise protocols for X25519 with libp2p-spec compliant signatures. +/// +/// **Note**: Only the XX handshake pattern is currently guaranteed to be +/// interoperable with other libp2p implementations. +impl Protocol for X25519Spec { + fn params_ik() -> ProtocolParams { + X25519::params_ik() + } + + fn params_ix() -> ProtocolParams { + X25519::params_ix() + } + + fn params_xx() -> ProtocolParams { + X25519::params_xx() + } + + fn public_from_bytes(bytes: &[u8]) -> Result, NoiseError> { + if bytes.len() != 32 { + return Err(NoiseError::InvalidKey) + } + let mut pk = [0u8; 32]; + pk.copy_from_slice(bytes); + Ok(PublicKey(X25519Spec(pk))) + } + + fn verify(id_pk: &identity::PublicKey, dh_pk: &PublicKey, sig: &Option>) -> bool + { + sig.as_ref().map_or(false, |s| { + id_pk.verify(&[STATIC_KEY_DOMAIN.as_bytes(), dh_pk.as_ref()].concat(), s) + }) + } + + fn sign(id_keys: &identity::Keypair, dh_pk: &PublicKey) -> Result, NoiseError> { + Ok(id_keys.sign(&[STATIC_KEY_DOMAIN.as_bytes(), dh_pk.as_ref()].concat())?) + } +} + +#[doc(hidden)] +impl snow::types::Dh for Keypair { + fn name(&self) -> &'static str { "25519" } + fn pub_len(&self) -> usize { 32 } + fn priv_len(&self) -> usize { 32 } + fn pubkey(&self) -> &[u8] { self.public.as_ref() } + fn privkey(&self) -> &[u8] { self.secret.as_ref() } + + fn set(&mut self, sk: &[u8]) { + let mut secret = [0u8; 32]; + secret.copy_from_slice(&sk[..]); + self.secret = SecretKey(X25519Spec(secret)); // Copy + self.public = PublicKey(X25519Spec(x25519(secret, X25519_BASEPOINT_BYTES))); + secret.zeroize(); + } + + fn generate(&mut self, rng: &mut dyn snow::types::Random) { + let mut secret = [0u8; 32]; + rng.fill_bytes(&mut secret); + self.secret = SecretKey(X25519Spec(secret)); // Copy + self.public = PublicKey(X25519Spec(x25519(secret, X25519_BASEPOINT_BYTES))); + secret.zeroize(); + } + + fn dh(&self, pk: &[u8], shared_secret: &mut [u8]) -> Result<(), ()> { + let mut p = [0; 32]; + p.copy_from_slice(&pk[.. 32]); + let ss = x25519((self.secret.0).0, p); + shared_secret[.. 32].copy_from_slice(&ss[..]); + Ok(()) + } +} diff --git a/protocols/noise/tests/smoke.rs b/protocols/noise/tests/smoke.rs index 1ac04491..744d447a 100644 --- a/protocols/noise/tests/smoke.rs +++ b/protocols/noise/tests/smoke.rs @@ -22,7 +22,7 @@ use futures::{future::{self, Either}, prelude::*}; use libp2p_core::identity; use libp2p_core::upgrade::{self, Negotiated, apply_inbound, apply_outbound}; use libp2p_core::transport::{Transport, ListenerEvent}; -use libp2p_noise::{Keypair, X25519, NoiseConfig, RemoteIdentity, NoiseError, NoiseOutput}; +use libp2p_noise::{Keypair, X25519, X25519Spec, NoiseConfig, RemoteIdentity, NoiseError, NoiseOutput}; use libp2p_tcp::{TcpConfig, TcpTransStream}; use log::info; use quickcheck::QuickCheck; @@ -38,6 +38,37 @@ fn core_upgrade_compat() { let _ = TcpConfig::new().upgrade(upgrade::Version::V1).authenticate(noise); } +#[test] +fn xx_spec() { + let _ = env_logger::try_init(); + fn prop(mut messages: Vec) -> bool { + messages.truncate(5); + let server_id = identity::Keypair::generate_ed25519(); + let client_id = identity::Keypair::generate_ed25519(); + + let server_id_public = server_id.public(); + let client_id_public = client_id.public(); + + let server_dh = Keypair::::new().into_authentic(&server_id).unwrap(); + let server_transport = TcpConfig::new() + .and_then(move |output, endpoint| { + upgrade::apply(output, NoiseConfig::xx(server_dh), endpoint, upgrade::Version::V1) + }) + .and_then(move |out, _| expect_identity(out, &client_id_public)); + + let client_dh = Keypair::::new().into_authentic(&client_id).unwrap(); + let client_transport = TcpConfig::new() + .and_then(move |output, endpoint| { + upgrade::apply(output, NoiseConfig::xx(client_dh), endpoint, upgrade::Version::V1) + }) + .and_then(move |out, _| expect_identity(out, &server_id_public)); + + run(server_transport, client_transport, messages); + true + } + QuickCheck::new().max_tests(30).quickcheck(prop as fn(Vec) -> bool) +} + #[test] fn xx() { let _ = env_logger::try_init(); @@ -144,15 +175,15 @@ fn ik_xx() { QuickCheck::new().max_tests(30).quickcheck(prop as fn(Vec) -> bool) } -type Output = (RemoteIdentity, NoiseOutput>); +type Output = (RemoteIdentity, NoiseOutput>); -fn run(server_transport: T, client_transport: U, messages: I) +fn run(server_transport: T, client_transport: U, messages: I) where - T: Transport, + T: Transport>, T::Dial: Send + 'static, T::Listener: Send + Unpin + 'static, T::ListenerUpgrade: Send + 'static, - U: Transport, + U: Transport>, U::Dial: Send + 'static, U::Listener: Send + 'static, U::ListenerUpgrade: Send + 'static, @@ -218,8 +249,8 @@ where }) } -fn expect_identity(output: Output, pk: &identity::PublicKey) - -> impl Future> +fn expect_identity(output: Output, pk: &identity::PublicKey) + -> impl Future, NoiseError>> { match output.0 { RemoteIdentity::IdentityKey(ref k) if k == pk => future::ok(output),