Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Use simple_x509 to generate webrtc self-signed cert #219

Merged
merged 6 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -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"] }
Expand Down
21 changes: 3 additions & 18 deletions src/p2p/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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)?
Expand Down
144 changes: 137 additions & 7 deletions src/p2p/transport/misc.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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 <http://oid-info.com/get/2.5.4.10>.
const ORGANISATION_NAME_OID: [u64; 4] = [2, 5, 4, 10];

/// OID for Elliptic Curve Public Key Cryptography. See <http://oid-info.com/get/1.2.840.10045.2.1>.
const EC_OID: [u64; 6] = [1, 2, 840, 10045, 2, 1];

/// OID for 256-bit Elliptic Curve Cryptography (ECC) with the P256 curve. See <http://oid-info.com/get/1.2.840.10045.3.1.7>.
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 <http://oid-info.com/get/1.2.840.10045.4.3.2>.
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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<String> {
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<KeyPair> {
//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::<Sha256>::from_prk(secret.as_ref()).expect("key length to be valid");

Expand All @@ -69,12 +157,9 @@ fn derive_keypair(keypair: &Keypair, salt: &[u8]) -> io::Result<KeyPair> {
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]> {
Expand Down Expand Up @@ -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")
}

}