diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b4ddc48..2a6c85601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.11.18 +- chore: Use simple_x509 for certificate generation. [PR 219](https://github.com/dariusc93/rust-ipfs/pull/219) + # 0.11.17 - fix: Remove aws-lc-rs feature. diff --git a/Cargo.lock b/Cargo.lock index 35ff60b65..5a7066939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4780,6 +4780,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "sha2 0.10.8", + "simple_x509", "sled", "tempfile", "thiserror", @@ -5196,6 +5197,29 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb4ea60fb301dc81dfc113df680571045d375ab7345d171c5dc7d7e13107a80" +dependencies = [ + "chrono", + "num-bigint", + "num-traits", + "thiserror", +] + +[[package]] +name = "simple_x509" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417fc25f99e6af54350cbc26997572b6ee04c4b8deec627ce5f16f76a7ed887b" +dependencies = [ + "chrono", + "num-traits", + "simple_asn1", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 00c13e3e6..65c71727a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ serde = { default-features = false, version = "1"} serde_json = { default-features = false, version = "1" } serde-wasm-bindgen = "0.6" sha2 = "0.10.8" +simple_x509 = "=1.1.0" sled = { version = "0.34" } thiserror = { default-features = false, version = "1"} tracing = { default-features = false, features = ["log"], version = "0.1"} @@ -143,6 +144,7 @@ libp2p-webrtc = { workspace = true, features = ["tokio",], optional = true } rcgen.workspace = true redb = { workspace = true, optional = true } rlimit.workspace = true +simple_x509.workspace = true sled = { workspace = true, optional = true } tokio = { features = ["full"], workspace = true } tokio-stream = { workspace = true, features = ["fs"] } diff --git a/src/p2p/transport.rs b/src/p2p/transport.rs index 3c7b7a483..18e04ecfd 100644 --- a/src/p2p/transport.rs +++ b/src/p2p/transport.rs @@ -141,8 +141,8 @@ pub(crate) fn build_transport( use libp2p::quic::tokio::Transport as TokioQuicTransport; use libp2p::quic::Config as QuicConfig; use libp2p::tcp::{tokio::Transport as TokioTcpTransport, Config as GenTcpConfig}; - use rcgen::KeyPair; use misc::generate_cert; + use rcgen::KeyPair; let noise_config = noise::Config::new(&keypair).map_err(io::Error::other)?; @@ -173,11 +173,7 @@ pub(crate) fn build_transport( (cert, priv_key) } None => { - let (cert, prv, _) = generate_cert( - &keypair, - b"libp2p-websocket", - false, - )?; + let (cert, prv, _) = generate_cert(&keypair, b"libp2p-websocket", false)?; let priv_key = libp2p::websocket::tls::PrivateKey::new(prv.serialize_der()); let self_cert = @@ -229,18 +225,7 @@ pub(crate) fn build_transport( None => { // This flag is internal, but is meant to allow generating an expired pem to satify webrtc let expired = true; - let (cert, prv, expired_pem) = - generate_cert(&keypair, b"libp2p-webrtc", expired)?; - // dtls requires pem with a dash in the label? - let priv_key = prv.serialize_pem().replace("PRIVATE KEY", "PRIVATE_KEY"); - let cert = cert.pem(); - - let pem = priv_key + "\n\n" + &cert; - - let pem = match expired_pem { - Some(epem) => epem + "\n\n" + &pem, - None => pem, - }; + let pem = misc::generate_wrtc_cert(&keypair)?; libp2p_webrtc::tokio::Certificate::from_pem(&pem) .map_err(std::io::Error::other)? diff --git a/src/p2p/transport/misc.rs b/src/p2p/transport/misc.rs index 7779bf37d..fd80d1603 100644 --- a/src/p2p/transport/misc.rs +++ b/src/p2p/transport/misc.rs @@ -1,5 +1,6 @@ use hkdf::Hkdf; use libp2p::identity::{self as identity, Keypair}; +use p256::ecdsa::signature::Signer; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use rcgen::{Certificate, CertificateParams, DnType, KeyPair}; @@ -8,8 +9,24 @@ use sha2::Sha256; use std::io; use web_time::{Duration, SystemTime}; +/// 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]; + const ENCODE_CONFIG: pem::EncodeConfig = { let line_ending = match cfg!(target_family = "windows") { true => pem::LineEnding::CRLF, @@ -20,7 +37,7 @@ const ENCODE_CONFIG: pem::EncodeConfig = { /// Generates a TLS certificate that derives from libp2p `Keypair` with a salt. /// Note: If `expire` is true, it will produce a expired pem that can be appended for webrtc transport -/// Additionally, this function does not generate deterministic certs *yet* due to +/// Additionally, this function does not generate deterministic certs *yet* due to /// `CertificateParams::self_signed` using ring rng. This may change in the future pub fn generate_cert( keypair: &Keypair, @@ -56,8 +73,79 @@ pub fn generate_cert( Ok((cert, internal_keypair, expired_pem)) } +/// Used to generate webrtc certificates. +/// Note: Although simple_x509 does not deal with crypto directly (eg signing certificate) +/// we would still have to be careful of any changes upstream that may cause a change in the certificate +#[allow(dead_code)] +pub(crate) fn generate_wrtc_cert(keypair: &Keypair) -> io::Result { + let (secret, public_key) = derive_keypair_secret(keypair, b"libp2p-webrtc")?; + let peer_id = keypair.public().to_peer_id(); + + let certificate = simple_x509::X509::builder() + .issuer_utf8(Vec::from(ORGANISATION_NAME_OID), "rust-ipfs") + .subject_utf8(Vec::from(ORGANISATION_NAME_OID), &peer_id.to_string()) + .not_before_gen(UNIX_2000) + .not_after_gen(UNIX_3000) + .pub_key_ec( + Vec::from(EC_OID), + public_key.to_encoded_point(false).as_bytes().to_owned(), + Vec::from(P256_OID), + ) + .sign_oid(Vec::from(ECDSA_SHA256_OID)) + .build() + .sign( + |cert, _| { + let signature: p256::ecdsa::DerSignature = secret.sign(cert); + Some(signature.as_bytes().to_owned()) + }, + &[], // We close over the keypair so no need to pass it. + ) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{e:?}")))?; + + let der_bytes = certificate.x509_enc().unwrap(); + + let cert_pem = pem::encode_config( + &pem::Pem::new("CERTIFICATE".to_string(), der_bytes), + ENCODE_CONFIG, + ); + + let private_pem = secret + .to_pkcs8_pem(Default::default()) + .map_err(std::io::Error::other)? + .replace("PRIVATE KEY", "PRIVATE_KEY"); + + let expired_pem = { + let expired = SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs(UNIX_3000 as u64)) + .expect("year 3000 to be representable by SystemTime") + .to_der() + .unwrap(); + + pem::encode_config( + &pem::Pem::new("EXPIRES".to_string(), expired), + ENCODE_CONFIG, + ) + }; + + let pem = expired_pem + "\n\n" + &private_pem + "\n\n" + &cert_pem; + + Ok(pem) +} + fn derive_keypair(keypair: &Keypair, salt: &[u8]) -> io::Result { - //Note: We could use `Keypair::derive_secret`, but this seems more sensible? + let (secret, _) = derive_keypair_secret(keypair, salt)?; + + let pem = secret + .to_pkcs8_pem(Default::default()) + .map_err(std::io::Error::other)?; + + KeyPair::from_pem(&pem).map_err(std::io::Error::other) +} + +fn derive_keypair_secret( + keypair: &Keypair, + salt: &[u8], +) -> io::Result<(p256::ecdsa::SigningKey, p256::ecdsa::VerifyingKey)> { let secret = keypair_secret(keypair).ok_or(io::Error::from(io::ErrorKind::Unsupported))?; let hkdf_gen = Hkdf::::from_prk(secret.as_ref()).expect("key length to be valid"); @@ -69,12 +157,9 @@ fn derive_keypair(keypair: &Keypair, salt: &[u8]) -> io::Result { let mut rng = ChaCha20Rng::from_seed(seed); let secret = p256::ecdsa::SigningKey::random(&mut rng); + let public = p256::ecdsa::VerifyingKey::from(&secret); - let pem = secret - .to_pkcs8_pem(Default::default()) - .map_err(std::io::Error::other)?; - - KeyPair::from_pem(&pem).map_err(std::io::Error::other) + Ok((secret, public)) } fn keypair_secret(keypair: &Keypair) -> Option<[u8; 32]> { @@ -102,3 +187,48 @@ fn keypair_secret(keypair: &Keypair) -> Option<[u8; 32]> { } } } + +#[cfg(test)] +mod test { + use libp2p::identity::Keypair; + + use crate::p2p::transport::misc::generate_wrtc_cert; + + const PEM: &str = r#"-----BEGIN EXPIRES----- +GA8yOTk5MTIzMTEzMDAwMFo= +-----END EXPIRES----- + + +-----BEGIN PRIVATE_KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXARqgq74dVCrVR6G +VT/iHnwBmx9s217QqvegG1xKNpqhRANCAAQvm08WYqoMCCEF36I5OAhA/XS7SqhR +7n2CahGwC/fEqtvRrwAfZGejF21lzOW/m+A3EbDIzjy+xpUY+zaCE57V +-----END PRIVATE_KEY----- + + +-----BEGIN CERTIFICATE----- +MIIBPjCB5QIBADAKBggqhkjOPQQDAjAUMRIwEAYDVQQKDAlydXN0LWlwZnMwIhgP +MTk5OTEyMzExMzAwMDBaGA8yOTk5MTIzMTEzMDAwMFowPzE9MDsGA1UECgw0MTJE +M0tvb1dQamNlUXJTd2RXWFB5TExlQUJSWG11cXQ2OVJnM3NCWWJVMU5mdDlIeVE2 +WDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABC+bTxZiqgwIIQXfojk4CED9dLtK +qFHufYJqEbAL98Sq29GvAB9kZ6MXbWXM5b+b4DcRsMjOPL7GlRj7NoITntUwCgYI +KoZIzj0EAwIDSAAwRQIhAP+F5COvtCQbZiyBQpAoiIoQP12KwIsNe1zhumki4bkU +AiAH43Q833G8p1eXxqJr2xRrA1B5vCZ1qgl/44Z++NDMqQ== +-----END CERTIFICATE----- +"#; + + #[test] + fn generate_cert() { + let keypair = generate_ed25519(); + let pem = generate_wrtc_cert(&keypair).expect("not to fail"); + assert_eq!(pem, PEM) + } + + fn generate_ed25519() -> Keypair { + let mut bytes = [0u8; 32]; + bytes[0] = 1; + + Keypair::ed25519_from_bytes(bytes).expect("only errors on wrong length") + } + +}