Skip to content

Commit c0579c3

Browse files
authored
[peer_connection] RTCCertificate: rework from_pem and serialize_pem (#333)
which makes it possible to serialise and persist a certificate to disk. Both methods require `pem` feature to be enabled (I think having a feature is good since I don't expect many people to use persistent certificate). Previous implementation of `from_pem` was incorrect. It used a given certificate as a CA (hence the 2nd argument) instead of just using the certificate directly (see the related TODO in tests where `cert1 == cert3` should be matching). API reflects one in Pion - https://github.com/pion/webrtc/blob/master/certificate.go#L179 (see `CertificateFromPEM` and `PEM` functions). Also - modify `RTCCertificate::get_fingerprints` to return `Vec<Fingerprint>`. No need for `Result`. - refactor `dtls::crypto::Certificate` - fixes TODO where two certificates should be equal when `from_pem` is being used.
1 parent 203529d commit c0579c3

File tree

13 files changed

+318
-318
lines changed

13 files changed

+318
-318
lines changed

dtls/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
## Unreleased
44

55
* Increased minimum support rust version to `1.60.0`.
6+
* Add `RTCCertificate::from_pem` and `RTCCertificate::serialize_pem` (only work with `pem` feature enabled) [#333]
7+
8+
[#333]: https://github.com/webrtc-rs/webrtc/pull/333
69

710
## v0.6.0
811

dtls/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ serde = { version = "1.0.110", features = ["derive"] }
4949
subtle = "2.4"
5050
log = "0.4.16"
5151
thiserror = "1.0"
52+
pem = { version = "1", optional = true }
5253

5354
[dev-dependencies]
5455
tokio-test = "0.4.0" # must match the min version of the `tokio` crate above
@@ -57,6 +58,9 @@ chrono = "0.4.19"
5758
clap = "3.2.6"
5859
hub = {path = "examples/hub"}
5960

61+
[features]
62+
pem = ["dep:pem"]
63+
6064
[[example]]
6165
name = "dial_psk"
6266
path = "examples/dial/psk/dial_psk.rs"

dtls/src/crypto/mod.rs

Lines changed: 129 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,103 +15,102 @@ use der_parser::{oid, oid::Oid};
1515
use rcgen::KeyPair;
1616
use ring::rand::SystemRandom;
1717
use ring::signature::{EcdsaKeyPair, Ed25519KeyPair, RsaKeyPair};
18+
use std::convert::TryFrom;
1819
use std::sync::Arc;
1920

20-
#[derive(Clone, PartialEq)]
21+
/// A X.509 certificate(s) used to authenticate a DTLS connection.
22+
#[derive(Clone, PartialEq, Debug)]
2123
pub struct Certificate {
24+
/// DER-encoded certificates.
2225
pub certificate: Vec<rustls::Certificate>,
26+
/// Private key.
2327
pub private_key: CryptoPrivateKey,
2428
}
2529

2630
impl Certificate {
31+
/// Generate a self-signed certificate.
32+
///
33+
/// See [`rcgen::generate_simple_self_signed`].
2734
pub fn generate_self_signed(subject_alt_names: impl Into<Vec<String>>) -> Result<Self> {
2835
let cert = rcgen::generate_simple_self_signed(subject_alt_names)?;
29-
let certificate = cert.serialize_der()?;
3036
let key_pair = cert.get_key_pair();
31-
let serialized_der = key_pair.serialize_der();
32-
let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) {
33-
CryptoPrivateKey {
34-
kind: CryptoPrivateKeyKind::Ed25519(
35-
Ed25519KeyPair::from_pkcs8(&serialized_der)
36-
.map_err(|e| Error::Other(e.to_string()))?,
37-
),
38-
serialized_der,
39-
}
40-
} else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) {
41-
CryptoPrivateKey {
42-
kind: CryptoPrivateKeyKind::Ecdsa256(
43-
EcdsaKeyPair::from_pkcs8(
44-
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
45-
&serialized_der,
46-
)
47-
.map_err(|e| Error::Other(e.to_string()))?,
48-
),
49-
serialized_der,
50-
}
51-
} else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) {
52-
CryptoPrivateKey {
53-
kind: CryptoPrivateKeyKind::Rsa256(
54-
RsaKeyPair::from_pkcs8(&serialized_der)
55-
.map_err(|e| Error::Other(e.to_string()))?,
56-
),
57-
serialized_der,
58-
}
59-
} else {
60-
return Err(Error::Other("Unsupported key_pair".to_owned()));
61-
};
6237

6338
Ok(Certificate {
64-
certificate: vec![rustls::Certificate(certificate)],
65-
private_key,
39+
certificate: vec![rustls::Certificate(cert.serialize_der()?)],
40+
private_key: CryptoPrivateKey::try_from(key_pair)?,
6641
})
6742
}
6843

44+
/// Generate a self-signed certificate with the given algorithm.
45+
///
46+
/// See [`rcgen::Certificate::from_params`].
6947
pub fn generate_self_signed_with_alg(
7048
subject_alt_names: impl Into<Vec<String>>,
7149
alg: &'static rcgen::SignatureAlgorithm,
7250
) -> Result<Self> {
7351
let mut params = rcgen::CertificateParams::new(subject_alt_names);
7452
params.alg = alg;
7553
let cert = rcgen::Certificate::from_params(params)?;
76-
let certificate = cert.serialize_der()?;
7754
let key_pair = cert.get_key_pair();
78-
let serialized_der = key_pair.serialize_der();
79-
let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) {
80-
CryptoPrivateKey {
81-
kind: CryptoPrivateKeyKind::Ed25519(
82-
Ed25519KeyPair::from_pkcs8(&serialized_der)
83-
.map_err(|e| Error::Other(e.to_string()))?,
84-
),
85-
serialized_der,
86-
}
87-
} else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) {
88-
CryptoPrivateKey {
89-
kind: CryptoPrivateKeyKind::Ecdsa256(
90-
EcdsaKeyPair::from_pkcs8(
91-
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
92-
&serialized_der,
93-
)
94-
.map_err(|e| Error::Other(e.to_string()))?,
95-
),
96-
serialized_der,
97-
}
98-
} else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) {
99-
CryptoPrivateKey {
100-
kind: CryptoPrivateKeyKind::Rsa256(
101-
RsaKeyPair::from_pkcs8(&serialized_der)
102-
.map_err(|e| Error::Other(e.to_string()))?,
103-
),
104-
serialized_der,
55+
56+
Ok(Certificate {
57+
certificate: vec![rustls::Certificate(cert.serialize_der()?)],
58+
private_key: CryptoPrivateKey::try_from(key_pair)?,
59+
})
60+
}
61+
62+
/// Parses a certificate from the ASCII PEM format.
63+
#[cfg(feature = "pem")]
64+
pub fn from_pem(pem_str: &str) -> Result<Self> {
65+
let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?;
66+
if pems.len() < 2 {
67+
return Err(Error::InvalidPEM(format!(
68+
"expected at least two PEM blocks, got {}",
69+
pems.len()
70+
)));
71+
}
72+
if pems[0].tag != "PRIVATE_KEY" {
73+
return Err(Error::InvalidPEM(format!(
74+
"invalid tag (expected: 'PRIVATE_KEY', got: '{}')",
75+
pems[0].tag
76+
)));
77+
}
78+
79+
let keypair = rcgen::KeyPair::from_der(&pems[0].contents)
80+
.map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {}", e)))?;
81+
82+
let mut rustls_certs = Vec::new();
83+
for p in pems.drain(1..) {
84+
if p.tag != "CERTIFICATE" {
85+
return Err(Error::InvalidPEM(format!(
86+
"invalid tag (expected: 'CERTIFICATE', got: '{}')",
87+
p.tag
88+
)));
10589
}
106-
} else {
107-
return Err(Error::Other("Unsupported key_pair".to_owned()));
108-
};
90+
rustls_certs.push(rustls::Certificate(p.contents));
91+
}
10992

11093
Ok(Certificate {
111-
certificate: vec![rustls::Certificate(certificate)],
112-
private_key,
94+
certificate: rustls_certs,
95+
private_key: CryptoPrivateKey::try_from(&keypair)?,
11396
})
11497
}
98+
99+
/// Serializes the certificate (including the private key) in PKCS#8 format in PEM.
100+
#[cfg(feature = "pem")]
101+
pub fn serialize_pem(&self) -> String {
102+
let mut data = vec![pem::Pem {
103+
tag: "PRIVATE_KEY".to_string(),
104+
contents: self.private_key.serialized_der.clone(),
105+
}];
106+
for rustls_cert in &self.certificate {
107+
data.push(pem::Pem {
108+
tag: "CERTIFICATE".to_string(),
109+
contents: rustls_cert.0.clone(),
110+
});
111+
}
112+
pem::encode_many(&data)
113+
}
115114
}
116115

