diff --git a/CHANGELOG.md b/CHANGELOG.md index 560fa51d324..4842ad21752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - [`libp2p-uds` CHANGELOG](transports/uds/CHANGELOG.md) - [`libp2p-wasm-ext` CHANGELOG](transports/wasm-ext/CHANGELOG.md) - [`libp2p-websocket` CHANGELOG](transports/websocket/CHANGELOG.md) +- [`libp2p-tls` CHANGELOG](transports/tls/CHANGELOG.md) ## Multiplexers @@ -65,6 +66,8 @@ See [PR 2962]. +- Introduce [`libp2p-tls` `v0.1.0`](transports/tls/CHANGELOG.md#010). See [PR 2945]. + - Update individual crates. - Update to [`libp2p-autonat` `v0.8.0`](protocols/autonat/CHANGELOG.md#0080). - Update to [`libp2p-core` `v0.37.0`](core/CHANGELOG.md#0370). @@ -94,6 +97,7 @@ [PR 2918]: https://github.com/libp2p/rust-libp2p/pull/2918 [PR 2962]: https://github.com/libp2p/rust-libp2p/pull/2962 +[PR 2945]: https://github.com/libp2p/rust-libp2p/pull/2945 # 0.48.0 diff --git a/Cargo.toml b/Cargo.toml index 5e6e5028a1b..26169817cfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ full = [ "serde", "tcp", "tokio", + "tls", "uds", "wasm-bindgen", "wasm-ext", @@ -52,6 +53,7 @@ floodsub = ["dep:libp2p-floodsub"] identify = ["dep:libp2p-identify", "libp2p-metrics?/identify"] kad = ["dep:libp2p-kad", "libp2p-metrics?/kad"] gossipsub = ["dep:libp2p-gossipsub", "libp2p-metrics?/gossipsub"] +tls = ["dep:libp2p-tls"] metrics = ["dep:libp2p-metrics"] mdns = ["dep:libp2p-mdns"] mplex = ["dep:libp2p-mplex"] @@ -118,6 +120,7 @@ libp2p-dns = { version = "0.37.0", path = "transports/dns", optional = true } libp2p-mdns = { version = "0.41.0", path = "protocols/mdns", optional = true } libp2p-tcp = { version = "0.37.0", path = "transports/tcp", optional = true } libp2p-websocket = { version = "0.39.0", path = "transports/websocket", optional = true } +libp2p-tls = { version = "0.1.0-alpha", path = "transports/tls", optional = true } [target.'cfg(not(target_os = "unknown"))'.dependencies] libp2p-gossipsub = { version = "0.42.1", path = "protocols/gossipsub", optional = true } @@ -157,6 +160,7 @@ members = [ "transports/deflate", "transports/dns", "transports/noise", + "transports/tls", "transports/plaintext", "transports/pnet", "transports/tcp", diff --git a/src/lib.rs b/src/lib.rs index 1be633baf86..d2b4226d2ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,6 +124,10 @@ pub use libp2p_swarm as swarm; #[cfg_attr(docsrs, doc(cfg(feature = "tcp")))] #[doc(inline)] pub use libp2p_tcp as tcp; +#[cfg(feature = "tls")] +#[cfg_attr(docsrs, doc(cfg(feature = "tls")))] +#[doc(inline)] +pub use libp2p_tls as tls; #[cfg(feature = "uds")] #[cfg_attr(docsrs, doc(cfg(feature = "uds")))] #[doc(inline)] diff --git a/transports/tls/CHANGELOG.md b/transports/tls/CHANGELOG.md new file mode 100644 index 00000000000..9a05756f769 --- /dev/null +++ b/transports/tls/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 [unreleased] + +Initial release. diff --git a/transports/tls/Cargo.toml b/transports/tls/Cargo.toml new file mode 100644 index 00000000000..ac8244aecc5 --- /dev/null +++ b/transports/tls/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "libp2p-tls" +version = "0.1.0-alpha" +edition = "2021" +license = "MIT" +exclude = ["src/test_assets"] + +[dependencies] +futures = { version = "0.3.24", default-features = false } +futures-rustls = "0.22.2" +libp2p-core = { version = "0.37.0", path = "../../core" } +rcgen = "0.9.2" +ring = "0.16.20" +thiserror = "1.0.36" +webpki = "0.22.0" +x509-parser = "0.14.0" +yasna = "0.5.0" + +# Exposed dependencies. Breaking changes to these are breaking changes to us. +[dependencies.rustls] +version = "0.20.7" +default-features = false +features = ["dangerous_configuration"] # Must enable this to allow for custom verification code. + +[dev-dependencies] +hex = "0.4.3" +hex-literal = "0.3.4" +libp2p = { path = "../..", features = ["yamux"], default-features = false } +tokio = { version = "1.21.1", features = ["full"] } + +# Passing arguments to the docsrs builder in order to properly document cfg's. +# More information: https://docs.rs/about/builds#cross-compiling +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +rustc-args = ["--cfg", "docsrs"] diff --git a/transports/tls/src/certificate.rs b/transports/tls/src/certificate.rs new file mode 100644 index 00000000000..43d9c8468ca --- /dev/null +++ b/transports/tls/src/certificate.rs @@ -0,0 +1,552 @@ +// Copyright 2021 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. + +//! X.509 certificate handling for libp2p +//! +//! This module handles generation, signing, and verification of certificates. + +use libp2p_core::{identity, PeerId}; +use x509_parser::{prelude::*, signature_algorithm::SignatureAlgorithm}; + +/// The libp2p Public Key Extension is a X.509 extension +/// with the Object Identier 1.3.6.1.4.1.53594.1.1, +/// allocated by IANA to the libp2p project at Protocol Labs. +const P2P_EXT_OID: [u64; 9] = [1, 3, 6, 1, 4, 1, 53594, 1, 1]; + +/// The peer signs the concatenation of the string `libp2p-tls-handshake:` +/// and the public key that it used to generate the certificate carrying +/// the libp2p Public Key Extension, using its private host key. +/// This signature provides cryptographic proof that the peer was +/// in possession of the private host key at the time the certificate was signed. +const P2P_SIGNING_PREFIX: [u8; 21] = *b"libp2p-tls-handshake:"; + +// Certificates MUST use the NamedCurve encoding for elliptic curve parameters. +// Similarly, hash functions with an output length less than 256 bits MUST NOT be used. +static P2P_SIGNATURE_ALGORITHM: &rcgen::SignatureAlgorithm = &rcgen::PKCS_ECDSA_P256_SHA256; + +/// Generates a self-signed TLS certificate that includes a libp2p-specific +/// certificate extension containing the public key of the given keypair. +pub fn generate( + identity_keypair: &identity::Keypair, +) -> Result<(rustls::Certificate, rustls::PrivateKey), GenError> { + // Keypair used to sign the certificate. + // SHOULD NOT be related to the host's key. + // Endpoints MAY generate a new key and certificate + // for every connection attempt, or they MAY reuse the same key + // and certificate for multiple connections. + let certificate_keypair = rcgen::KeyPair::generate(P2P_SIGNATURE_ALGORITHM)?; + let rustls_key = rustls::PrivateKey(certificate_keypair.serialize_der()); + + let certificate = { + let mut params = rcgen::CertificateParams::new(vec![]); + params.distinguished_name = rcgen::DistinguishedName::new(); + params.custom_extensions.push(make_libp2p_extension( + identity_keypair, + &certificate_keypair, + )?); + params.alg = P2P_SIGNATURE_ALGORITHM; + params.key_pair = Some(certificate_keypair); + rcgen::Certificate::from_params(params)? + }; + + let rustls_certificate = rustls::Certificate(certificate.serialize_der()?); + + Ok((rustls_certificate, rustls_key)) +} + +/// Attempts to parse the provided bytes as a [`P2pCertificate`]. +/// +/// For this to succeed, the certificate must contain the specified extension and the signature must +/// match the embedded public key. +pub fn parse(certificate: &rustls::Certificate) -> Result, ParseError> { + let certificate = parse_unverified(certificate.as_ref())?; + + certificate.verify()?; + + Ok(certificate) +} + +/// An X.509 certificate with a libp2p-specific extension +/// is used to secure libp2p connections. +#[derive(Debug)] +pub struct P2pCertificate<'a> { + certificate: X509Certificate<'a>, + /// This is a specific libp2p Public Key Extension with two values: + /// * the public host key + /// * a signature performed using the private host key + extension: P2pExtension, +} + +/// The contents of the specific libp2p extension, containing the public host key +/// and a signature performed using the private host key. +#[derive(Debug)] +pub struct P2pExtension { + public_key: identity::PublicKey, + /// This signature provides cryptographic proof that the peer was + /// in possession of the private host key at the time the certificate was signed. + signature: Vec, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct GenError(#[from] rcgen::RcgenError); + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ParseError(#[from] pub(crate) webpki::Error); + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct VerificationError(#[from] pub(crate) webpki::Error); + +/// Internal function that only parses but does not verify the certificate. +/// +/// Useful for testing but unsuitable for production. +fn parse_unverified(der_input: &[u8]) -> Result { + let x509 = X509Certificate::from_der(der_input) + .map(|(_rest_input, x509)| x509) + .map_err(|_| webpki::Error::BadDer)?; + + let p2p_ext_oid = der_parser::oid::Oid::from(&P2P_EXT_OID) + .expect("This is a valid OID of p2p extension; qed"); + + let mut libp2p_extension = None; + + for ext in x509.extensions() { + let oid = &ext.oid; + if oid == &p2p_ext_oid && libp2p_extension.is_some() { + // The extension was already parsed + return Err(webpki::Error::BadDer); + } + + if oid == &p2p_ext_oid { + // The public host key and the signature are ANS.1-encoded + // into the SignedKey data structure, which is carried + // in the libp2p Public Key Extension. + // SignedKey ::= SEQUENCE { + // publicKey OCTET STRING, + // signature OCTET STRING + // } + let (public_key, signature): (Vec, Vec) = + yasna::decode_der(ext.value).map_err(|_| webpki::Error::ExtensionValueInvalid)?; + // The publicKey field of SignedKey contains the public host key + // of the endpoint, encoded using the following protobuf: + // enum KeyType { + // RSA = 0; + // Ed25519 = 1; + // Secp256k1 = 2; + // ECDSA = 3; + // } + // message PublicKey { + // required KeyType Type = 1; + // required bytes Data = 2; + // } + let public_key = identity::PublicKey::from_protobuf_encoding(&public_key) + .map_err(|_| webpki::Error::UnknownIssuer)?; + let ext = P2pExtension { + public_key, + signature, + }; + libp2p_extension = Some(ext); + continue; + } + + if ext.critical { + // Endpoints MUST abort the connection attempt if the certificate + // contains critical extensions that the endpoint does not understand. + return Err(webpki::Error::UnsupportedCriticalExtension); + } + + // Implementations MUST ignore non-critical extensions with unknown OIDs. + } + + // The certificate MUST contain the libp2p Public Key Extension. + // If this extension is missing, endpoints MUST abort the connection attempt. + let extension = libp2p_extension.ok_or(webpki::Error::BadDer)?; + + let certificate = P2pCertificate { + certificate: x509, + extension, + }; + + Ok(certificate) +} + +fn make_libp2p_extension( + identity_keypair: &identity::Keypair, + certificate_keypair: &rcgen::KeyPair, +) -> Result { + // The peer signs the concatenation of the string `libp2p-tls-handshake:` + // and the public key that it used to generate the certificate carrying + // the libp2p Public Key Extension, using its private host key. + let signature = { + let mut msg = vec![]; + msg.extend(P2P_SIGNING_PREFIX); + msg.extend(certificate_keypair.public_key_der()); + + identity_keypair + .sign(&msg) + .map_err(|_| rcgen::RcgenError::RingUnspecified)? + }; + + // The public host key and the signature are ANS.1-encoded + // into the SignedKey data structure, which is carried + // in the libp2p Public Key Extension. + // SignedKey ::= SEQUENCE { + // publicKey OCTET STRING, + // signature OCTET STRING + // } + let extension_content = { + let serialized_pubkey = identity_keypair.public().to_protobuf_encoding(); + yasna::encode_der(&(serialized_pubkey, signature)) + }; + + // This extension MAY be marked critical. + let mut ext = rcgen::CustomExtension::from_oid_content(&P2P_EXT_OID, extension_content); + ext.set_criticality(true); + + Ok(ext) +} + +impl P2pCertificate<'_> { + /// The [`PeerId`] of the remote peer. + pub fn peer_id(&self) -> PeerId { + self.extension.public_key.to_peer_id() + } + + /// Verify the `signature` of the `message` signed by the private key corresponding to the public key stored + /// in the certificate. + pub fn verify_signature( + &self, + signature_scheme: rustls::SignatureScheme, + message: &[u8], + signature: &[u8], + ) -> Result<(), VerificationError> { + let pk = self.public_key(signature_scheme)?; + pk.verify(message, signature) + .map_err(|_| webpki::Error::InvalidSignatureForPublicKey)?; + + Ok(()) + } + + /// Get a [`ring::signature::UnparsedPublicKey`] for this `signature_scheme`. + /// Return `Error` if the `signature_scheme` does not match the public key signature + /// and hashing algorithm or if the `signature_scheme` is not supported. + fn public_key( + &self, + signature_scheme: rustls::SignatureScheme, + ) -> Result, webpki::Error> { + use ring::signature; + use rustls::SignatureScheme::*; + + let current_signature_scheme = self.signature_scheme()?; + if signature_scheme != current_signature_scheme { + // This certificate was signed with a different signature scheme + return Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKey); + } + + let verification_algorithm: &dyn signature::VerificationAlgorithm = match signature_scheme { + RSA_PKCS1_SHA256 => &signature::RSA_PKCS1_2048_8192_SHA256, + RSA_PKCS1_SHA384 => &signature::RSA_PKCS1_2048_8192_SHA384, + RSA_PKCS1_SHA512 => &signature::RSA_PKCS1_2048_8192_SHA512, + ECDSA_NISTP256_SHA256 => &signature::ECDSA_P256_SHA256_ASN1, + ECDSA_NISTP384_SHA384 => &signature::ECDSA_P384_SHA384_ASN1, + ECDSA_NISTP521_SHA512 => { + // See https://github.com/briansmith/ring/issues/824 + return Err(webpki::Error::UnsupportedSignatureAlgorithm); + } + RSA_PSS_SHA256 => &signature::RSA_PSS_2048_8192_SHA256, + RSA_PSS_SHA384 => &signature::RSA_PSS_2048_8192_SHA384, + RSA_PSS_SHA512 => &signature::RSA_PSS_2048_8192_SHA512, + ED25519 => &signature::ED25519, + ED448 => { + // See https://github.com/briansmith/ring/issues/463 + return Err(webpki::Error::UnsupportedSignatureAlgorithm); + } + // Similarly, hash functions with an output length less than 256 bits + // MUST NOT be used, due to the possibility of collision attacks. + // In particular, MD5 and SHA1 MUST NOT be used. + RSA_PKCS1_SHA1 => return Err(webpki::Error::UnsupportedSignatureAlgorithm), + ECDSA_SHA1_Legacy => return Err(webpki::Error::UnsupportedSignatureAlgorithm), + Unknown(_) => return Err(webpki::Error::UnsupportedSignatureAlgorithm), + }; + let spki = &self.certificate.tbs_certificate.subject_pki; + let key = signature::UnparsedPublicKey::new( + verification_algorithm, + spki.subject_public_key.as_ref(), + ); + + Ok(key) + } + + /// This method validates the certificate according to libp2p TLS 1.3 specs. + /// The certificate MUST: + /// 1. be valid at the time it is received by the peer; + /// 2. use the NamedCurve encoding; + /// 3. use hash functions with an output length not less than 256 bits; + /// 4. be self signed; + /// 5. contain a valid signature in the specific libp2p extension. + fn verify(&self) -> Result<(), webpki::Error> { + use webpki::Error; + // The certificate MUST have NotBefore and NotAfter fields set + // such that the certificate is valid at the time it is received by the peer. + if !self.certificate.validity().is_valid() { + return Err(Error::InvalidCertValidity); + } + + // Certificates MUST use the NamedCurve encoding for elliptic curve parameters. + // Similarly, hash functions with an output length less than 256 bits + // MUST NOT be used, due to the possibility of collision attacks. + // In particular, MD5 and SHA1 MUST NOT be used. + // Endpoints MUST abort the connection attempt if it is not used. + let signature_scheme = self.signature_scheme()?; + // Endpoints MUST abort the connection attempt if the certificate’s + // self-signature is not valid. + let raw_certificate = self.certificate.tbs_certificate.as_ref(); + let signature = self.certificate.signature_value.as_ref(); + // check if self signed + self.verify_signature(signature_scheme, raw_certificate, signature) + .map_err(|_| Error::SignatureAlgorithmMismatch)?; + + let subject_pki = self.certificate.public_key().raw; + + // The peer signs the concatenation of the string `libp2p-tls-handshake:` + // and the public key that it used to generate the certificate carrying + // the libp2p Public Key Extension, using its private host key. + let mut msg = vec![]; + msg.extend(P2P_SIGNING_PREFIX); + msg.extend(subject_pki); + + // This signature provides cryptographic proof that the peer was in possession + // of the private host key at the time the certificate was signed. + // Peers MUST verify the signature, and abort the connection attempt + // if signature verification fails. + let user_owns_sk = self + .extension + .public_key + .verify(&msg, &self.extension.signature); + if !user_owns_sk { + return Err(Error::UnknownIssuer); + } + + Ok(()) + } + + /// Return the signature scheme corresponding to [`AlgorithmIdentifier`]s + /// of `subject_pki` and `signature_algorithm` + /// according to . + fn signature_scheme(&self) -> Result { + // Certificates MUST use the NamedCurve encoding for elliptic curve parameters. + // Endpoints MUST abort the connection attempt if it is not used. + use oid_registry::*; + use rustls::SignatureScheme::*; + + let signature_algorithm = &self.certificate.signature_algorithm; + let pki_algorithm = &self.certificate.tbs_certificate.subject_pki.algorithm; + + if pki_algorithm.algorithm == OID_PKCS1_RSAENCRYPTION { + if signature_algorithm.algorithm == OID_PKCS1_SHA256WITHRSA { + return Ok(RSA_PKCS1_SHA256); + } + if signature_algorithm.algorithm == OID_PKCS1_SHA384WITHRSA { + return Ok(RSA_PKCS1_SHA384); + } + if signature_algorithm.algorithm == OID_PKCS1_SHA512WITHRSA { + return Ok(RSA_PKCS1_SHA512); + } + if signature_algorithm.algorithm == OID_PKCS1_RSASSAPSS { + // According to https://datatracker.ietf.org/doc/html/rfc4055#section-3.1: + // Inside of params there shuld be a sequence of: + // - Hash Algorithm + // - Mask Algorithm + // - Salt Length + // - Trailer Field + + // We are interested in Hash Algorithm only + + if let Ok(SignatureAlgorithm::RSASSA_PSS(params)) = + SignatureAlgorithm::try_from(signature_algorithm) + { + let hash_oid = params.hash_algorithm_oid(); + if hash_oid == &OID_NIST_HASH_SHA256 { + return Ok(RSA_PSS_SHA256); + } + if hash_oid == &OID_NIST_HASH_SHA384 { + return Ok(RSA_PSS_SHA384); + } + if hash_oid == &OID_NIST_HASH_SHA512 { + return Ok(RSA_PSS_SHA512); + } + } + + // Default hash algo is SHA-1, however: + // In particular, MD5 and SHA1 MUST NOT be used. + return Err(webpki::Error::UnsupportedSignatureAlgorithm); + } + } + + if pki_algorithm.algorithm == OID_KEY_TYPE_EC_PUBLIC_KEY { + let signature_param = pki_algorithm + .parameters + .as_ref() + .ok_or(webpki::Error::BadDer)? + .as_oid() + .map_err(|_| webpki::Error::BadDer)?; + if signature_param == OID_EC_P256 + && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA256 + { + return Ok(ECDSA_NISTP256_SHA256); + } + if signature_param == OID_NIST_EC_P384 + && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA384 + { + return Ok(ECDSA_NISTP384_SHA384); + } + if signature_param == OID_NIST_EC_P521 + && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA512 + { + return Ok(ECDSA_NISTP521_SHA512); + } + return Err(webpki::Error::UnsupportedSignatureAlgorithm); + } + + if signature_algorithm.algorithm == OID_SIG_ED25519 { + return Ok(ED25519); + } + if signature_algorithm.algorithm == OID_SIG_ED448 { + return Ok(ED448); + } + + Err(webpki::Error::UnsupportedSignatureAlgorithm) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn sanity_check() { + let keypair = identity::Keypair::generate_ed25519(); + + let (cert, _) = generate(&keypair).unwrap(); + let parsed_cert = parse(&cert).unwrap(); + + assert!(parsed_cert.verify().is_ok()); + assert_eq!(keypair.public(), parsed_cert.extension.public_key); + } + + macro_rules! check_cert { + ($name:ident, $path:literal, $scheme:path) => { + #[test] + fn $name() { + let cert: &[u8] = include_bytes!($path); + + let cert = parse_unverified(cert).unwrap(); + assert!(cert.verify().is_err()); // Because p2p extension + // was not signed with the private key + // of the certificate. + assert_eq!(cert.signature_scheme(), Ok($scheme)); + } + }; + } + + check_cert! {ed448, "./test_assets/ed448.der", rustls::SignatureScheme::ED448} + check_cert! {ed25519, "./test_assets/ed25519.der", rustls::SignatureScheme::ED25519} + check_cert! {rsa_pkcs1_sha256, "./test_assets/rsa_pkcs1_sha256.der", rustls::SignatureScheme::RSA_PKCS1_SHA256} + check_cert! {rsa_pkcs1_sha384, "./test_assets/rsa_pkcs1_sha384.der", rustls::SignatureScheme::RSA_PKCS1_SHA384} + check_cert! {rsa_pkcs1_sha512, "./test_assets/rsa_pkcs1_sha512.der", rustls::SignatureScheme::RSA_PKCS1_SHA512} + check_cert! {nistp256_sha256, "./test_assets/nistp256_sha256.der", rustls::SignatureScheme::ECDSA_NISTP256_SHA256} + check_cert! {nistp384_sha384, "./test_assets/nistp384_sha384.der", rustls::SignatureScheme::ECDSA_NISTP384_SHA384} + check_cert! {nistp521_sha512, "./test_assets/nistp521_sha512.der", rustls::SignatureScheme::ECDSA_NISTP521_SHA512} + + #[test] + fn rsa_pss_sha384() { + let cert = rustls::Certificate(include_bytes!("./test_assets/rsa_pss_sha384.der").to_vec()); + + let cert = parse(&cert).unwrap(); + + assert_eq!( + cert.signature_scheme(), + Ok(rustls::SignatureScheme::RSA_PSS_SHA384) + ); + } + + #[test] + fn nistp384_sha256() { + let cert: &[u8] = include_bytes!("./test_assets/nistp384_sha256.der"); + + let cert = parse_unverified(cert).unwrap(); + + assert!(cert.signature_scheme().is_err()); + } + + #[test] + fn can_parse_certificate_with_ed25519_keypair() { + let certificate = rustls::Certificate(hex!("308201773082011ea003020102020900f5bd0debaa597f52300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200046bf9871220d71dcb3483ecdfcbfcc7c103f8509d0974b3c18ab1f1be1302d643103a08f7a7722c1b247ba3876fe2c59e26526f479d7718a85202ddbe47562358a37f307d307b060a2b0601040183a25a01010101ff046a30680424080112207fda21856709c5ae12fd6e8450623f15f11955d384212b89f56e7e136d2e17280440aaa6bffabe91b6f30c35e3aa4f94b1188fed96b0ffdd393f4c58c1c047854120e674ce64c788406d1c2c4b116581fd7411b309881c3c7f20b46e54c7e6fe7f0f300a06082a8648ce3d040302034700304402207d1a1dbd2bda235ff2ec87daf006f9b04ba076a5a5530180cd9c2e8f6399e09d0220458527178c7e77024601dbb1b256593e9b96d961b96349d1f560114f61a87595").to_vec()); + + let peer_id = parse(&certificate).unwrap().peer_id(); + + assert_eq!( + "12D3KooWJRSrypvnpHgc6ZAgyCni4KcSmbV7uGRaMw5LgMKT18fq" + .parse::() + .unwrap(), + peer_id + ); + } + + #[test] + fn fails_to_parse_bad_certificate_with_ed25519_keypair() { + let certificate = rustls::Certificate(hex!("308201773082011da003020102020830a73c5d896a1109300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004bbe62df9a7c1c46b7f1f21d556deec5382a36df146fb29c7f1240e60d7d5328570e3b71d99602b77a65c9b3655f62837f8d66b59f1763b8c9beba3be07778043a37f307d307b060a2b0601040183a25a01010101ff046a3068042408011220ec8094573afb9728088860864f7bcea2d4fd412fef09a8e2d24d482377c20db60440ecabae8354afa2f0af4b8d2ad871e865cb5a7c0c8d3dbdbf42de577f92461a0ebb0a28703e33581af7d2a4f2270fc37aec6261fcc95f8af08f3f4806581c730a300a06082a8648ce3d040302034800304502202dfb17a6fa0f94ee0e2e6a3b9fb6e986f311dee27392058016464bd130930a61022100ba4b937a11c8d3172b81e7cd04aedb79b978c4379c2b5b24d565dd5d67d3cb3c").to_vec()); + + let error = parse(&certificate).unwrap_err(); + + assert_eq!(format!("{}", error), "UnknownIssuer"); + } + + #[test] + fn can_parse_certificate_with_ecdsa_keypair() { + let certificate = rustls::Certificate(hex!("308201c030820166a003020102020900eaf419a6e3edb4a6300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200048dbf1116c7c608d6d5292bd826c3feb53483a89fce434bf64538a359c8e07538ff71f6766239be6a146dcc1a5f3bb934bcd4ae2ae1d4da28ac68b4a20593f06ba381c63081c33081c0060a2b0601040183a25a01010101ff0481ae3081ab045f0803125b3059301306072a8648ce3d020106082a8648ce3d0301070342000484b93fa456a74bd0153919f036db7bc63c802f055bc7023395d0203de718ee0fc7b570b767cdd858aca6c7c4113ff002e78bd2138ac1a3b26dde3519e06979ad04483046022100bc84014cea5a41feabdf4c161096564b9ccf4b62fbef4fe1cd382c84e11101780221009204f086a84cb8ed8a9ddd7868dc90c792ee434adf62c66f99a08a5eba11615b300a06082a8648ce3d0403020348003045022054b437be9a2edf591312d68ff24bf91367ad4143f76cf80b5658f232ade820da022100e23b48de9df9c25d4c83ddddf75d2676f0b9318ee2a6c88a736d85eab94a912f").to_vec()); + + let peer_id = parse(&certificate).unwrap().peer_id(); + + assert_eq!( + "QmZcrvr3r4S3QvwFdae3c2EWTfo792Y14UpzCZurhmiWeX" + .parse::() + .unwrap(), + peer_id + ); + } + + #[test] + fn can_parse_certificate_with_secp256k1_keypair() { + let certificate = rustls::Certificate(hex!("3082018230820128a003020102020900f3b305f55622cfdf300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d0301070342000458f7e9581748ff9bdd933b655cc0e5552a1248f840658cc221dec2186b5a2fe4641b86ab7590a3422cdbb1000cf97662f27e5910d7569f22feed8829c8b52e0fa38188308185308182060a2b0601040183a25a01010101ff0471306f042508021221026b053094d1112bce799dc8026040ae6d4eb574157929f1598172061f753d9b1b04463044022040712707e97794c478d93989aaa28ae1f71c03af524a8a4bd2d98424948a782302207b61b7f074b696a25fb9e0059141a811cccc4cc28042d9301b9b2a4015e87470300a06082a8648ce3d04030203480030450220143ae4d86fdc8675d2480bb6912eca5e39165df7f572d836aa2f2d6acfab13f8022100831d1979a98f0c4a6fb5069ca374de92f1a1205c962a6d90ad3d7554cb7d9df4").to_vec()); + + let peer_id = parse(&certificate).unwrap().peer_id(); + + assert_eq!( + "16Uiu2HAm2dSCBFxuge46aEt7U1oejtYuBUZXxASHqmcfVmk4gsbx" + .parse::() + .unwrap(), + peer_id + ); + } +} diff --git a/transports/tls/src/lib.rs b/transports/tls/src/lib.rs new file mode 100644 index 00000000000..6bf22eea3f3 --- /dev/null +++ b/transports/tls/src/lib.rs @@ -0,0 +1,80 @@ +// Copyright 2021 Parity Technologies (UK) Ltd. +// Copyright 2022 Protocol Labs. +// +// 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. + +//! TLS configuration based on libp2p TLS specs. +//! +//! See . + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod certificate; +mod upgrade; +mod verifier; + +use libp2p_core::{identity::Keypair, PeerId}; +use std::sync::Arc; + +pub use futures_rustls::TlsStream; +pub use upgrade::Config; +pub use upgrade::UpgradeError; + +const P2P_ALPN: [u8; 6] = *b"libp2p"; + +/// Create a TLS client configuration for libp2p. +pub fn make_client_config( + keypair: &Keypair, + remote_peer_id: Option, +) -> Result { + let (certificate, private_key) = certificate::generate(keypair)?; + + let mut crypto = rustls::ClientConfig::builder() + .with_cipher_suites(verifier::CIPHERSUITES) + .with_safe_default_kx_groups() + .with_protocol_versions(verifier::PROTOCOL_VERSIONS) + .expect("Cipher suites and kx groups are configured; qed") + .with_custom_certificate_verifier(Arc::new( + verifier::Libp2pCertificateVerifier::with_remote_peer_id(remote_peer_id), + )) + .with_single_cert(vec![certificate], private_key) + .expect("Client cert key DER is valid; qed"); + crypto.alpn_protocols = vec![P2P_ALPN.to_vec()]; + + Ok(crypto) +} + +/// Create a TLS server configuration for libp2p. +pub fn make_server_config( + keypair: &Keypair, +) -> Result { + let (certificate, private_key) = certificate::generate(keypair)?; + + let mut crypto = rustls::ServerConfig::builder() + .with_cipher_suites(verifier::CIPHERSUITES) + .with_safe_default_kx_groups() + .with_protocol_versions(verifier::PROTOCOL_VERSIONS) + .expect("Cipher suites and kx groups are configured; qed") + .with_client_cert_verifier(Arc::new(verifier::Libp2pCertificateVerifier::new())) + .with_single_cert(vec![certificate], private_key) + .expect("Server cert key DER is valid; qed"); + crypto.alpn_protocols = vec![P2P_ALPN.to_vec()]; + + Ok(crypto) +} diff --git a/transports/tls/src/test_assets/ed25519.der b/transports/tls/src/test_assets/ed25519.der new file mode 100644 index 00000000000..494a199561a Binary files /dev/null and b/transports/tls/src/test_assets/ed25519.der differ diff --git a/transports/tls/src/test_assets/ed448.der b/transports/tls/src/test_assets/ed448.der new file mode 100644 index 00000000000..c7412386847 Binary files /dev/null and b/transports/tls/src/test_assets/ed448.der differ diff --git a/transports/tls/src/test_assets/gen.sh b/transports/tls/src/test_assets/gen.sh new file mode 100644 index 00000000000..4b7718874dd --- /dev/null +++ b/transports/tls/src/test_assets/gen.sh @@ -0,0 +1,63 @@ +#ED25519 (works): +openssl genpkey -algorithm ed25519 -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out ed25519.der + +#ED448 (works): +openssl genpkey -algorithm ed448 -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out ed448.der + +#RSA_PKCS1_SHA256 (works): +openssl genpkey -algorithm rsa -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha256 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out rsa_pkcs1_sha256.der + +#RSA_PKCS1_SHA384 (works): +# reuse privateKey.key and req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha384 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out rsa_pkcs1_sha384.der + +#RSA_PKCS1_SHA512 (works): +# reuse privateKey.key and req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha512 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out rsa_pkcs1_sha512.der + +#RSA-PSS TODO +# openssl genpkey -algorithm rsa-pss -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:3 -out privateKey.key +# # -sigopt rsa_pss_saltlen:20 +# # -sigopt rsa_padding_mode:pss +# # -sigopt rsa_mgf1_md:sha256 +# openssl req -x509 -nodes -days 365 -subj="/" -key privateKey.key -sha256 -sigopt rsa_pss_saltlen:20 -sigopt rsa_padding_mode:pss -sigopt rsa_mgf1_md:sha256 -out certificate.crt + +#ECDSA_NISTP256_SHA256 (works): +openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha256 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out nistp256_sha256.der + +#ECDSA_NISTP384_SHA384 (works): +openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha384 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out nistp384_sha384.der + +#ECDSA_NISTP521_SHA512 (works): +openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-521 -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha512 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out nistp521_sha512.der + +#ECDSA_NISTP384_SHA256 (must fail): +openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out privateKey.key +openssl req -new -subj="/" -key privateKey.key -out req.pem +openssl x509 -req -in req.pem -signkey privateKey.key -sha256 -out certificate.crt -extensions p2p_ext -extfile ./openssl.cfg +openssl x509 -outform der -in certificate.crt -out nistp384_sha256.der + + +# Remove tmp files + +rm req.pem certificate.crt privateKey.key diff --git a/transports/tls/src/test_assets/nistp256_sha256.der b/transports/tls/src/test_assets/nistp256_sha256.der new file mode 100644 index 00000000000..8023645e9b0 Binary files /dev/null and b/transports/tls/src/test_assets/nistp256_sha256.der differ diff --git a/transports/tls/src/test_assets/nistp384_sha256.der b/transports/tls/src/test_assets/nistp384_sha256.der new file mode 100644 index 00000000000..5d76fa8f4a9 Binary files /dev/null and b/transports/tls/src/test_assets/nistp384_sha256.der differ diff --git a/transports/tls/src/test_assets/nistp384_sha384.der b/transports/tls/src/test_assets/nistp384_sha384.der new file mode 100644 index 00000000000..a81a5ce1ab7 Binary files /dev/null and b/transports/tls/src/test_assets/nistp384_sha384.der differ diff --git a/transports/tls/src/test_assets/nistp521_sha512.der b/transports/tls/src/test_assets/nistp521_sha512.der new file mode 100644 index 00000000000..2846361f278 Binary files /dev/null and b/transports/tls/src/test_assets/nistp521_sha512.der differ diff --git a/transports/tls/src/test_assets/openssl.cfg b/transports/tls/src/test_assets/openssl.cfg new file mode 100644 index 00000000000..62f02baee8b --- /dev/null +++ b/transports/tls/src/test_assets/openssl.cfg @@ -0,0 +1,6 @@ +[ p2p_ext ] +1.3.6.1.4.1.53594.1.1 = critical,ASN1:SEQUENCE:ExtBody + +[ ExtBody ] +pubkey = FORMAT:HEX,OCTETSTRING:08011220DF6491C415ED084B87E8F00CDB4A41C4035CFEA5F9D23D25FF9CA897E7FDDC0F +signature = FORMAT:HEX,OCTETSTRING:94A89E52CC24FD29B4B49DE615C37D268362E8D7C7C096FB7CD013DC9402572AF4886480FEC507C3C03DB07A2EC816B2B6714427DC28F379E0859C6F3B15BB05 diff --git a/transports/tls/src/test_assets/pkcs1_sha256.der b/transports/tls/src/test_assets/pkcs1_sha256.der new file mode 100644 index 00000000000..0449728ee28 Binary files /dev/null and b/transports/tls/src/test_assets/pkcs1_sha256.der differ diff --git a/transports/tls/src/test_assets/rsa_pkcs1_sha256.der b/transports/tls/src/test_assets/rsa_pkcs1_sha256.der new file mode 100644 index 00000000000..c9ed93d3913 Binary files /dev/null and b/transports/tls/src/test_assets/rsa_pkcs1_sha256.der differ diff --git a/transports/tls/src/test_assets/rsa_pkcs1_sha384.der b/transports/tls/src/test_assets/rsa_pkcs1_sha384.der new file mode 100644 index 00000000000..ac940fc473d Binary files /dev/null and b/transports/tls/src/test_assets/rsa_pkcs1_sha384.der differ diff --git a/transports/tls/src/test_assets/rsa_pkcs1_sha512.der b/transports/tls/src/test_assets/rsa_pkcs1_sha512.der new file mode 100644 index 00000000000..305cc65a88e Binary files /dev/null and b/transports/tls/src/test_assets/rsa_pkcs1_sha512.der differ diff --git a/transports/tls/src/test_assets/rsa_pss_sha384.der b/transports/tls/src/test_assets/rsa_pss_sha384.der new file mode 100644 index 00000000000..d0b8f39adcd Binary files /dev/null and b/transports/tls/src/test_assets/rsa_pss_sha384.der differ diff --git a/transports/tls/src/upgrade.rs b/transports/tls/src/upgrade.rs new file mode 100644 index 00000000000..4efb3471d7f --- /dev/null +++ b/transports/tls/src/upgrade.rs @@ -0,0 +1,130 @@ +// Copyright 2022 Protocol Labs. +// +// 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. + +use crate::certificate; +use crate::certificate::P2pCertificate; +use futures::future::BoxFuture; +use futures::AsyncWrite; +use futures::{AsyncRead, FutureExt}; +use futures_rustls::TlsStream; +use libp2p_core::{identity, InboundUpgrade, OutboundUpgrade, PeerId, UpgradeInfo}; +use rustls::{CommonState, ServerName}; +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::Arc; + +#[derive(thiserror::Error, Debug)] +pub enum UpgradeError { + #[error("Failed to generate certificate")] + CertificateGeneration(#[from] certificate::GenError), + #[error("Failed to upgrade server connection")] + ServerUpgrade(std::io::Error), + #[error("Failed to upgrade client connection")] + ClientUpgrade(std::io::Error), + #[error("Failed to parse certificate")] + BadCertificate(#[from] certificate::ParseError), +} + +#[derive(Clone)] +pub struct Config { + server: rustls::ServerConfig, + client: rustls::ClientConfig, +} + +impl Config { + pub fn new(identity: &identity::Keypair) -> Result { + Ok(Self { + server: crate::make_server_config(identity)?, + client: crate::make_client_config(identity, None)?, + }) + } +} + +impl UpgradeInfo for Config { + type Info = &'static [u8]; + type InfoIter = std::iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + std::iter::once(b"/tls/1.0.0") + } +} + +impl InboundUpgrade for Config +where + C: AsyncRead + AsyncWrite + Send + Unpin + 'static, +{ + type Output = (PeerId, TlsStream); + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_inbound(self, socket: C, _: Self::Info) -> Self::Future { + async move { + let stream = futures_rustls::TlsAcceptor::from(Arc::new(self.server)) + .accept(socket) + .await + .map_err(UpgradeError::ServerUpgrade)?; + + let peer_id = extract_single_certificate(stream.get_ref().1)?.peer_id(); + + Ok((peer_id, stream.into())) + } + .boxed() + } +} + +impl OutboundUpgrade for Config +where + C: AsyncRead + AsyncWrite + Send + Unpin + 'static, +{ + type Output = (PeerId, TlsStream); + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_outbound(self, socket: C, _: Self::Info) -> Self::Future { + async move { + // Spec: In order to keep this flexibility for future versions, clients that only support the version of the handshake defined in this document MUST NOT send any value in the Server Name Indication. + // Setting `ServerName` to unspecified will disable the use of the SNI extension. + let name = ServerName::IpAddress(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + + let stream = futures_rustls::TlsConnector::from(Arc::new(self.client)) + .connect(name, socket) + .await + .map_err(UpgradeError::ClientUpgrade)?; + + let peer_id = extract_single_certificate(stream.get_ref().1)?.peer_id(); + + Ok((peer_id, stream.into())) + } + .boxed() + } +} + +fn extract_single_certificate( + state: &CommonState, +) -> Result, certificate::ParseError> { + let cert = match state + .peer_certificates() + .expect("config enforces presence of certificates") + { + [single] => single, + _ => panic!("config enforces exactly one certificate"), + }; + + certificate::parse(cert) +} diff --git a/transports/tls/src/verifier.rs b/transports/tls/src/verifier.rs new file mode 100644 index 00000000000..05d38068414 --- /dev/null +++ b/transports/tls/src/verifier.rs @@ -0,0 +1,256 @@ +// Copyright 2021 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. + +//! TLS 1.3 certificates and handshakes handling for libp2p +//! +//! This module handles a verification of a client/server certificate chain +//! and signatures allegedly by the given certificates. + +use crate::certificate; +use libp2p_core::PeerId; +use rustls::{ + cipher_suite::{ + TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256, + }, + client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + internal::msgs::handshake::DigitallySignedStruct, + server::{ClientCertVerified, ClientCertVerifier}, + Certificate, DistinguishedNames, SignatureScheme, SupportedCipherSuite, + SupportedProtocolVersion, +}; + +/// The protocol versions supported by this verifier. +/// +/// The spec says: +/// +/// > The libp2p handshake uses TLS 1.3 (and higher). +/// > Endpoints MUST NOT negotiate lower TLS versions. +pub static PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&rustls::version::TLS13]; + +/// A list of the TLS 1.3 cipher suites supported by rustls. +// By default rustls creates client/server configs with both +// TLS 1.3 __and__ 1.2 cipher suites. But we don't need 1.2. +pub static CIPHERSUITES: &[SupportedCipherSuite] = &[ + // TLS1.3 suites + TLS13_CHACHA20_POLY1305_SHA256, + TLS13_AES_256_GCM_SHA384, + TLS13_AES_128_GCM_SHA256, +]; + +/// Implementation of the `rustls` certificate verification traits for libp2p. +/// +/// Only TLS 1.3 is supported. TLS 1.2 should be disabled in the configuration of `rustls`. +pub struct Libp2pCertificateVerifier { + /// The peer ID we intend to connect to + remote_peer_id: Option, +} + +/// libp2p requires the following of X.509 server certificate chains: +/// +/// - Exactly one certificate must be presented. +/// - The certificate must be self-signed. +/// - The certificate must have a valid libp2p extension that includes a +/// signature of its public key. +impl Libp2pCertificateVerifier { + pub fn new() -> Self { + Self { + remote_peer_id: None, + } + } + pub fn with_remote_peer_id(remote_peer_id: Option) -> Self { + Self { remote_peer_id } + } + + /// Return the list of SignatureSchemes that this verifier will handle, + /// in `verify_tls12_signature` and `verify_tls13_signature` calls. + /// + /// This should be in priority order, with the most preferred first. + fn verification_schemes() -> Vec { + vec![ + // TODO SignatureScheme::ECDSA_NISTP521_SHA512 is not supported by `ring` yet + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP256_SHA256, + // TODO SignatureScheme::ED448 is not supported by `ring` yet + SignatureScheme::ED25519, + // In particular, RSA SHOULD NOT be used unless + // no elliptic curve algorithms are supported. + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA256, + ] + } +} + +impl ServerCertVerifier for Libp2pCertificateVerifier { + fn verify_server_cert( + &self, + end_entity: &Certificate, + intermediates: &[Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> Result { + let peer_id = verify_presented_certs(end_entity, intermediates)?; + + if let Some(remote_peer_id) = self.remote_peer_id { + // The public host key allows the peer to calculate the peer ID of the peer + // it is connecting to. Clients MUST verify that the peer ID derived from + // the certificate matches the peer ID they intended to connect to, + // and MUST abort the connection if there is a mismatch. + if remote_peer_id != peer_id { + return Err(rustls::Error::PeerMisbehavedError( + "Wrong peer ID in p2p extension".to_string(), + )); + } + } + + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &Certificate, + _dss: &DigitallySignedStruct, + ) -> Result { + unreachable!("`PROTOCOL_VERSIONS` only allows TLS 1.3") + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &Certificate, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature(cert, dss.scheme, message, dss.signature()) + } + + fn supported_verify_schemes(&self) -> Vec { + Self::verification_schemes() + } +} + +/// libp2p requires the following of X.509 client certificate chains: +/// +/// - Exactly one certificate must be presented. In particular, client +/// authentication is mandatory in libp2p. +/// - The certificate must be self-signed. +/// - The certificate must have a valid libp2p extension that includes a +/// signature of its public key. +impl ClientCertVerifier for Libp2pCertificateVerifier { + fn offer_client_auth(&self) -> bool { + true + } + + fn client_auth_root_subjects(&self) -> Option { + Some(vec![]) + } + + fn verify_client_cert( + &self, + end_entity: &Certificate, + intermediates: &[Certificate], + _now: std::time::SystemTime, + ) -> Result { + verify_presented_certs(end_entity, intermediates)?; + + Ok(ClientCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &Certificate, + _dss: &DigitallySignedStruct, + ) -> Result { + unreachable!("`PROTOCOL_VERSIONS` only allows TLS 1.3") + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &Certificate, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature(cert, dss.scheme, message, dss.signature()) + } + + fn supported_verify_schemes(&self) -> Vec { + Self::verification_schemes() + } +} + +/// When receiving the certificate chain, an endpoint +/// MUST check these conditions and abort the connection attempt if +/// (a) the presented certificate is not yet valid, OR +/// (b) if it is expired. +/// Endpoints MUST abort the connection attempt if more than one certificate is received, +/// or if the certificate’s self-signature is not valid. +fn verify_presented_certs( + end_entity: &Certificate, + intermediates: &[Certificate], +) -> Result { + if !intermediates.is_empty() { + return Err(rustls::Error::General( + "libp2p-tls requires exactly one certificate".into(), + )); + } + + let cert = certificate::parse(end_entity)?; + + Ok(cert.peer_id()) +} + +fn verify_tls13_signature( + cert: &Certificate, + signature_scheme: SignatureScheme, + message: &[u8], + signature: &[u8], +) -> Result { + certificate::parse(cert)?.verify_signature(signature_scheme, message, signature)?; + + Ok(HandshakeSignatureValid::assertion()) +} + +impl From for rustls::Error { + fn from(certificate::ParseError(e): certificate::ParseError) -> Self { + use webpki::Error::*; + match e { + BadDer => rustls::Error::InvalidCertificateEncoding, + e => rustls::Error::InvalidCertificateData(format!("invalid peer certificate: {}", e)), + } + } +} +impl From for rustls::Error { + fn from(certificate::VerificationError(e): certificate::VerificationError) -> Self { + use webpki::Error::*; + match e { + InvalidSignatureForPublicKey => rustls::Error::InvalidCertificateSignature, + UnsupportedSignatureAlgorithm | UnsupportedSignatureAlgorithmForPublicKey => { + rustls::Error::InvalidCertificateSignatureType + } + e => rustls::Error::InvalidCertificateData(format!("invalid peer certificate: {}", e)), + } + } +} diff --git a/transports/tls/tests/smoke.rs b/transports/tls/tests/smoke.rs new file mode 100644 index 00000000000..1def8717e01 --- /dev/null +++ b/transports/tls/tests/smoke.rs @@ -0,0 +1,73 @@ +use futures::{future, StreamExt}; +use libp2p::multiaddr::Protocol; +use libp2p::swarm::{keep_alive, SwarmEvent}; +use libp2p::Swarm; +use libp2p_core::transport::MemoryTransport; +use libp2p_core::upgrade::Version; +use libp2p_core::Transport; + +#[tokio::test] +async fn can_establish_connection() { + let mut swarm1 = make_swarm(); + let mut swarm2 = make_swarm(); + + let listen_address = { + let expected_listener_id = swarm1.listen_on(Protocol::Memory(0).into()).unwrap(); + + loop { + match swarm1.next().await.unwrap() { + SwarmEvent::NewListenAddr { + address, + listener_id, + } if listener_id == expected_listener_id => break address, + _ => continue, + }; + } + }; + swarm2.dial(listen_address).unwrap(); + + let await_inbound_connection = async { + loop { + match swarm1.next().await.unwrap() { + SwarmEvent::ConnectionEstablished { peer_id, .. } => break peer_id, + SwarmEvent::IncomingConnectionError { error, .. } => { + panic!("Incoming connection failed: {}", error) + } + _ => continue, + }; + } + }; + let await_outbound_connection = async { + loop { + match swarm2.next().await.unwrap() { + SwarmEvent::ConnectionEstablished { peer_id, .. } => break peer_id, + SwarmEvent::OutgoingConnectionError { error, .. } => { + panic!("Failed to dial: {}", error) + } + _ => continue, + }; + } + }; + + let (inbound_peer_id, outbound_peer_id) = + future::join(await_inbound_connection, await_outbound_connection).await; + + assert_eq!(&inbound_peer_id, swarm2.local_peer_id()); + assert_eq!(&outbound_peer_id, swarm1.local_peer_id()); +} + +fn make_swarm() -> Swarm { + let identity = libp2p::identity::Keypair::generate_ed25519(); + + let transport = MemoryTransport::default() + .upgrade(Version::V1) + .authenticate(libp2p_tls::Config::new(&identity).unwrap()) + .multiplex(libp2p::yamux::YamuxConfig::default()) + .boxed(); + + Swarm::new( + transport, + keep_alive::Behaviour, + identity.public().to_peer_id(), + ) +}