diff --git a/Cargo.toml b/Cargo.toml index 60b9920d603..6b43e4b497e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,8 @@ libp2p-swarm = { version = "0.23.0", path = "swarm" } libp2p-uds = { version = "0.23.0", path = "transports/uds", optional = true } libp2p-wasm-ext = { version = "0.23.0", path = "transports/wasm-ext", optional = true } libp2p-yamux = { version = "0.26.0", path = "muxers/yamux", optional = true } +libp2p-signed-envelope = { version = "0.1.0", path = "misc/signed-envelope", optional = true } +libp2p-peer-routing-record = { version = "0.1.0", path = "misc/peer-routing-record", optional = true } multiaddr = { package = "parity-multiaddr", version = "0.9.3", path = "misc/multiaddr" } multihash = "0.11.0" parking_lot = "0.11.0" @@ -104,6 +106,8 @@ members = [ "misc/multiaddr", "misc/multistream-select", "misc/peer-id-generator", + "misc/signed-envelope", + "misc/peer-routing-record", "muxers/mplex", "muxers/yamux", "protocols/floodsub", diff --git a/core/src/identity.rs b/core/src/identity.rs index 916720f4443..9b38fb1ff30 100644 --- a/core/src/identity.rs +++ b/core/src/identity.rs @@ -30,6 +30,7 @@ pub mod error; use self::error::*; use crate::{PeerId, keys_proto}; +use std::convert::TryFrom; /// Identity keypair of a node. /// @@ -130,61 +131,34 @@ pub enum PublicKey { Secp256k1(secp256k1::PublicKey) } -impl PublicKey { - /// Verify a signature for a message using this public key, i.e. check - /// that the signature has been produced by the corresponding - /// private key (authenticity), and that the message has not been - /// tampered with (integrity). - pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool { - use PublicKey::*; +impl Into for PublicKey { + fn into(self) -> keys_proto::PublicKey { match self { - Ed25519(pk) => pk.verify(msg, sig), - #[cfg(not(target_arch = "wasm32"))] - Rsa(pk) => pk.verify(msg, sig), - #[cfg(feature = "secp256k1")] - Secp256k1(pk) => pk.verify(msg, sig) - } - } - - /// Encode the public key into a protobuf structure for storage or - /// exchange with other nodes. - pub fn into_protobuf_encoding(self) -> Vec { - use prost::Message; - - let public_key = match self { PublicKey::Ed25519(key) => keys_proto::PublicKey { r#type: keys_proto::KeyType::Ed25519 as i32, - data: key.encode().to_vec() + data: key.encode().to_vec(), }, #[cfg(not(target_arch = "wasm32"))] PublicKey::Rsa(key) => keys_proto::PublicKey { r#type: keys_proto::KeyType::Rsa as i32, - data: key.encode_x509() + data: key.encode_x509(), }, #[cfg(feature = "secp256k1")] PublicKey::Secp256k1(key) => keys_proto::PublicKey { r#type: keys_proto::KeyType::Secp256k1 as i32, - data: key.encode().to_vec() + data: key.encode().to_vec(), } - }; - - let mut buf = Vec::with_capacity(public_key.encoded_len()); - public_key.encode(&mut buf).expect("Vec provides capacity as needed"); - buf + } } +} - /// Decode a public key from a protobuf structure, e.g. read from storage - /// or received from another node. - pub fn from_protobuf_encoding(bytes: &[u8]) -> Result { - use prost::Message; - - #[allow(unused_mut)] // Due to conditional compilation. - let mut pubkey = keys_proto::PublicKey::decode(bytes) - .map_err(|e| DecodingError::new("Protobuf").source(e))?; +impl TryFrom for PublicKey { + type Error = DecodingError; + fn try_from(pubkey: keys_proto::PublicKey) -> Result { let key_type = keys_proto::KeyType::from_i32(pubkey.r#type) .ok_or_else(|| DecodingError::new(format!("unknown key type: {}", pubkey.r#type)))?; @@ -212,6 +186,47 @@ impl PublicKey { } } } +} + +impl PublicKey { + /// Verify a signature for a message using this public key, i.e. check + /// that the signature has been produced by the corresponding + /// private key (authenticity), and that the message has not been + /// tampered with (integrity). + pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool { + use PublicKey::*; + match self { + Ed25519(pk) => pk.verify(msg, sig), + #[cfg(not(target_arch = "wasm32"))] + Rsa(pk) => pk.verify(msg, sig), + #[cfg(feature = "secp256k1")] + Secp256k1(pk) => pk.verify(msg, sig) + } + } + + /// Encode the public key into a protobuf structure for storage or + /// exchange with other nodes. + pub fn into_protobuf_encoding(self) -> Vec { + use prost::Message; + + let public_key: keys_proto::PublicKey = self.into(); + + let mut buf = Vec::with_capacity(public_key.encoded_len()); + public_key.encode(&mut buf).expect("Vec provides capacity as needed"); + buf + } + + /// Decode a public key from a protobuf structure, e.g. read from storage + /// or received from another node. + pub fn from_protobuf_encoding(bytes: &[u8]) -> Result { + use prost::Message; + + #[allow(unused_mut)] // Due to conditional compilation. + let mut pubkey = keys_proto::PublicKey::decode(bytes) + .map_err(|e| DecodingError::new("Protobuf").source(e))?; + + PublicKey::try_from(pubkey) + } /// Convert the `PublicKey` into the corresponding `PeerId`. pub fn into_peer_id(self) -> PeerId { diff --git a/core/src/lib.rs b/core/src/lib.rs index ae244dc8eb7..cd26b1b553f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -35,7 +35,7 @@ //! define how to upgrade each individual substream to use a protocol. //! See the `upgrade` module. -mod keys_proto { +pub mod keys_proto { include!(concat!(env!("OUT_DIR"), "/keys_proto.rs")); } diff --git a/misc/peer-routing-record/Cargo.toml b/misc/peer-routing-record/Cargo.toml new file mode 100644 index 00000000000..0f8a5a8d866 --- /dev/null +++ b/misc/peer-routing-record/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "libp2p-peer-routing-record" +version = "0.1.0" +authors = ["blacktemplar "] +edition = "2018" +license = "MIT" +repository = "https://github.com/libp2p/rust-libp2p" +description = "Provides a structure for storing a signed peer routing record." + + +[dependencies] +libp2p-core = { path = "../../core" } +prost = "0.6.1" +libp2p-signed-envelope = { path = "../signed-envelope" } +log = "0.4" + +[build-dependencies] +prost-build = "0.6" diff --git a/misc/peer-routing-record/build.rs b/misc/peer-routing-record/build.rs new file mode 100644 index 00000000000..bf6b6b46aa1 --- /dev/null +++ b/misc/peer-routing-record/build.rs @@ -0,0 +1,23 @@ +// Copyright 2020 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. + +fn main() { + prost_build::compile_protos(&["src/peer_record.proto"], &["src"]).unwrap(); +} diff --git a/misc/peer-routing-record/src/lib.rs b/misc/peer-routing-record/src/lib.rs new file mode 100644 index 00000000000..3760af613a6 --- /dev/null +++ b/misc/peer-routing-record/src/lib.rs @@ -0,0 +1,155 @@ +use libp2p_core::identity::Keypair; +use libp2p_core::{Multiaddr, PeerId}; +use libp2p_signed_envelope::{Envelope, Error, Record}; +use log::error; +use prost::Message; +use std::convert::TryFrom; +use std::time::{SystemTime, UNIX_EPOCH}; + +mod peer_record_proto { + include!(concat!(env!("OUT_DIR"), "/peer_record_proto.rs")); +} + +#[derive(Clone, Debug, PartialEq)] +struct PeerRecord { + peer_id: PeerId, + addresses: Vec, + seq: u64, +} + +#[derive(Debug)] +pub enum DecodeError { + ProtoBufDecodingError(prost::DecodeError), + PeerIdDecodingError(Vec), + MultiaddrDecodingError(>>::Error), +} + +impl TryFrom> for PeerRecord { + type Error = DecodeError; + + fn try_from(value: Vec) -> Result { + use DecodeError::*; + let record = peer_record_proto::PeerRecord::decode(value.as_slice()) + .map_err(ProtoBufDecodingError)?; + let addresses: Result, >>::Error> = record + .addresses + .into_iter() + .map(|address_info| Multiaddr::try_from(address_info.multiaddr)) + .collect(); + Ok(PeerRecord { + peer_id: PeerId::from_bytes(record.peer_id).map_err(PeerIdDecodingError)?, + addresses: addresses.map_err(MultiaddrDecodingError)?, + seq: record.seq, + }) + } +} + +impl Into> for PeerRecord { + fn into(self) -> Vec { + let record = peer_record_proto::PeerRecord { + peer_id: self.peer_id.into_bytes(), + addresses: self + .addresses + .iter() + .map(|addr| peer_record_proto::peer_record::AddressInfo { + multiaddr: addr.to_vec(), + }) + .collect(), + seq: self.seq, + }; + let mut result = Vec::with_capacity(record.encoded_len()); + record + .encode(&mut result) + .expect("Vec provides capacity as needed"); + result + } +} + +impl Record for PeerRecord { + fn payload_type() -> &'static [u8] { + "/libp2p/routing-state-record".as_bytes() + } + + fn domain() -> &'static str { + "libp2p-routing-state" + } +} + +type SignedPeerRecord = Envelope; + +pub fn timestamp_seq() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .and_then(|d| match d.as_nanos() { + x if x <= u64::MAX as u128 => u64::try_from(x).ok(), + x => x + .checked_rem(u64::MAX as u128) + .and_then(|x| u64::try_from(x).ok()), + }) + .unwrap_or_else(|| { + error!( + "Couldn't use system time for creating routing record sequence numbers. The \ + system time seems to be before unix epoch." + ); + 0 + }) +} + +impl PeerRecord { + pub fn new(peer_id: PeerId, addresses: Vec) -> Self { + Self { + peer_id, + addresses, + seq: timestamp_seq(), + } + } + + pub fn sign(self, keypair: &Keypair) -> Result> { + SignedPeerRecord::sign(self, keypair) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libp2p_core::multiaddr::Protocol::*; + use std::iter::FromIterator; + use std::net::{Ipv4Addr, Ipv6Addr}; + + fn record() -> PeerRecord { + PeerRecord::new( + PeerId::random(), + vec![ + Multiaddr::from(Ipv4Addr::new(1, 2, 3, 4)), + Multiaddr::from(Ipv6Addr::new(5, 6, 7, 8, 9, 10, 11, 12)), + Multiaddr::from_iter( + vec![ + Ip4(Ipv4Addr::new(13, 14, 15, 16)), + Ip6(Ipv6Addr::new(17, 18, 19, 20, 21, 22, 23, 24)), + ] + .into_iter(), + ), + ], + ) + } + + #[test] + fn test_encoding_decoding_tour() { + let record = record(); + let vec: Vec = record.clone().into(); + + let record2 = PeerRecord::try_from(vec).unwrap(); + + assert_eq!(record, record2); + } + + #[test] + fn test_sign_and_verify_record() { + let record = record(); + let keypair = Keypair::generate_ed25519(); + let envelope = record.sign(&keypair).unwrap(); + + assert!(envelope.verify()); + } +} diff --git a/misc/peer-routing-record/src/peer_record.proto b/misc/peer-routing-record/src/peer_record.proto new file mode 100644 index 00000000000..8536d8ca5b1 --- /dev/null +++ b/misc/peer-routing-record/src/peer_record.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package peer_record_proto; + +// PeerRecord contains the listen addresses for a peer at a particular point in time. +message PeerRecord { + // AddressInfo wraps a multiaddr. In the future, it may be extended to + // contain additional metadata, such as "routability" (whether an address is + // local or global, etc). + message AddressInfo { + bytes multiaddr = 1; + } + + // the peer id of the subject of the record (who these addresses belong to). + bytes peer_id = 1; + + // A monotonically increasing sequence number, used for record ordering. + uint64 seq = 2; + + // All current listen addresses + repeated AddressInfo addresses = 3; +} diff --git a/misc/signed-envelope/Cargo.toml b/misc/signed-envelope/Cargo.toml new file mode 100644 index 00000000000..90b1094bc5c --- /dev/null +++ b/misc/signed-envelope/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "libp2p-signed-envelope" +version = "0.1.0" +authors = ["blacktemplar "] +edition = "2018" +license = "MIT" +repository = "https://github.com/libp2p/rust-libp2p" +description = "Provides a signed envelope structure that can contain and sign arbitrary byte payloads." + + +[dependencies] +libp2p-core = { path = "../../core" } +unsigned-varint = "0.5" +prost = "0.6.1" + +[build-dependencies] +prost-build = "0.6" diff --git a/misc/signed-envelope/build.rs b/misc/signed-envelope/build.rs new file mode 100644 index 00000000000..f3983c88e67 --- /dev/null +++ b/misc/signed-envelope/build.rs @@ -0,0 +1,23 @@ +// Copyright 2020 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. + +fn main() { + prost_build::compile_protos(&["src/envelope.proto"], &["src", "../../core/src"]).unwrap(); +} diff --git a/misc/signed-envelope/src/envelope.proto b/misc/signed-envelope/src/envelope.proto new file mode 100644 index 00000000000..fc61f53a70d --- /dev/null +++ b/misc/signed-envelope/src/envelope.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package envelope_proto; + +import "keys.proto"; + +message Envelope { + // The public_key field contains the public key whose secret counterpart was used to sign the message. This MUST be + // consistent with the peer id of the signing peer, as the recipient will derive the peer id of the signer from this + // key. + keys_proto.PublicKey public_key = 1; + + // The payload_type field contains a multicodec-prefixed type indicator. + bytes payload_type = 2; + + // The payload field contains the arbitrary byte string payload. + bytes payload = 3; + + // The signature field contains a signature of all fields except public_key, generated as described below. + bytes signature = 5; +} diff --git a/misc/signed-envelope/src/lib.rs b/misc/signed-envelope/src/lib.rs new file mode 100644 index 00000000000..138b55633b4 --- /dev/null +++ b/misc/signed-envelope/src/lib.rs @@ -0,0 +1,137 @@ +use libp2p_core::identity::error::{DecodingError, SigningError as IdentitySigningError}; +use libp2p_core::identity::Keypair; +use libp2p_core::keys_proto; +use libp2p_core::PublicKey; +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use unsigned_varint::encode; +use Error::*; + +pub mod envelope_proto { + include!(concat!(env!("OUT_DIR"), "/envelope_proto.rs")); +} + +pub trait Record: TryFrom> + TryInto> + Clone { + fn payload_type() -> &'static [u8]; + fn domain() -> &'static str; +} + +pub struct Envelope { + pub content: T, + pub public_key: PublicKey, + pub signature: Vec, + + payload: Vec, +} + +#[derive(Debug)] +pub enum Error +where + >>::Error: Debug, + >>::Error: Debug, +{ + SerializationError(>>::Error), + DeserializationError(>>::Error), + EmptyDomain, + EmptyPayload, + SigningError(IdentitySigningError), + NoPublicKey, + InvalidSignature, + PublicKeyDecodingError(DecodingError), + WrongPayloadType, +} + +impl Envelope +where + >>::Error: Debug, + >>::Error: Debug, +{ + pub fn sign(content: T, key_pair: &Keypair) -> Result> { + let payload: Vec = content.clone().try_into().map_err(SerializationError)?; + + let buffer = Self::get_buffer(&payload)?; + + let signature = key_pair.sign(&buffer).map_err(SigningError)?; + + Ok(Self { + content, + public_key: key_pair.public(), + signature, + payload, + }) + } + + pub fn verify(&self) -> bool { + if let Ok(buffer) = Self::get_buffer(&self.payload) { + self.public_key.verify(&buffer, &self.signature) + } else { + false + } + } + + fn get_buffer(payload: &[u8]) -> Result, Error> { + if T::domain() == "" { + return Err(EmptyDomain); + } + + if payload.len() == 0 { + return Err(EmptyPayload); + } + + Ok(Self::concatenate_payloads(&[ + T::domain().as_bytes(), + T::payload_type(), + &payload, + ])) + } + + /// Concatenates all payloads and prefixes them with their length + fn concatenate_payloads(payloads: &[&[u8]]) -> Vec { + let mut result = Vec::new(); + let mut buf = encode::usize_buffer(); + for payload in payloads { + result.extend_from_slice(encode::usize(payload.len(), &mut buf)); + result.extend_from_slice(payload); + } + result + } +} + +impl Into for Envelope { + fn into(self) -> envelope_proto::Envelope { + envelope_proto::Envelope { + public_key: Some(self.public_key.into()), + payload_type: T::payload_type().to_vec(), + payload: self.payload, + signature: self.signature, + } + } +} + +impl TryFrom for Envelope +where + >>::Error: Debug, + >>::Error: Debug, +{ + type Error = Error; + + fn try_from(value: envelope_proto::Envelope) -> Result { + let public_key = PublicKey::try_from(value.public_key.ok_or(NoPublicKey)?) + .map_err(PublicKeyDecodingError)?; + + let payload = value.payload; + + if value.payload_type != T::payload_type() { + return Err(WrongPayloadType); + } + + let content = T::try_from(payload.clone()).map_err(DeserializationError)?; + + Ok(Self { + content, + public_key, + signature: value.signature, + payload, + }) + } +}