117116
pub(crate) fn value_key_message(
@@ -134,14 +133,20 @@ pub(crate) fn value_key_message(
134133
plaintext
135134
}
136135

136+
/// Either ED25519, ECDSA or RSA keypair.
137+
#[derive(Debug)]
137138
pub enum CryptoPrivateKeyKind {
138139
Ed25519(Ed25519KeyPair),
139140
Ecdsa256(EcdsaKeyPair),
140141
Rsa256(RsaKeyPair),
141142
}
142143

144+
/// Private key.
145+
#[derive(Debug)]
143146
pub struct CryptoPrivateKey {
147+
/// Keypair.
144148
pub kind: CryptoPrivateKeyKind,
149+
/// DER-encoded keypair.
145150
pub serialized_der: Vec<u8>,
146151
}
147152

@@ -196,6 +201,44 @@ impl Clone for CryptoPrivateKey {
196201
}
197202
}
198203

204+
impl TryFrom<&KeyPair> for CryptoPrivateKey {
205+
type Error = Error;
206+
207+
fn try_from(key_pair: &KeyPair) -> Result<Self> {
208+
let serialized_der = key_pair.serialize_der();
209+
if key_pair.is_compatible(&rcgen::PKCS_ED25519) {
210+
Ok(CryptoPrivateKey {
211+
kind: CryptoPrivateKeyKind::Ed25519(
212+
Ed25519KeyPair::from_pkcs8(&serialized_der)
213+
.map_err(|e| Error::Other(e.to_string()))?,
214+
),
215+
serialized_der,
216+
})
217+
} else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) {
218+
Ok(CryptoPrivateKey {
219+
kind: CryptoPrivateKeyKind::Ecdsa256(
220+
EcdsaKeyPair::from_pkcs8(
221+
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
222+
&serialized_der,
223+
)
224+
.map_err(|e| Error::Other(e.to_string()))?,
225+
),
226+
serialized_der,
227+
})
228+
} else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) {
229+
Ok(CryptoPrivateKey {
230+
kind: CryptoPrivateKeyKind::Rsa256(
231+
RsaKeyPair::from_pkcs8(&serialized_der)
232+
.map_err(|e| Error::Other(e.to_string()))?,
233+
),
234+
serialized_der,
235+
})
236+
} else {
237+
Err(Error::Other("Unsupported key_pair".to_owned()))
238+
}
239+
}
240+
}
241+
199242
impl CryptoPrivateKey {
200243
pub fn from_key_pair(key_pair: &KeyPair) -> Result<Self> {
201244
let serialized_der = key_pair.serialize_der();
@@ -458,3 +501,21 @@ pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len:
458501

459502
additional_data
460503
}
504+
505+
#[cfg(test)]
506+
mod test {
507+
use super::*;
508+
509+
#[cfg(feature = "pem")]
510+
#[test]
511+
fn test_certificate_serialize_pem_and_from_pem() -> Result<()> {
512+
let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?;
513+
514+
let pem = cert.serialize_pem();
515+
let loaded_cert = Certificate::from_pem(&pem)?;
516+
517+
assert_eq!(loaded_cert, cert);
518+
519+
Ok(())
520+
}
521+
}

dtls/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ pub enum Error {
166166
#[error("keying material: {0}")]
167167
KeyingMaterial(#[from] KeyingMaterialExporterError),
168168

169+
/// Error parsing a given PEM string.
170+
#[error("invalid PEM: {0}")]
171+
InvalidPEM(String),
172+
169173
#[allow(non_camel_case_types)]
170174
#[error("{0}")]
171175
Other(String),

webrtc/CHANGELOG.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ directions that should not send. [#316](https://github.com/webrtc-rs/webrtc/pull
2020
change for MediaEngine::register_header_extension
2121
* Removes support for Plan-B. All major implementations of WebRTC now support unified and continuing support for plan-b is an undue maintenance burden when unified can be used. See [“Unified Plan” Transition Guide (JavaScript)](https://docs.google.com/document/d/1-ZfikoUtoJa9k-GZG1daN0BU3IjIanQ_JSscHxQesvU/) for an overview of the changes required to migrate. [#320](https://github.com/webrtc-rs/webrtc/pull/320) by [@algesten](https://github.com/algesten).
2222

23+
#### Breaking changes
24+
25+
* Remove 2nd argument from `RTCCertificate::from_pem` and guard it with `pem` feature [#333]
26+
* Rename `RTCCertificate::pem` to `serialize_pem` and guard it with `pem` feature [#333]
27+
* Remove `RTCCertificate::expires` [#333]
28+
* `RTCCertificate::get_fingerprints` no longer returns `Result` [#333]
29+
30+
[#333]: https://github.com/webrtc-rs/webrtc/pull/333
2331

2432
## 0.5.1
2533

@@ -33,12 +41,12 @@ change for MediaEngine::register_header_extension
3341

3442
* The serialized format for `RTCIceCandidateInit` has changed to match what the specification i.e. keys are camelCase. [#153 Make RTCIceCandidateInit conform to WebRTC spec](https://github.com/webrtc-rs/webrtc/pull/153) contributed by [jmatss](https://github.com/jmatss).
3543
* Improved robustness when proposing RTP extension IDs and handling of collisions in these. This change is only breaking if you have assumed anything about the nature of these extension IDs. [#154 Fix RTP extension id collision](https://github.com/webrtc-rs/webrtc/pull/154) contributed by [k0nserv](https://github.com/k0nserv)
36-
* Transceivers will now not stop when either or both directions are disabled. That is, applying and SDP with `a=inactive` will not stop the transceiver, instead attached senders and receivers will pause. A transceiver can be resurrected by setting direction back to e.g. `a=sendrecv`. The desired direction can be controlled with the newly introduced public method `set_direction` on `RTCRtpTransceiver`.
44+
* Transceivers will now not stop when either or both directions are disabled. That is, applying and SDP with `a=inactive` will not stop the transceiver, instead attached senders and receivers will pause. A transceiver can be resurrected by setting direction back to e.g. `a=sendrecv`. The desired direction can be controlled with the newly introduced public method `set_direction` on `RTCRtpTransceiver`.
3745
* [#201 Handle inactive transceivers more correctly](https://github.com/webrtc-rs/webrtc/pull/201) contributed by [k0nserv](https://github.com/k0nserv)
38-
* [#210 Rework transceiver direction support further](https://github.com/webrtc-rs/webrtc/pull/210) contributed by [k0nserv](https://github.com/k0nserv)
46+
* [#210 Rework transceiver direction support further](https://github.com/webrtc-rs/webrtc/pull/210) contributed by [k0nserv](https://github.com/k0nserv)
3947
* [#214 set_direction add missing Send + Sync bound](https://github.com/webrtc-rs/webrtc/pull/214) contributed by [algesten](https://github.com/algesten)
4048
* [#213 set_direction add missing Sync bound](https://github.com/webrtc-rs/webrtc/pull/213) contributed by [algesten](https://github.com/algesten)
41-
* [#212 Public RTCRtpTransceiver::set_direction](https://github.com/webrtc-rs/webrtc/pull/212) contributed by [algesten](https://github.com/algesten)
49+
* [#212 Public RTCRtpTransceiver::set_direction](https://github.com/webrtc-rs/webrtc/pull/212) contributed by [algesten](https://github.com/algesten)
4250
* [#268 Fix current direction update when applying answer](https://github.com/webrtc-rs/webrtc/pull/268) contributed by [k0nserv](https://github.com/k0nserv)
4351
* [#236 Pause RTP writing if direction indicates it](https://github.com/webrtc-rs/webrtc/pull/236) contributed by [algesten](https://github.com/algesten)
4452
* Generated the `a=msid` line for `m=` line sections according to the specification. This might be break remote peers that relied on the previous, incorrect, behaviour. This also fixes a bug where an endless negotiation loop could happen. [#217 Correct msid handling for RtpSender](https://github.com/webrtc-rs/webrtc/pull/217) contributed by [k0nserv](https://github.com/k0nserv)
@@ -108,7 +116,7 @@ The various sub-crates have been updated as follows:
108116

109117
Their respective change logs are found in the old, now archived, repositories and within their respective `CHANGELOG.md` files in the monorepo.
110118

111-
### Contributors
119+
### Contributors
112120

113121
A big thanks to all the contributors that have made this release happen:
114122

webrtc/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ ring = "0.16.20"
4646
sha2 = "0.10.2"
4747
lazy_static = "1.4"
4848
hex = "0.4.3"
49+
pem = { version = "1", optional = true }
4950

5051
# [minimal-versions]
5152
# fixes "the trait bound `time::Month: From<u8>` is not satisfied"
@@ -56,3 +57,6 @@ time = "~0.3.1"
5657
[dev-dependencies]
5758
tokio-test = "0.4.0" # must match the min version of the `tokio` crate above
5859
env_logger = "0.9.0"
60+
61+
[features]
62+
pem = ["dep:pem", "dtls/pem"]

webrtc/src/api/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl API {
8686
if !certificates.is_empty() {
8787
let now = SystemTime::now();
8888
for cert in &certificates {
89-
cert.expires()
89+
cert.expires
9090
.duration_since(now)
9191
.map_err(|_| Error::ErrCertificateExpired)?;
9292
}

webrtc/src/dtls_transport/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ impl RTCDtlsTransport {
156156
let mut fingerprints = vec![];
157157

158158
for c in &self.certificates {
159-
fingerprints.extend(c.get_fingerprints()?);
159+
fingerprints.extend(c.get_fingerprints());
160160
}
161161

162162
Ok(DTLSParameters {
@@ -336,7 +336,7 @@ impl RTCDtlsTransport {
336336
}
337337

338338
let certificate = if let Some(cert) = self.certificates.first() {
339-
cert.certificate.clone()
339+
cert.dtls_certificate.clone()
340340
} else {
341341
return Err(Error::ErrNonCertificate);
342342
};

webrtc/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ pub enum Error {
408408
#[error("parse url: {0}")]
409409
ParseUrl(#[from] url::ParseError),
410410

411+
/// Error parsing a given PEM string.
412+
#[error("invalid PEM: {0}")]
413+
InvalidPEM(String),
414+
411415
#[allow(non_camel_case_types)]
412416
#[error("{0}")]
413417
new(String),

0 commit comments

Comments
 (0)