diff --git a/Cargo.lock b/Cargo.lock index d812fc0fa4b..d115f78db4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2899,7 +2899,7 @@ dependencies = [ [[package]] name = "libp2p-identify" -version = "0.46.0" +version = "0.47.0" dependencies = [ "asynchronous-codec", "either", diff --git a/Cargo.toml b/Cargo.toml index f6220cece5e..bec194058b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ libp2p-dcutr = { version = "0.13.0", path = "protocols/dcutr" } libp2p-dns = { version = "0.43.0", path = "transports/dns" } libp2p-floodsub = { version = "0.46.1", path = "protocols/floodsub" } libp2p-gossipsub = { version = "0.48.1", path = "protocols/gossipsub" } -libp2p-identify = { version = "0.46.0", path = "protocols/identify" } +libp2p-identify = { version = "0.47.0", path = "protocols/identify" } libp2p-identity = { version = "0.2.10" } libp2p-kad = { version = "0.47.0", path = "protocols/kad" } libp2p-mdns = { version = "0.47.0", path = "protocols/mdns" } diff --git a/protocols/identify/CHANGELOG.md b/protocols/identify/CHANGELOG.md index 720dd66f721..b70f5bc2383 100644 --- a/protocols/identify/CHANGELOG.md +++ b/protocols/identify/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.47.0 + +- Implement optional `signedPeerRecord` support for identify messages. + See [PR 5785](https://github.com/libp2p/rust-libp2p/pull/5785) + ## 0.46.0 - Add `hide_listen_addrs` option to prevent leaking (local) listen addresses. diff --git a/protocols/identify/Cargo.toml b/protocols/identify/Cargo.toml index 07730626f42..2546ceed0e1 100644 --- a/protocols/identify/Cargo.toml +++ b/protocols/identify/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-identify" edition = "2021" rust-version = { workspace = true } description = "Nodes identification protocol for libp2p" -version = "0.46.0" +version = "0.47.0" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/protocols/identify/src/behaviour.rs b/protocols/identify/src/behaviour.rs index 0cd27d90717..d23828af05f 100644 --- a/protocols/identify/src/behaviour.rs +++ b/protocols/identify/src/behaviour.rs @@ -21,14 +21,17 @@ use std::{ collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, num::NonZeroUsize, + sync::Arc, task::{Context, Poll}, time::Duration, }; 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 +120,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 send signed [`PeerRecord`](libp2p_core::PeerRecord) in + /// its identify message only when supplied with a keypair. + local_key: Arc, /// Name and version of the local peer implementation, similar to the /// `User-Agent` header in the HTTP protocol. /// @@ -156,12 +161,26 @@ 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_signed_peer_record`](Config::new_with_signed_peer_record) for + /// `signedPeerRecord` support. pub fn new(protocol_version: String, local_public_key: PublicKey) -> Self { + Self::new_with_key(protocol_version, local_public_key) + } + + /// 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::new_with_key(protocol_version, local_keypair) + } + + fn new_with_key(protocol_version: String, key: impl Into) -> Self { Self { protocol_version, agent_version: format!("rust-libp2p/{}", env!("CARGO_PKG_VERSION")), - local_public_key, + local_key: Arc::new(key.into()), interval: Duration::from_secs(5 * 60), push_listen_addr_updates: false, cache_size: 100, @@ -209,7 +228,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 +399,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 +432,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 +689,37 @@ impl PeerCache { } } +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum KeyType { + PublicKey(PublicKey), + Keypair { + keypair: Keypair, + public_key: PublicKey, + }, +} +impl From for KeyType { + fn from(value: PublicKey) -> Self { + Self::PublicKey(value) + } +} +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..d9891e27153 100644 --- a/protocols/identify/src/generated/structs.proto +++ b/protocols/identify/src/generated/structs.proto @@ -24,4 +24,12 @@ 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 https://github.com/libp2p/rust-libp2p/blob/8ac5b5aac5f5c25a85f1065e292deeaf58290189/core/src/generated/envelope.proto#L12 + // and https://github.com/libp2p/rust-libp2p/blob/8ac5b5aac5f5c25a85f1065e292deeaf58290189/core/src/generated/peer_record.proto#L11 + // 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..c2e31ae95f6 100644 --- a/protocols/identify/src/handler.rs +++ b/protocols/identify/src/handler.rs @@ -20,6 +20,7 @@ use std::{ collections::HashSet, + sync::Arc, task::{Context, Poll}, time::Duration, }; @@ -32,7 +33,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 +46,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 +81,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: Arc, /// Application-specific version of the protocol family used by the peer, /// e.g. `ipfs/1.0.0` or `polkadot/1.0.0`. @@ -125,10 +126,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: Arc, protocol_version: String, agent_version: String, observed_addr: Multiaddr, @@ -144,7 +145,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 +227,23 @@ impl Handler { } fn build_info(&mut self) -> Info { + let signed_envelope = match self.local_key.as_ref() { + 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..0fb4e32d357 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,8 @@ pub struct Info { pub protocols: Vec, /// Address observed by or for the remote. pub observed_addr: Multiaddr, + /// Verifiable addresses of the peer. + pub signed_peer_record: Option, } impl Info { @@ -108,6 +110,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( @@ -213,7 +219,7 @@ impl TryFrom for Info { type Error = UpgradeError; fn try_from(msg: proto::Identify) -> Result { - let public_key = { + let identify_public_key = { match parse_public_key(msg.publicKey) { Some(key) => key, // This will always produce a DecodingError if the public key is missing. @@ -221,13 +227,29 @@ impl TryFrom for Info { } }; + // When signedPeerRecord contains valid addresses, ignore addresses in listenAddrs. + // When signedPeerRecord is invalid or signed by others, ignore the signedPeerRecord(set to + // `None`). + let (listen_addrs, signed_envelope) = msg + .signedPeerRecord + .and_then(|b| { + let envelope = SignedEnvelope::from_protobuf_encoding(b.as_ref()).ok()?; + let peer_record = PeerRecord::from_signed_envelope(envelope).ok()?; + (peer_record.peer_id() == identify_public_key.to_peer_id()).then_some(( + peer_record.addresses().to_vec(), + Some(peer_record.into_signed_envelope()), + )) + }) + .unwrap_or_else(|| (parse_listen_addrs(msg.listenAddrs), None)); + let info = Info { - public_key, + public_key: identify_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: signed_envelope, }; 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) + } }