webrtc/peer_connection/
certificate.rs

1use std::ops::Add;
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4use dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind};
5use rcgen::{CertificateParams, KeyPair};
6use ring::rand::SystemRandom;
7use ring::rsa;
8use ring::signature::{EcdsaKeyPair, Ed25519KeyPair};
9use sha2::{Digest, Sha256};
10
11use crate::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint;
12use crate::error::{Error, Result};
13use crate::peer_connection::math_rand_alpha;
14use crate::stats::stats_collector::StatsCollector;
15use crate::stats::{CertificateStats, StatsReportType};
16
17/// Certificate represents a X.509 certificate used to authenticate WebRTC communications.
18///
19/// ## Specifications
20///
21/// * [MDN]
22/// * [W3C]
23///
24/// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate
25/// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtccertificate
26#[derive(Clone, Debug)]
27pub struct RTCCertificate {
28    /// DTLS certificate.
29    pub(crate) dtls_certificate: dtls::crypto::Certificate,
30    /// Timestamp after which this certificate is no longer valid.
31    pub(crate) expires: SystemTime,
32    /// Certificate's ID used for statistics.
33    ///
34    /// Example: "certificate-1667202302853538793"
35    ///
36    /// See [`CertificateStats`].
37    pub(crate) stats_id: String,
38}
39
40impl PartialEq for RTCCertificate {
41    fn eq(&self, other: &Self) -> bool {
42        self.dtls_certificate == other.dtls_certificate
43    }
44}
45
46impl RTCCertificate {
47    /// Generates a new certificate from the given parameters.
48    ///
49    /// See [`rcgen::Certificate::from_params`].
50    fn from_params(params: CertificateParams, key_pair: KeyPair) -> Result<Self> {
51        let not_after = params.not_after;
52
53        let x509_cert = params.self_signed(&key_pair).unwrap();
54        let serialized_der = key_pair.serialize_der();
55
56        let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) {
57            CryptoPrivateKey {
58                kind: CryptoPrivateKeyKind::Ed25519(
59                    Ed25519KeyPair::from_pkcs8(&serialized_der)
60                        .map_err(|e| Error::new(e.to_string()))?,
61                ),
62                serialized_der,
63            }
64        } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) {
65            CryptoPrivateKey {
66                kind: CryptoPrivateKeyKind::Ecdsa256(
67                    EcdsaKeyPair::from_pkcs8(
68                        &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
69                        &serialized_der,
70                        &SystemRandom::new(),
71                    )
72                    .map_err(|e| Error::new(e.to_string()))?,
73                ),
74                serialized_der,
75            }
76        } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) {
77            CryptoPrivateKey {
78                kind: CryptoPrivateKeyKind::Rsa256(
79                    rsa::KeyPair::from_pkcs8(&serialized_der)
80                        .map_err(|e| Error::new(e.to_string()))?,
81                ),
82                serialized_der,
83            }
84        } else {
85            return Err(Error::new("Unsupported key_pair".to_owned()));
86        };
87
88        let expires = if cfg!(target_arch = "arm") {
89            // Workaround for issue overflow when adding duration to instant on armv7
90            // https://github.com/webrtc-rs/examples/issues/5 https://github.com/chronotope/chrono/issues/343
91            SystemTime::now().add(Duration::from_secs(172800)) //60*60*48 or 2 days
92        } else {
93            not_after.into()
94        };
95
96        Ok(Self {
97            dtls_certificate: dtls::crypto::Certificate {
98                certificate: vec![x509_cert.der().to_owned()],
99                private_key,
100            },
101            expires,
102            stats_id: gen_stats_id(),
103        })
104    }
105
106    /// Generates a new certificate with default [`CertificateParams`] using the given keypair.
107    pub fn from_key_pair(key_pair: KeyPair) -> Result<Self> {
108        if !(key_pair.is_compatible(&rcgen::PKCS_ED25519)
109            || key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256)
110            || key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256))
111        {
112            return Err(Error::new("Unsupported key_pair".to_owned()));
113        }
114
115        RTCCertificate::from_params(
116            CertificateParams::new(vec![math_rand_alpha(16)]).unwrap(),
117            key_pair,
118        )
119    }
120
121    /// Parses a certificate from the ASCII PEM format.
122    #[cfg(feature = "pem")]
123    pub fn from_pem(pem_str: &str) -> Result<Self> {
124        let mut pem_blocks = pem_str.split("\n\n");
125        let first_block = if let Some(b) = pem_blocks.next() {
126            b
127        } else {
128            return Err(Error::InvalidPEM("empty PEM".into()));
129        };
130        let expires_pem =
131            pem::parse(first_block).map_err(|e| Error::new(format!("can't parse PEM: {e}")))?;
132        if expires_pem.tag() != "EXPIRES" {
133            return Err(Error::InvalidPEM(format!(
134                "invalid tag (expected: 'EXPIRES', got '{}')",
135                expires_pem.tag()
136            )));
137        }
138        let mut bytes = [0u8; 8];
139        bytes.copy_from_slice(&expires_pem.contents()[..8]);
140        let expires = if let Some(e) =
141            SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(u64::from_le_bytes(bytes)))
142        {
143            e
144        } else {
145            return Err(Error::InvalidPEM("failed to calculate SystemTime".into()));
146        };
147        let dtls_certificate =
148            dtls::crypto::Certificate::from_pem(&pem_blocks.collect::<Vec<&str>>().join("\n\n"))?;
149        Ok(RTCCertificate::from_existing(dtls_certificate, expires))
150    }
151
152    /// Builds a [`RTCCertificate`] using the existing DTLS certificate.
153    ///
154    /// Use this method when you have a persistent certificate (i.e. you don't want to generate a
155    /// new one for each DTLS connection).
156    ///
157    /// NOTE: ID used for statistics will be different as it's neither derived from the given
158    /// certificate nor persisted along it when using [`RTCCertificate::serialize_pem`].
159    pub fn from_existing(dtls_certificate: dtls::crypto::Certificate, expires: SystemTime) -> Self {
160        Self {
161            dtls_certificate,
162            expires,
163            // TODO: figure out if it needs to be persisted
164            stats_id: gen_stats_id(),
165        }
166    }
167
168    /// Serializes the certificate (including the private key) in PKCS#8 format in PEM.
169    #[cfg(any(doc, feature = "pem"))]
170    pub fn serialize_pem(&self) -> String {
171        // Encode `expires` as a PEM block.
172        //
173        // TODO: serialize as nanos when https://github.com/rust-lang/rust/issues/103332 is fixed.
174        let expires_pem = pem::Pem::new(
175            "EXPIRES".to_string(),
176            self.expires
177                .duration_since(SystemTime::UNIX_EPOCH)
178                .expect("expires to be valid")
179                .as_secs()
180                .to_le_bytes()
181                .to_vec(),
182        );
183        format!(
184            "{}\n{}",
185            pem::encode(&expires_pem),
186            self.dtls_certificate.serialize_pem()
187        )
188    }
189
190    /// get_fingerprints returns a SHA-256 fingerprint of this certificate.
191    ///
192    /// TODO: return a fingerprint computed with the digest algorithm used in the certificate
193    /// signature.
194    pub fn get_fingerprints(&self) -> Vec<RTCDtlsFingerprint> {
195        let mut fingerprints = Vec::new();
196
197        for c in &self.dtls_certificate.certificate {
198            let mut h = Sha256::new();
199            h.update(c.as_ref());
200            let hashed = h.finalize();
201            let values: Vec<String> = hashed.iter().map(|x| format! {"{x:02x}"}).collect();
202
203            fingerprints.push(RTCDtlsFingerprint {
204                algorithm: "sha-256".to_owned(),
205                value: values.join(":"),
206            });
207        }
208
209        fingerprints
210    }
211
212    pub(crate) async fn collect_stats(&self, collector: &StatsCollector) {
213        if let Some(fingerprint) = self.get_fingerprints().into_iter().next() {
214            let stats = CertificateStats::new(self, fingerprint);
215            collector.insert(
216                self.stats_id.clone(),
217                StatsReportType::CertificateStats(stats),
218            );
219        }
220    }
221}
222
223fn gen_stats_id() -> String {
224    format!(
225        "certificate-{}",
226        SystemTime::now()
227            .duration_since(UNIX_EPOCH)
228            .unwrap()
229            .as_nanos() as u64
230    )
231}
232
233#[cfg(test)]
234mod test {
235    use super::*;
236
237    #[test]
238    fn test_generate_certificate_rsa() -> Result<()> {
239        let key_pair = KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256);
240        assert!(key_pair.is_err(), "RcgenError::KeyGenerationUnavailable");
241
242        Ok(())
243    }
244
245    #[test]
246    fn test_generate_certificate_ecdsa() -> Result<()> {
247        let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
248        let _cert = RTCCertificate::from_key_pair(kp)?;
249
250        Ok(())
251    }
252
253    #[test]
254    fn test_generate_certificate_eddsa() -> Result<()> {
255        let kp = KeyPair::generate_for(&rcgen::PKCS_ED25519)?;
256        let _cert = RTCCertificate::from_key_pair(kp)?;
257
258        Ok(())
259    }
260
261    #[test]
262    fn test_certificate_equal() -> Result<()> {
263        let kp1 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
264        let cert1 = RTCCertificate::from_key_pair(kp1)?;
265
266        let kp2 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
267        let cert2 = RTCCertificate::from_key_pair(kp2)?;
268
269        assert_ne!(cert1, cert2);
270
271        Ok(())
272    }
273
274    #[test]
275    fn test_generate_certificate_expires_and_stats_id() -> Result<()> {
276        let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
277        let cert = RTCCertificate::from_key_pair(kp)?;
278
279        let now = SystemTime::now();
280        assert!(cert.expires.duration_since(now).is_ok());
281        assert!(cert.stats_id.contains("certificate"));
282
283        Ok(())
284    }
285
286    #[cfg(feature = "pem")]
287    #[test]
288    fn test_certificate_serialize_pem_and_from_pem() -> Result<()> {
289        let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
290        let cert = RTCCertificate::from_key_pair(kp)?;
291
292        let pem = cert.serialize_pem();
293        let loaded_cert = RTCCertificate::from_pem(&pem)?;
294
295        assert_eq!(loaded_cert, cert);
296
297        Ok(())
298    }
299}