diff --git a/transports/webrtc/Cargo.toml b/transports/webrtc/Cargo.toml index 13acae4396c..aa728a1bdde 100644 --- a/transports/webrtc/Cargo.toml +++ b/transports/webrtc/Cargo.toml @@ -16,22 +16,29 @@ bytes = "1" futures = "0.3" futures-timer = "3" hex = "0.4" +hkdf = "0.12.3" if-watch = "2.0" libp2p-core = { version = "0.37.0", path = "../../core" } libp2p-noise = { version = "0.40.0", path = "../../transports/noise" } log = "0.4" multihash = { version = "0.16", default-features = false, features = ["sha2"] } +pem = "1.1.0" prost = "0.11" prost-codec = { version = "0.2.1", path = "../../misc/prost-codec" } rand = "0.8" -rcgen = "0.9.3" +rand_chacha = "0.3.1" +ring = { git = "https://github.com/briansmith/ring", features = ["std"], rev = "abe9529fc063f575759f8166bba02db171a3a0f6" } +rustls = "0.19.1" # Must match version in webrtc library. serde = { version = "1.0", features = ["derive"] } +sha2 = "0.10.6" +simple_x509 = "=0.2.2" # Version MUST be pinned to ensure a patch-update doesn't accidentially break deterministic certs. stun = "0.4" thiserror = "1" tinytemplate = "1.2" tokio = { version = "1.18", features = ["net"], optional = true} tokio-util = { version = "0.7", features = ["compat"], optional = true } -webrtc = { version = "0.5.0", optional = true } +# webrtc = { version = "0.5.0", optional = true } +webrtc = { git = "https://github.com/thomaseizinger/webrtc", optional = true, rev = "dc8d896784eb7ba8f5cecc2e5ae2917bb16c0c65" } [features] tokio = ["dep:tokio", "dep:tokio-util", "dep:webrtc"] @@ -45,6 +52,7 @@ env_logger = "0.9" hex-literal = "0.3" libp2p = { path = "../..", features = ["request-response", "webrtc"], default-features = false } unsigned-varint = { version = "0.7", features = ["asynchronous_codec"] } +rand_chacha = "0.3.1" [[test]] name = "smoke" diff --git a/transports/webrtc/src/tokio/certificate.rs b/transports/webrtc/src/tokio/certificate.rs index 519cf9a283c..d18dfe379af 100644 --- a/transports/webrtc/src/tokio/certificate.rs +++ b/transports/webrtc/src/tokio/certificate.rs @@ -18,31 +18,68 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use rand::{distributions::DistString, CryptoRng, Rng}; +use hkdf::Hkdf; +use libp2p_core::identity; +use rand::{CryptoRng, Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}; +use ring::test::rand::FixedSliceSequenceRandom; +use sha2::Sha256; +use webrtc::dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind}; use webrtc::peer_connection::certificate::RTCCertificate; use crate::tokio::fingerprint::Fingerprint; +use std::cell::UnsafeCell; +use std::time::{Duration, SystemTime}; + #[derive(Clone, PartialEq)] pub struct Certificate { inner: RTCCertificate, } - impl Certificate { - /// Generate new certificate. + /// Derives a new certificate from the provided keypair. /// - /// TODO: make use of `rng` - pub fn generate(_rng: &mut R) -> Result - where - R: CryptoRng + Rng, - { - let mut params = rcgen::CertificateParams::new(vec![ - rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 16) - ]); - params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; - Ok(Self { - inner: RTCCertificate::from_params(params).expect("default params to work"), - }) + /// This derivation is pure and will yield the same certificate for the same keypair. + pub fn new(identity: &identity::Keypair) -> Result { + let ed25519_keypair = match identity { + identity::Keypair::Ed25519(inner) => inner, + _ => return Err(CertificateError(Kind::UnsupportedKeypair)), + }; + + let hk = Hkdf::::from_prk(ed25519_keypair.secret().as_ref()) + .expect("key length to be valid"); + let mut seed = [0u8; 32]; + hk.expand(b"libp2p-webrtc-certificate".as_slice(), &mut seed) + .expect("32 is a valid length for Sha256 to output"); + + let mut rng = ChaCha20Rng::from_seed(seed); + + let (key_pair, key_pair_der_bytes) = make_keypair(&mut rng)?; + + let (certificate, expiry) = make_minimal_certificate(&mut rng, &key_pair)?; + + let certificate = RTCCertificate::from_existing( + webrtc::dtls::crypto::Certificate { + certificate: vec![rustls::Certificate(certificate.clone())], + private_key: CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ecdsa256(key_pair), + serialized_der: key_pair_der_bytes, + }, + }, + &pem::encode_config( + &pem::Pem { + tag: "CERTIFICATE".to_string(), + contents: certificate, + }, + pem::EncodeConfig { + line_ending: pem::LineEnding::LF, + }, + ), + expiry, + ); + + Ok(Self { inner: certificate }) } /// Returns SHA-256 fingerprint of this certificate. @@ -61,6 +98,12 @@ impl Certificate { Fingerprint::try_from_rtc_dtls(sha256_fingerprint).expect("we filtered by sha-256") } + /// Returns this certificate in PEM format. + #[cfg(test)] + pub fn to_pem(&self) -> &str { + self.inner.pem() + } + /// Extract the [`RTCCertificate`] from this wrapper. /// /// This function is `pub(crate)` to avoid leaking the `webrtc` dependency to our users. @@ -69,9 +112,194 @@ impl Certificate { } } +/// The year 2000. +const UNIX_2000: i64 = 946645200; + +/// The year 3000. +const UNIX_3000: i64 = 32503640400; + +/// OID for the organisation name. See . +const ORGANISATION_NAME_OID: [u64; 4] = [2, 5, 4, 10]; + +/// OID for Elliptic Curve Public Key Cryptography. See . +const EC_OID: [u64; 6] = [1, 2, 840, 10045, 2, 1]; + +/// OID for 256-bit Elliptic Curve Cryptography (ECC) with the P256 curve. See . +const P256_OID: [u64; 7] = [1, 2, 840, 10045, 3, 1, 7]; + +/// OID for the ECDSA signature algorithm with using SHA256 as the hash function. See . +const ECDSA_SHA256_OID: [u64; 7] = [1, 2, 840, 10045, 4, 3, 2]; + +/// Create a deterministic ECDSA keypair from the provided randomness source. +fn make_keypair(rng: &mut R) -> Result<(EcdsaKeyPair, Vec), Kind> +where + R: CryptoRng + Rng, +{ + let rng = FixedSliceSequenceRandom { + bytes: &[&rng.gen::<[u8; 32]>(), &rng.gen::<[u8; 32]>()], + current: UnsafeCell::new(0), + }; + + let document = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)?; + let der_bytes = document.as_ref().to_owned(); + + let key_pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &der_bytes, &rng)?; + + Ok((key_pair, der_bytes)) +} + +/// Constructs a minimal x509 certificate. +/// +/// The returned bytes are DER-encoded. +fn make_minimal_certificate( + rng: &mut R, + key_pair: &EcdsaKeyPair, +) -> Result<(Vec, SystemTime), Kind> +where + R: CryptoRng + Rng, +{ + let mut rand = [0u8; 32]; + rng.fill(&mut rand); + + let rng = FixedSliceSequenceRandom { + bytes: &[&rand], + current: UnsafeCell::new(0), + }; + + let certificate = simple_x509::X509::builder() + .issuer_utf8(Vec::from(ORGANISATION_NAME_OID), "rust-libp2p") + .subject_utf8(Vec::from(ORGANISATION_NAME_OID), "rust-libp2p") + .not_before_gen(UNIX_2000) + .not_after_gen(UNIX_3000) + .pub_key_ec( + Vec::from(EC_OID), + key_pair.public_key().as_ref().to_owned(), + Vec::from(P256_OID), + ) + .sign_oid(Vec::from(ECDSA_SHA256_OID)) + .build() + .sign( + |cert, _| Some(key_pair.sign(&rng, cert).ok()?.as_ref().to_owned()), + &vec![], // We close over the keypair so no need to pass it. + ) + .ok_or(Kind::FailedToSign)?; + + let der_bytes = certificate.x509_enc().ok_or(Kind::FailedToEncode)?; + + let expiry = SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs(UNIX_3000 as u64)) + .expect("year 3000 to be representable by SystemTime"); + + Ok((der_bytes, expiry)) +} + #[derive(thiserror::Error, Debug)] #[error("Failed to generate certificate")] -pub struct Error(#[from] Kind); +pub struct CertificateError(#[from] Kind); #[derive(thiserror::Error, Debug)] -enum Kind {} +enum Kind { + #[error(transparent)] + KeyRejected(#[from] ring::error::KeyRejected), + #[error(transparent)] + Unspecified(#[from] ring::error::Unspecified), + #[error("Failed to sign certificate")] + FailedToSign, + #[error("Failed to DER-encode certificate")] + FailedToEncode, + #[error("Only ED25519 keys are supported")] + UnsupportedKeypair, +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; + + #[test] + fn certificate_generation_is_deterministic() { + let keypair_seed = [0u8; 32]; + let cert_seed = [1u8; 32]; + + let (keypair1, _) = make_keypair(&mut ChaCha20Rng::from_seed(keypair_seed)).unwrap(); + let (keypair2, _) = make_keypair(&mut ChaCha20Rng::from_seed(keypair_seed)).unwrap(); + + let (bytes1, _) = + make_minimal_certificate(&mut ChaCha20Rng::from_seed(cert_seed), &keypair1).unwrap(); + let (bytes2, _) = + make_minimal_certificate(&mut ChaCha20Rng::from_seed(cert_seed), &keypair2).unwrap(); + + assert_eq!(bytes1, bytes2) + } + + #[test] + fn different_seed_yields_different_certificate() { + let keypair_seed = [0u8; 32]; + let cert1_seed = [1u8; 32]; + let cert2_seed = [2u8; 32]; + + let (keypair1, _) = make_keypair(&mut ChaCha20Rng::from_seed(keypair_seed)).unwrap(); + let (keypair2, _) = make_keypair(&mut ChaCha20Rng::from_seed(keypair_seed)).unwrap(); + + let (bytes1, _) = + make_minimal_certificate(&mut ChaCha20Rng::from_seed(cert1_seed), &keypair1).unwrap(); + let (bytes2, _) = + make_minimal_certificate(&mut ChaCha20Rng::from_seed(cert2_seed), &keypair2).unwrap(); + + assert_ne!(bytes1, bytes2) + } + + #[test] + fn keypair_generation_is_deterministic() { + let (key1, bytes1) = make_keypair(&mut ChaCha20Rng::from_seed([0u8; 32])).unwrap(); + let (key2, bytes2) = make_keypair(&mut ChaCha20Rng::from_seed([0u8; 32])).unwrap(); + + assert_eq!(bytes1, bytes2); + assert_eq!(key1.public_key().as_ref(), key2.public_key().as_ref()); + } + + // YOU MUST NOT EVER CHANGE THE ASSERTION IN THIS TEST. + // THIS TEST FAILING MEANS YOU BROKE DETERMINISTIC CERTIFICATION GENERATION. + // + // libp2p essentially performs certificate pinning through the `/certhash` protocol + // in multiaddresses. Peers expect boot nodes to have a certain certificate hash which + // must not change after that nodes has been deployed. + #[test] + fn certificate_snapshot_test() { + let ed25519_keypair = identity::ed25519::Keypair::decode( + [ + 53, 9, 7, 245, 172, 170, 239, 116, 74, 71, 187, 78, 240, 228, 186, 179, 151, 77, + 254, 157, 252, 125, 55, 88, 122, 71, 32, 138, 208, 235, 105, 116, 112, 48, 166, + 200, 235, 76, 139, 188, 249, 115, 178, 226, 0, 75, 229, 47, 222, 197, 68, 15, 212, + 233, 54, 5, 236, 0, 54, 166, 0, 75, 188, 237, + ] + .as_mut(), + ) + .unwrap(); + + let certificate = Certificate::new(&identity::Keypair::Ed25519(ed25519_keypair)).unwrap(); + + assert_eq!( + certificate.to_pem(), + "-----BEGIN CERTIFICATE----- +MIIBEDCBvgIBADAKBggqhkjOPQQDAjAWMRQwEgYDVQQKDAtydXN0LWxpYnAycDAi +GA8xOTk5MTIzMTEzMDAwMFoYDzI5OTkxMjMxMTMwMDAwWjAWMRQwEgYDVQQKDAty +dXN0LWxpYnAycDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP7qJcaJtDqlVagl +J8ZwSDFiI996MW8W4W05WboigwMfpDpBCfcOjeoERjjFepm/CAFhmxW6dt2mK67V +w1hIsWkwCgYIKoZIzj0EAwIDQQB77aXmXxlEpqUSJbn/Xp+W+5Oje+lXHojeOTAN +UaEhlIypY2gCreXJ0o2MsiCsT5NXfyHQJ5sSgiZtPx+ldICa +-----END CERTIFICATE----- +" + ) + } + + #[test] + fn cloned_certificate_is_equivalent() { + let certificate = Certificate::new(&identity::Keypair::generate_ed25519()).unwrap(); + + let cloned_certificate = certificate.clone(); + + assert!(certificate == cloned_certificate) + } +} diff --git a/transports/webrtc/src/tokio/mod.rs b/transports/webrtc/src/tokio/mod.rs index 81775c6d0f6..e4ffc2b33f5 100644 --- a/transports/webrtc/src/tokio/mod.rs +++ b/transports/webrtc/src/tokio/mod.rs @@ -18,7 +18,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -pub mod certificate; +mod certificate; mod connection; mod error; mod fingerprint; @@ -29,7 +29,7 @@ mod transport; mod udp_mux; mod upgrade; -pub use certificate::Certificate; +pub use certificate::CertificateError; pub use connection::Connection; pub use error::Error; pub use transport::Transport; diff --git a/transports/webrtc/src/tokio/transport.rs b/transports/webrtc/src/tokio/transport.rs index 25a328a0d46..0f26047c44e 100644 --- a/transports/webrtc/src/tokio/transport.rs +++ b/transports/webrtc/src/tokio/transport.rs @@ -41,7 +41,7 @@ use crate::tokio::{ error::Error, fingerprint::Fingerprint, udp_mux::{UDPMuxEvent, UDPMuxNewAddr}, - upgrade, + upgrade, CertificateError, }; /// A WebRTC transport with direct p2p communication (without a STUN server). @@ -59,17 +59,22 @@ impl Transport { /// /// ``` /// use libp2p_core::identity; + /// use webrtc::peer_connection::certificate::RTCCertificate; + /// use rand::distributions::DistString; /// use rand::thread_rng; - /// use libp2p_webrtc::tokio::{Transport, Certificate}; + /// use libp2p_webrtc::tokio::{Certificate, Transport}; /// /// let id_keys = identity::Keypair::generate_ed25519(); - /// let transport = Transport::new(id_keys, Certificate::generate(&mut thread_rng()).unwrap()); + /// + /// let transport = Transport::new(id_keys); /// ``` - pub fn new(id_keys: identity::Keypair, certificate: Certificate) -> Self { - Self { + pub fn new(id_keys: identity::Keypair) -> Result { + let certificate = Certificate::new(&id_keys)?; + + Ok(Self { config: Config::new(id_keys, certificate), listeners: SelectAll::new(), - } + }) } } @@ -241,8 +246,7 @@ impl ListenStream { { return Poll::Ready(TransportEvent::NewAddress { listener_id: self.listener_id, - listen_addr: self - .listen_multiaddress(ip, self.config.id_keys.public().to_peer_id()), + listen_addr: self.listen_multiaddress(ip), }); } } @@ -253,8 +257,7 @@ impl ListenStream { { return Poll::Ready(TransportEvent::AddressExpired { listener_id: self.listener_id, - listen_addr: self - .listen_multiaddress(ip, self.config.id_keys.public().to_peer_id()), + listen_addr: self.listen_multiaddress(ip), }); } } @@ -271,11 +274,10 @@ impl ListenStream { } /// Constructs a [`Multiaddr`] for the given IP address that represents our listen address. - fn listen_multiaddress(&self, ip: IpAddr, local_peer_id: PeerId) -> Multiaddr { + fn listen_multiaddress(&self, ip: IpAddr) -> Multiaddr { let socket_addr = SocketAddr::new(ip, self.listen_addr.port()); socketaddr_to_multiaddr(&socket_addr, Some(self.config.fingerprint)) - .with(Protocol::P2p(*local_peer_id.as_ref())) } } @@ -431,7 +433,6 @@ mod tests { use super::*; use futures::future::poll_fn; use libp2p_core::{multiaddr::Protocol, Transport as _}; - use rand::thread_rng; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; #[test] @@ -551,8 +552,7 @@ mod tests { #[tokio::test] async fn close_listener() { let id_keys = identity::Keypair::generate_ed25519(); - let mut transport = - Transport::new(id_keys, Certificate::generate(&mut thread_rng()).unwrap()); + let mut transport = Transport::new(id_keys).unwrap(); assert!(poll_fn(|cx| Pin::new(&mut transport).as_mut().poll(cx)) .now_or_never() diff --git a/transports/webrtc/tests/smoke.rs b/transports/webrtc/tests/smoke.rs index d100f905d1c..0fbb9438166 100644 --- a/transports/webrtc/tests/smoke.rs +++ b/transports/webrtc/tests/smoke.rs @@ -32,7 +32,7 @@ use libp2p::request_response::{ }; use libp2p::swarm::{Swarm, SwarmBuilder, SwarmEvent}; use libp2p::webrtc::tokio as webrtc; -use rand::{thread_rng, RngCore}; +use rand::RngCore; use std::{io, iter}; @@ -470,10 +470,7 @@ impl RequestResponseCodec for PingCodec { fn create_swarm() -> Result>> { let id_keys = identity::Keypair::generate_ed25519(); let peer_id = id_keys.public().to_peer_id(); - let transport = webrtc::Transport::new( - id_keys, - webrtc::Certificate::generate(&mut thread_rng()).unwrap(), - ); + let transport = webrtc::Transport::new(id_keys)?; let protocols = iter::once((PingProtocol(), ProtocolSupport::Full)); let cfg = RequestResponseConfig::default();