diff --git a/core/src/peer_record.rs b/core/src/peer_record.rs index 9c6b7f73f05..ac7c1dd19c9 100644 --- a/core/src/peer_record.rs +++ b/core/src/peer_record.rs @@ -1,11 +1,11 @@ -use libp2p_identity::{Keypair, PeerId, SigningError}; +use libp2p_identity::{Keypair, PeerId, PublicKey, SigningError}; use quick_protobuf::{BytesReader, Writer}; use web_time::SystemTime; use crate::{proto, signed_envelope, signed_envelope::SignedEnvelope, DecodeError, Multiaddr}; -const PAYLOAD_TYPE: &str = "/libp2p/routing-state-record"; -const DOMAIN_SEP: &str = "libp2p-routing-state"; +pub const PAYLOAD_TYPE: &str = "/libp2p/routing-state-record"; +pub const DOMAIN_SEP: &str = "libp2p-routing-state"; /// Represents a peer routing record. /// @@ -30,26 +30,7 @@ impl PeerRecord { /// If this function succeeds, the [`SignedEnvelope`] contained a peer record with a valid /// signature and can hence be considered authenticated. pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result { - use quick_protobuf::MessageRead; - - let (payload, signing_key) = - envelope.payload_and_signing_key(String::from(DOMAIN_SEP), PAYLOAD_TYPE.as_bytes())?; - let mut reader = BytesReader::from_bytes(payload); - let record = proto::PeerRecord::from_reader(&mut reader, payload).map_err(DecodeError)?; - - let peer_id = PeerId::from_bytes(&record.peer_id)?; - - if peer_id != signing_key.to_peer_id() { - return Err(FromEnvelopeError::MismatchedSignature); - } - - let seq = record.seq; - let addresses = record - .addresses - .into_iter() - .map(|a| a.multiaddr.to_vec().try_into()) - .collect::, _>>()?; - + let (_, peer_id, seq, addresses) = Self::try_deserialize_signed_envelope(&envelope)?; Ok(Self { peer_id, seq, @@ -126,6 +107,30 @@ impl PeerRecord { pub fn addresses(&self) -> &[Multiaddr] { self.addresses.as_slice() } + + pub fn try_deserialize_signed_envelope( + envelope: &SignedEnvelope, + ) -> Result<(&PublicKey, PeerId, u64, Vec), FromEnvelopeError> { + use quick_protobuf::MessageRead; + + let (payload, signing_key) = + envelope.payload_and_signing_key(String::from(DOMAIN_SEP), PAYLOAD_TYPE.as_bytes())?; + let mut reader = BytesReader::from_bytes(payload); + let record = proto::PeerRecord::from_reader(&mut reader, payload).map_err(DecodeError)?; + + let peer_id = PeerId::from_bytes(&record.peer_id)?; + + if peer_id != signing_key.to_peer_id() { + return Err(FromEnvelopeError::MismatchedSignature); + } + + let addresses = record + .addresses + .into_iter() + .map(|a| a.multiaddr.to_vec().try_into()) + .collect::, _>>()?; + Ok((signing_key, peer_id, record.seq, addresses)) + } } #[derive(thiserror::Error, Debug)] diff --git a/protocols/identify/src/behaviour.rs b/protocols/identify/src/behaviour.rs index 0cd27d90717..79014a96eb2 100644 --- a/protocols/identify/src/behaviour.rs +++ b/protocols/identify/src/behaviour.rs @@ -26,9 +26,11 @@ use std::{ }; use libp2p_core::{ - multiaddr, multiaddr::Protocol, transport::PortUse, ConnectedPoint, Endpoint, Multiaddr, + multiaddr::{self, Protocol}, + transport::PortUse, + ConnectedPoint, Endpoint, Multiaddr, }; -use libp2p_identity::{PeerId, PublicKey}; +use libp2p_identity::{Keypair, PeerId, PublicKey}; use libp2p_swarm::{ behaviour::{ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm}, ConnectionDenied, ConnectionId, DialError, ExternalAddresses, ListenAddresses, @@ -117,8 +119,10 @@ pub struct Config { /// Application-specific version of the protocol family used by the peer, /// e.g. `ipfs/1.0.0` or `polkadot/1.0.0`. protocol_version: String, - /// The public key of the local node. To report on the wire. - local_public_key: PublicKey, + /// The key of the local node. Only the public key will be report on the wire. + /// The behaviour will not produce [`PeerRecord`](libp2p_core::PeerRecord) when + /// supplied with a public key. + local_key: KeyType, /// Name and version of the local peer implementation, similar to the /// `User-Agent` header in the HTTP protocol. /// @@ -156,12 +160,29 @@ pub struct Config { impl Config { /// Creates a new configuration for the identify [`Behaviour`] that - /// advertises the given protocol version and public key. + /// advertises the given protocol version and public key. + /// Use [`new_with_keypair`](Config::new_with_keypair) for `signedPeerRecord` support. pub fn new(protocol_version: String, local_public_key: PublicKey) -> Self { Self { protocol_version, agent_version: format!("rust-libp2p/{}", env!("CARGO_PKG_VERSION")), - local_public_key, + local_key: local_public_key.into(), + interval: Duration::from_secs(5 * 60), + push_listen_addr_updates: false, + cache_size: 100, + hide_listen_addrs: false, + } + } + + /// Creates a new configuration for the identify [`Behaviour`] that + /// advertises the given protocol version and public key. + /// The private key will be used to sign [`PeerRecord`](libp2p_core::PeerRecord) + /// for verifiable address advertisement. + pub fn new_with_signed_peer_record(protocol_version: String, local_keypair: &Keypair) -> Self { + Self { + protocol_version, + agent_version: format!("rust-libp2p/{}", env!("CARGO_PKG_VERSION")), + local_key: local_keypair.into(), interval: Duration::from_secs(5 * 60), push_listen_addr_updates: false, cache_size: 100, @@ -209,7 +230,7 @@ impl Config { /// Get the local public key of the Config. pub fn local_public_key(&self) -> &PublicKey { - &self.local_public_key + self.local_key.public_key() } /// Get the agent version of the Config. @@ -380,7 +401,7 @@ impl NetworkBehaviour for Behaviour { Ok(Handler::new( self.config.interval, peer, - self.config.local_public_key.clone(), + self.config.local_key.clone(), self.config.protocol_version.clone(), self.config.agent_version.clone(), remote_addr.clone(), @@ -413,7 +434,7 @@ impl NetworkBehaviour for Behaviour { Ok(Handler::new( self.config.interval, peer, - self.config.local_public_key.clone(), + self.config.local_key.clone(), self.config.protocol_version.clone(), self.config.agent_version.clone(), // TODO: This is weird? That is the public address we dialed, @@ -670,6 +691,39 @@ impl PeerCache { } } +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum KeyType { + // With public key only the behaviour will not + // be able to produce a `SignedEnvelope`. + PublicKey(PublicKey), + Keypair { + keypair: Keypair, + public_key: PublicKey, + }, +} +impl From for KeyType { + fn from(value: PublicKey) -> Self { + Self::PublicKey(value.clone()) + } +} +impl From<&Keypair> for KeyType { + fn from(value: &Keypair) -> Self { + Self::Keypair { + public_key: value.public(), + keypair: value.clone(), + } + } +} +impl KeyType { + pub(crate) fn public_key(&self) -> &PublicKey { + match &self { + KeyType::PublicKey(pubkey) => pubkey, + KeyType::Keypair { public_key, .. } => public_key, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/protocols/identify/src/generated/structs.proto b/protocols/identify/src/generated/structs.proto index fbe6836bfdb..c9df399294c 100644 --- a/protocols/identify/src/generated/structs.proto +++ b/protocols/identify/src/generated/structs.proto @@ -24,4 +24,11 @@ message Identify { optional bytes observedAddr = 4; repeated string protocols = 3; + + // signedPeerRecord contains a serialized SignedEnvelope containing a PeerRecord, + // signed by the sending node. It contains the same addresses as the listenAddrs field, but + // in a form that lets us share authenticated addrs with other peers. + // see github.com/libp2p/go-libp2p/core/record/pb/envelope.proto and + // github.com/libp2p/go-libp2p/core/peer/pb/peer_record.proto for message definitions. + optional bytes signedPeerRecord = 8; } diff --git a/protocols/identify/src/generated/structs.rs b/protocols/identify/src/generated/structs.rs index 3be9b6f94ad..fcda2159a60 100644 --- a/protocols/identify/src/generated/structs.rs +++ b/protocols/identify/src/generated/structs.rs @@ -22,6 +22,7 @@ pub struct Identify { pub listenAddrs: Vec>, pub observedAddr: Option>, pub protocols: Vec, + pub signedPeerRecord: Option>, } impl<'a> MessageRead<'a> for Identify { @@ -35,6 +36,7 @@ impl<'a> MessageRead<'a> for Identify { Ok(18) => msg.listenAddrs.push(r.read_bytes(bytes)?.to_owned()), Ok(34) => msg.observedAddr = Some(r.read_bytes(bytes)?.to_owned()), Ok(26) => msg.protocols.push(r.read_string(bytes)?.to_owned()), + Ok(66) => msg.signedPeerRecord = Some(r.read_bytes(bytes)?.to_owned()), Ok(t) => { r.read_unknown(bytes, t)?; } Err(e) => return Err(e), } @@ -52,6 +54,7 @@ impl MessageWrite for Identify { + self.listenAddrs.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + self.observedAddr.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + self.protocols.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + + self.signedPeerRecord.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) } fn write_message(&self, w: &mut Writer) -> Result<()> { @@ -61,6 +64,7 @@ impl MessageWrite for Identify { for s in &self.listenAddrs { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } if let Some(ref s) = self.observedAddr { w.write_with_tag(34, |w| w.write_bytes(&**s))?; } for s in &self.protocols { w.write_with_tag(26, |w| w.write_string(&**s))?; } + if let Some(ref s) = self.signedPeerRecord { w.write_with_tag(66, |w| w.write_bytes(&**s))?; } Ok(()) } } diff --git a/protocols/identify/src/handler.rs b/protocols/identify/src/handler.rs index 7acdfceb0a6..c40242a5786 100644 --- a/protocols/identify/src/handler.rs +++ b/protocols/identify/src/handler.rs @@ -32,7 +32,7 @@ use libp2p_core::{ upgrade::{ReadyUpgrade, SelectUpgrade}, Multiaddr, }; -use libp2p_identity::{PeerId, PublicKey}; +use libp2p_identity::PeerId; use libp2p_swarm::{ handler::{ ConnectionEvent, DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, @@ -45,8 +45,8 @@ use smallvec::SmallVec; use tracing::Level; use crate::{ - protocol, - protocol::{Info, PushInfo, UpgradeError}, + behaviour::KeyType, + protocol::{self, Info, PushInfo, UpgradeError}, PROTOCOL_NAME, PUSH_PROTOCOL_NAME, }; @@ -80,8 +80,8 @@ pub struct Handler { /// The interval of `trigger_next_identify`, i.e. the recurrent delay. interval: Duration, - /// The public key of the local peer. - public_key: PublicKey, + /// The key of the local peer. + local_key: KeyType, /// Application-specific version of the protocol family used by the peer, /// e.g. `ipfs/1.0.0` or `polkadot/1.0.0`. @@ -125,10 +125,10 @@ pub enum Event { impl Handler { /// Creates a new `Handler`. - pub fn new( + pub(crate) fn new( interval: Duration, remote_peer_id: PeerId, - public_key: PublicKey, + local_key: KeyType, protocol_version: String, agent_version: String, observed_addr: Multiaddr, @@ -144,7 +144,7 @@ impl Handler { trigger_next_identify: Delay::new(Duration::ZERO), exchanged_one_periodic_identify: false, interval, - public_key, + local_key, protocol_version, agent_version, observed_addr, @@ -226,13 +226,23 @@ impl Handler { } fn build_info(&mut self) -> Info { + let signed_envelope = match &self.local_key { + KeyType::PublicKey(_) => None, + KeyType::Keypair { keypair, .. } => libp2p_core::PeerRecord::new( + keypair, + Vec::from_iter(self.external_addresses.iter().cloned()), + ) + .ok() + .map(|r| r.into_signed_envelope()), + }; Info { - public_key: self.public_key.clone(), + public_key: self.local_key.public_key().clone(), protocol_version: self.protocol_version.clone(), agent_version: self.agent_version.clone(), listen_addrs: Vec::from_iter(self.external_addresses.iter().cloned()), protocols: Vec::from_iter(self.local_supported_protocols.iter().cloned()), observed_addr: self.observed_addr.clone(), + signed_peer_record: signed_envelope, } } diff --git a/protocols/identify/src/protocol.rs b/protocols/identify/src/protocol.rs index 257ec1f88d2..e3354b5b02a 100644 --- a/protocols/identify/src/protocol.rs +++ b/protocols/identify/src/protocol.rs @@ -22,7 +22,7 @@ use std::io; use asynchronous_codec::{FramedRead, FramedWrite}; use futures::prelude::*; -use libp2p_core::{multiaddr, Multiaddr}; +use libp2p_core::{multiaddr, Multiaddr, PeerRecord, SignedEnvelope}; use libp2p_identity as identity; use libp2p_identity::PublicKey; use libp2p_swarm::StreamProtocol; @@ -53,6 +53,7 @@ pub struct Info { pub protocols: Vec, /// Address observed by or for the remote. pub observed_addr: Multiaddr, + pub signed_peer_record: Option, } impl Info { @@ -108,6 +109,10 @@ where listenAddrs: listen_addrs, observedAddr: Some(info.observed_addr.to_vec()), protocols: info.protocols.iter().map(|p| p.to_string()).collect(), + signedPeerRecord: info + .signed_peer_record + .clone() + .map(|r| r.into_protobuf_encoding()), }; let mut framed_io = FramedWrite::new( @@ -221,13 +226,30 @@ impl TryFrom for Info { } }; + let signed_peer_record = msg + .signedPeerRecord + .and_then(|b| SignedEnvelope::from_protobuf_encoding(b.as_ref()).ok()); + + // When signedPeerRecord contains valid addresses, ignore addresses in listenAddrs. + // When signedPeerRecord is invalid or signed by others, ignore the signedPeerRecord(set to + // `None`). + let (signed_peer_record, listen_addrs) = signed_peer_record + .as_ref() + .and_then(|envelope| PeerRecord::try_deserialize_signed_envelope(envelope).ok()) + .and_then(|(envelope_public_key, _, _, addresses)| { + (*envelope_public_key == public_key).then_some(addresses) + }) + .map(|addrs| (signed_peer_record, addrs)) + .unwrap_or_else(|| (None, parse_listen_addrs(msg.listenAddrs))); + let info = Info { public_key, protocol_version: msg.protocolVersion.unwrap_or_default(), agent_version: msg.agentVersion.unwrap_or_default(), - listen_addrs: parse_listen_addrs(msg.listenAddrs), + listen_addrs, protocols: parse_protocols(msg.protocols), observed_addr: parse_observed_addr(msg.observedAddr).unwrap_or(Multiaddr::empty()), + signed_peer_record, }; Ok(info) @@ -267,7 +289,11 @@ pub enum UpgradeError { #[cfg(test)] mod tests { + use std::str::FromStr; + + use libp2p_core::PeerRecord; use libp2p_identity as identity; + use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer}; use super::*; @@ -293,10 +319,87 @@ mod tests { .public() .encode_protobuf(), ), + signedPeerRecord: None, }; let info = PushInfo::try_from(payload).expect("not to fail"); assert_eq!(info.listen_addrs, vec![valid_multiaddr]) } + + #[test] + fn protobuf_roundtrip() { + // from go implementation of identify, + // see https://github.com/libp2p/go-libp2p/blob/2209ae05976df6a1cc2631c961f57549d109008c/p2p/protocol/identify/pb/identify.pb.go#L133 + // signedPeerRecord field is a dummy one that can't be properly parsed into SignedEnvelope, + // but the wire format doesn't care. + let go_protobuf: [u8; 375] = [ + 0x0a, 0x27, 0x70, 0x32, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x2f, 0x70, 0x62, 0x2f, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x2e, 0x70, 0x62, 0x22, 0x86, + 0x02, 0x0a, 0x08, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x12, 0x28, 0x0a, + 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, + 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, + 0x52, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, + 0x22, 0x0a, 0x0c, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x41, 0x64, 0x64, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x42, 0x36, 0x5a, + 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, + 0x62, 0x70, 0x32, 0x70, 0x2f, 0x67, 0x6f, 0x2d, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70, + 0x2f, 0x70, 0x32, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x2f, 0x70, 0x62, + ]; + let mut buf = [0u8; 375]; + let mut message = + proto::Identify::from_reader(&mut BytesReader::from_bytes(&go_protobuf), &go_protobuf) + .expect("read to succeed"); + + // The actual bytes they put in is "github.com/libp2p/go-libp2p/p2p/protocol/identify/pb". + // Starting with Z4 means it is zig-zag-encoded 4-byte varint of string, appended by + // protobuf. + assert_eq!( + String::from_utf8( + message + .signedPeerRecord + .clone() + .expect("field to be present") + ) + .expect("parse to succeed"), + "Z4github.com/libp2p/go-libp2p/p2p/protocol/identify/pb".to_string() + ); + message + .write_message(&mut Writer::new(&mut buf[..])) + .expect("same length after roundtrip"); + assert_eq!(go_protobuf, buf); + + let identity = identity::Keypair::generate_ed25519(); + let record = PeerRecord::new( + &identity, + vec![Multiaddr::from_str("/ip4/0.0.0.0").expect("parse to succeed")], + ) + .expect("infallible siging using ed25519"); + message + .signedPeerRecord + .replace(record.into_signed_envelope().into_protobuf_encoding()); + let mut buf = Vec::new(); + message + .write_message(&mut Writer::new(&mut buf)) + .expect("write to succeed"); + let parsed_message = proto::Identify::from_reader(&mut BytesReader::from_bytes(&buf), &buf) + .expect("read to succeed"); + assert_eq!(message, parsed_message) + } }