From 3ef8dc4100458bf788351a655e4cfc738d122c76 Mon Sep 17 00:00:00 2001 From: yngrtc Date: Sat, 6 Jan 2024 14:41:29 -0800 Subject: [PATCH] add certficate --- Cargo.toml | 5 +- examples/chat.rs | 1 - src/server/certificate.rs | 224 ++++++++++++++++++++++++++++++++ src/server/config.rs | 6 + src/server/mod.rs | 3 + src/server/room/endpoint/mod.rs | 2 +- src/server/room/mod.rs | 1 + src/server/states.rs | 23 ++++ src/shared/types.rs | 18 +++ 9 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 src/server/certificate.rs create mode 100644 src/server/config.rs create mode 100644 src/server/states.rs diff --git a/Cargo.toml b/Cargo.toml index 4daf1e2..7d7168c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,15 @@ bytes = "1.5.0" log = "0.4.20" rand = "0.8.5" async-broadcast = "0.6.0" -rcgen = { version = "0.12.0", features = ["pem", "x509-parser"] } +rcgen = { version = "0.11", features = ["pem", "x509-parser"] } ctrlc = "3.4.2" thiserror = "1.0.53" core_affinity = "0.8.1" num_cpus = "1.16.0" smol = "2.0.0" +ring = "0.17.7" +sha2 = "0.10.8" +rustls = "0.21.7" # RTC protocol shared = { path = "rtc/shared"} diff --git a/examples/chat.rs b/examples/chat.rs index 6105c4a..3231121 100644 --- a/examples/chat.rs +++ b/examples/chat.rs @@ -81,7 +81,6 @@ fn main() -> anyhow::Result<()> { //let key_pair = rcgen::KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?; // let certificate = RTCCertificate::from_key_pair(key_pair)?; - // fingerprints = certificate.get_fingerprints(); /*let (cancel_tx, signal_cancel_rx) = broadcast::channel(1); let rtc_cancel_rx = cancel_tx.subscribe(); diff --git a/src/server/certificate.rs b/src/server/certificate.rs new file mode 100644 index 0000000..0784053 --- /dev/null +++ b/src/server/certificate.rs @@ -0,0 +1,224 @@ +use dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind}; +use rand::thread_rng; +use rand::Rng; +use rcgen::{CertificateParams, KeyPair}; +use ring::rand::SystemRandom; +use ring::rsa; +use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; +use sha2::{Digest, Sha256}; +use shared::error::{Error, Result}; +use std::ops::Add; +use std::time::{Duration, SystemTime}; + +/// DTLSFingerprint specifies the hash function algorithm and certificate +/// fingerprint as described in . +#[derive(Default, Debug, Clone)] +pub struct RTCDtlsFingerprint { + /// Algorithm specifies one of the the hash function algorithms defined in + /// the 'Hash function Textual Names' registry. + pub algorithm: String, + + /// Value specifies the value of the certificate fingerprint in lowercase + /// hex string as expressed utilizing the syntax of 'fingerprint' in + /// . + pub value: String, +} + +/// Certificate represents a X.509 certificate used to authenticate WebRTC communications. +#[derive(Clone, Debug)] +pub struct RTCCertificate { + /// DTLS certificate. + pub(crate) dtls_certificate: dtls::crypto::Certificate, + /// Timestamp after which this certificate is no longer valid. + pub(crate) expires: SystemTime, +} + +impl PartialEq for RTCCertificate { + fn eq(&self, other: &Self) -> bool { + self.dtls_certificate == other.dtls_certificate + } +} + +impl RTCCertificate { + /// Generates a new certificate from the given parameters. + /// + /// See [`rcgen::Certificate::from_params`]. + pub fn from_params(params: CertificateParams) -> Result { + let not_after = params.not_after; + let x509_cert = rcgen::Certificate::from_params(params)?; + + let key_pair = x509_cert.get_key_pair(); + let serialized_der = key_pair.serialize_der(); + + let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { + CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ed25519( + Ed25519KeyPair::from_pkcs8(&serialized_der) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der, + } + } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { + CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ecdsa256( + EcdsaKeyPair::from_pkcs8( + &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &serialized_der, + &SystemRandom::new(), + ) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der, + } + } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { + CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Rsa256( + rsa::KeyPair::from_pkcs8(&serialized_der) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der, + } + } else { + return Err(Error::Other("Unsupported key_pair".to_owned())); + }; + + let expires = if cfg!(target_arch = "arm") { + // Workaround for issue overflow when adding duration to instant on armv7 + // https://github.com/webrtc-rs/examples/issues/5 https://github.com/chronotope/chrono/issues/343 + SystemTime::now().add(Duration::from_secs(172800)) //60*60*48 or 2 days + } else { + not_after.into() + }; + + Ok(Self { + dtls_certificate: dtls::crypto::Certificate { + certificate: vec![rustls::Certificate(x509_cert.serialize_der()?)], + private_key, + }, + expires, + }) + } + + /// Generates a new certificate with default [`CertificateParams`] using the given keypair. + pub fn from_key_pair(key_pair: KeyPair) -> Result { + let mut params = CertificateParams::new(vec![math_rand_alpha(16)]); + + if key_pair.is_compatible(&rcgen::PKCS_ED25519) { + params.alg = &rcgen::PKCS_ED25519; + } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { + params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; + } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { + params.alg = &rcgen::PKCS_RSA_SHA256; + } else { + return Err(Error::Other("Unsupported key_pair".to_owned())); + }; + params.key_pair = Some(key_pair); + + RTCCertificate::from_params(params) + } + + /// Parses a certificate from the ASCII PEM format. + #[cfg(feature = "pem")] + pub fn from_pem(pem_str: &str) -> Result { + let mut pem_blocks = pem_str.split("\n\n"); + let first_block = if let Some(b) = pem_blocks.next() { + b + } else { + return Err(Error::InvalidPEM("empty PEM".into())); + }; + let expires_pem = + pem::parse(first_block).map_err(|e| Error::new(format!("can't parse PEM: {e}")))?; + if expires_pem.tag() != "EXPIRES" { + return Err(Error::InvalidPEM(format!( + "invalid tag (expected: 'EXPIRES', got '{}')", + expires_pem.tag() + ))); + } + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&expires_pem.contents()[..8]); + let expires = if let Some(e) = + SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(u64::from_le_bytes(bytes))) + { + e + } else { + return Err(Error::InvalidPEM("failed to calculate SystemTime".into())); + }; + let dtls_certificate = + dtls::crypto::Certificate::from_pem(&pem_blocks.collect::>().join("\n\n"))?; + Ok(RTCCertificate::from_existing(dtls_certificate, expires)) + } + + /// Builds a [`RTCCertificate`] using the existing DTLS certificate. + /// + /// Use this method when you have a persistent certificate (i.e. you don't want to generate a + /// new one for each DTLS connection). + /// + /// NOTE: ID used for statistics will be different as it's neither derived from the given + /// certificate nor persisted along it when using [`serialize_pem`]. + pub fn from_existing(dtls_certificate: dtls::crypto::Certificate, expires: SystemTime) -> Self { + Self { + dtls_certificate, + expires, + } + } + + /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. + #[cfg(feature = "pem")] + pub fn serialize_pem(&self) -> String { + // Encode `expires` as a PEM block. + // + // TODO: serialize as nanos when https://github.com/rust-lang/rust/issues/103332 is fixed. + let expires_pem = pem::Pem::new( + "EXPIRES".to_string(), + self.expires + .duration_since(SystemTime::UNIX_EPOCH) + .expect("expires to be valid") + .as_secs() + .to_le_bytes() + .to_vec(), + ); + format!( + "{}\n{}", + pem::encode(&expires_pem), + self.dtls_certificate.serialize_pem() + ) + } + + /// get_fingerprints returns a SHA-256 fingerprint of this certificate. + /// + /// TODO: return a fingerprint computed with the digest algorithm used in the certificate + /// signature. + pub fn get_fingerprints(&self) -> Vec { + let mut fingerprints = Vec::new(); + + for c in &self.dtls_certificate.certificate { + let mut h = Sha256::new(); + h.update(c.as_ref()); + let hashed = h.finalize(); + let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); + + fingerprints.push(RTCDtlsFingerprint { + algorithm: "sha-256".to_owned(), + value: values.join(":"), + }); + } + + fingerprints + } +} + +const RUNES_ALPHA: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +/// math_rand_alpha generates a mathematical random alphabet sequence of the requested length. +fn math_rand_alpha(n: usize) -> String { + let mut rng = thread_rng(); + + let rand_string: String = (0..n) + .map(|_| { + let idx = rng.gen_range(0..RUNES_ALPHA.len()); + RUNES_ALPHA[idx] as char + }) + .collect(); + + rand_string +} diff --git a/src/server/config.rs b/src/server/config.rs new file mode 100644 index 0000000..5c19175 --- /dev/null +++ b/src/server/config.rs @@ -0,0 +1,6 @@ +use crate::server::certificate::RTCCertificate; + +#[derive(Default, Debug)] +pub struct ServerConfig { + pub certificate: Option, +} diff --git a/src/server/mod.rs b/src/server/mod.rs index db4ec2d..11e8baa 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -2,7 +2,10 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +pub mod certificate; +pub mod config; pub mod room; +pub mod states; use crate::shared::types::RoomId; use room::Room; diff --git a/src/server/room/endpoint/mod.rs b/src/server/room/endpoint/mod.rs index d258f2f..5fcda08 100644 --- a/src/server/room/endpoint/mod.rs +++ b/src/server/room/endpoint/mod.rs @@ -1,6 +1,6 @@ use crate::shared::types::{EndpointId, RoomId}; -#[derive(Default)] +#[derive(Default, Debug)] pub struct Endpoint { room_id: RoomId, endpoint_id: EndpointId, diff --git a/src/server/room/mod.rs b/src/server/room/mod.rs index 8b027a9..7444135 100644 --- a/src/server/room/mod.rs +++ b/src/server/room/mod.rs @@ -7,6 +7,7 @@ pub mod endpoint; use crate::shared::types::{EndpointId, RoomId}; use endpoint::Endpoint; +#[derive(Default, Debug)] pub struct Room { room_id: RoomId, endpoints: RefCell>>, diff --git a/src/server/states.rs b/src/server/states.rs new file mode 100644 index 0000000..c7fb882 --- /dev/null +++ b/src/server/states.rs @@ -0,0 +1,23 @@ +use crate::server::config::ServerConfig; +use crate::server::room::{endpoint::Endpoint, Room}; +use crate::shared::types::{FourTuple, RoomId}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::Arc; + +#[derive(Default, Debug)] +pub struct ServerStates { + config: Arc, + rooms: RefCell>>, + endpoints: RefCell>>, +} + +impl ServerStates { + pub fn new(config: Arc) -> Self { + Self { + config, + ..Default::default() + } + } +} diff --git a/src/shared/types.rs b/src/shared/types.rs index 28c56f4..d772184 100644 --- a/src/shared/types.rs +++ b/src/shared/types.rs @@ -1,2 +1,20 @@ +use retty::transport::TransportContext; +use std::net::SocketAddr; + pub type EndpointId = u64; pub type RoomId = u64; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct FourTuple { + pub local_addr: SocketAddr, + pub peer_addr: SocketAddr, +} + +impl From<&TransportContext> for FourTuple { + fn from(value: &TransportContext) -> Self { + Self { + local_addr: value.local_addr, + peer_addr: value.peer_addr, + } + } +}