sipx_media/dtls/openssl.rs
1//! The DTLS handshake itself, over the media socket.
2//!
3//! **Experimental** (`A-8`): behind the `dtls` feature, which is off by default. `sipx-call` reaches
4//! it only after an application explicitly selects DTLS-SRTP; enabling the feature alone changes
5//! no call. [`super`] says the same of what it keys; this module says it on its own page because a
6//! reader who lands here should not have to go up a level to find out (`A-8`'s rule).
7//!
8//! Everything RFC 5764 *decides* is in [`super`] and is compiled whatever the features say. This
9//! module is only the record layer and the handshake, and it is behind the `dtls` feature because
10//! it is where the C dependency lives.
11//!
12//! Why not a pure-Rust one: there is no DTLS implementation in Rust with comparable scrutiny, and
13//! a hand-rolled handshake for a security-critical protocol is the kind of liability this project
14//! declines elsewhere — the same reasoning that has SRTP's AES come from `RustCrypto` rather than
15//! from here. OpenSSL is also where `use_srtp` (RFC 5764 §4.1.1) and the RFC 5705 exporter have
16//! been exercised against every other implementation for a decade, which is what a keying
17//! mechanism needs most.
18
19use std::io::{Read, Write};
20use std::net::{SocketAddr, UdpSocket};
21use std::time::Duration;
22
23use openssl::ssl::{Ssl, SslContext, SslMethod, SslOptions, SslStream, SslVerifyMode};
24
25use super::{Handshake, Profile, Role};
26
27/// Why a handshake could not be run.
28#[derive(Debug, thiserror::Error)]
29#[non_exhaustive]
30pub enum DtlsError {
31 /// OpenSSL refused something.
32 #[error("openssl: {0}")]
33 Ssl(String),
34 /// The socket failed.
35 #[error("io: {0}")]
36 Io(#[from] std::io::Error),
37 /// The peer proposed no protection profile sipx implements.
38 #[error("no SRTP protection profile in common")]
39 NoProfile,
40}
41
42impl From<openssl::error::ErrorStack> for DtlsError {
43 fn from(error: openssl::error::ErrorStack) -> Self {
44 Self::Ssl(error.to_string())
45 }
46}
47
48/// A self-signed certificate to present on the media path, and its fingerprint.
49///
50/// RFC 5763 §5 wants a self-signed certificate here and says why the absence of a chain does not
51/// matter: what authenticates the peer is not the certificate's issuer but the fingerprint that
52/// arrived in the signalling. A certificate authority would authenticate a *name*, and there is no
53/// name on a media path to authenticate.
54pub struct Identity {
55 certificate: openssl::x509::X509,
56 key: openssl::pkey::PKey<openssl::pkey::Private>,
57}
58
59impl std::fmt::Debug for Identity {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("Identity").finish_non_exhaustive()
62 }
63}
64
65impl Identity {
66 /// Mint a fresh self-signed certificate.
67 ///
68 /// One per call is fine and one per process is fine; what must not happen is presenting a
69 /// certificate whose fingerprint is not the one the SDP announced, so the two are produced
70 /// together and [`Identity::fingerprint`] is the only way to get one.
71 pub fn generate() -> Result<Self, DtlsError> {
72 use openssl::asn1::Asn1Time;
73 use openssl::bn::{BigNum, MsbOption};
74 use openssl::ec::{EcGroup, EcKey};
75 use openssl::hash::MessageDigest;
76 use openssl::nid::Nid;
77 use openssl::x509::{X509, X509NameBuilder};
78
79 // P-256, because it is what every WebRTC endpoint negotiates and the point of DTLS-SRTP
80 // here is to be callable by one.
81 let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?;
82 let key = openssl::pkey::PKey::from_ec_key(EcKey::generate(&group)?)?;
83
84 let mut name = X509NameBuilder::new()?;
85 // The name is not checked by anything — §5 authenticates the fingerprint, not the subject
86 // — so it says what it is rather than pretending to be a host.
87 name.append_entry_by_nid(Nid::COMMONNAME, "sipx DTLS-SRTP")?;
88 let name = name.build();
89
90 let mut builder = X509::builder()?;
91 builder.set_version(2)?;
92 let mut serial = BigNum::new()?;
93 serial.rand(159, MsbOption::MAYBE_ZERO, false)?;
94 let serial = serial.to_asn1_integer()?;
95 builder.set_serial_number(&serial)?;
96 builder.set_subject_name(&name)?;
97 builder.set_issuer_name(&name)?;
98 builder.set_pubkey(&key)?;
99 let not_before = Asn1Time::days_from_now(0)?;
100 builder.set_not_before(¬_before)?;
101 // Thirty days. A media certificate outliving the call by a month is generous and still
102 // bounded; an unbounded one is a key with no end of life.
103 let not_after = Asn1Time::days_from_now(30)?;
104 builder.set_not_after(¬_after)?;
105 builder.sign(&key, MessageDigest::sha256())?;
106
107 Ok(Self {
108 certificate: builder.build(),
109 key,
110 })
111 }
112
113 /// The fingerprint to put in the SDP (RFC 8122).
114 pub fn fingerprint(&self) -> Result<sipx_sdp::fingerprint::Fingerprint, DtlsError> {
115 let der = self.certificate.to_der()?;
116 Ok(sipx_sdp::fingerprint::Fingerprint::of(
117 &der,
118 sipx_sdp::fingerprint::HashFunc::Sha256,
119 ))
120 }
121}
122
123/// A UDP socket connected to one peer, presented to OpenSSL as a stream.
124///
125/// DTLS is datagram-oriented and OpenSSL wants something that reads and writes; a connected
126/// `UdpSocket` is both, and connecting it is what makes the record layer see only this peer's
127/// packets. It also means the kernel drops everything else — which is fine here because a media
128/// port that is doing DTLS is doing it with the party the SDP named.
129#[derive(Debug)]
130struct Datagrams {
131 socket: UdpSocket,
132}
133
134trait DtlsIo: Read + Write + Send {}
135
136impl<T: Read + Write + Send> DtlsIo for T {}
137
138impl Read for Datagrams {
139 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
140 self.socket.recv(buf)
141 }
142}
143
144impl Write for Datagrams {
145 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
146 self.socket.send(buf)
147 }
148
149 fn flush(&mut self) -> std::io::Result<()> {
150 Ok(())
151 }
152}
153
154/// A DTLS-SRTP handshake over a media socket.
155pub struct Session {
156 stream: Option<SslStream<Box<dyn DtlsIo>>>,
157 pending: Option<Ssl>,
158 io: Option<Box<dyn DtlsIo>>,
159}
160
161impl std::fmt::Debug for Session {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.debug_struct("Session")
164 .field("handshaken", &self.stream.is_some())
165 .finish_non_exhaustive()
166 }
167}
168
169impl Session {
170 /// Prepare a handshake with `peer`, presenting `identity`.
171 ///
172 /// `timeout` bounds the handshake. DTLS retransmits its own flights, so without one a lost
173 /// final flight leaves both ends waiting — and a media path that never keys is worse than one
174 /// that fails, because the call is up and silent.
175 pub fn new(
176 socket: UdpSocket,
177 peer: SocketAddr,
178 identity: &Identity,
179 timeout: Duration,
180 ) -> Result<Self, DtlsError> {
181 socket.connect(peer)?;
182 socket.set_read_timeout(Some(timeout))?;
183 socket.set_write_timeout(Some(timeout))?;
184
185 Self::with_io(Datagrams { socket }, identity)
186 }
187
188 /// Prepare a handshake over an adapter owned by the browser component.
189 ///
190 /// The adapter is the only DTLS view of the component: it receives records already admitted
191 /// by ICE nomination and sends through the still-bound component socket. It never owns or
192 /// duplicates that socket descriptor.
193 pub(crate) fn with_io<I>(io: I, identity: &Identity) -> Result<Self, DtlsError>
194 where
195 I: Read + Write + Send + 'static,
196 {
197 let mut context = SslContext::builder(SslMethod::dtls())?;
198 context.set_certificate(&identity.certificate)?;
199 context.set_private_key(&identity.key)?;
200 // RFC 5764 §4.1.1: the profiles this endpoint will accept, in preference order. Only the
201 // one `sipx-rtp` can actually perform — offering more would be agreeing to a transform
202 // this stack cannot apply.
203 context.set_tlsext_use_srtp(Profile::Aes128CmHmacSha1_80.as_str())?;
204 // The peer's certificate is *requested* and not validated by OpenSSL, because there is
205 // nothing for it to validate against: RFC 5763 §5 expects a self-signed certificate, and
206 // what authenticates it is the fingerprint from the SDP. `super::establish` performs that
207 // check, and refuses the keys if it fails — so this is not verification being skipped, it
208 // is verification happening somewhere OpenSSL cannot see.
209 context.set_verify_callback(
210 SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT,
211 |_valid, _store| true,
212 );
213 // DTLS 1.0 is long dead and 1.2 is what every peer speaks.
214 context.set_options(SslOptions::NO_DTLSV1);
215
216 Ok(Self {
217 pending: Some(Ssl::new(&context.build())?),
218 stream: None,
219 io: Some(Box::new(io)),
220 })
221 }
222}
223
224impl Handshake for Session {
225 type Error = DtlsError;
226
227 fn run(&mut self, role: Role) -> Result<(), Self::Error> {
228 let (Some(ssl), Some(io)) = (self.pending.take(), self.io.take()) else {
229 // Already handshaken. Running twice would start a renegotiation nobody asked for.
230 return Ok(());
231 };
232 let mut stream = SslStream::new(ssl, io)?;
233 // The role is the negotiated `a=setup`, never a guess: a UA that connects when it agreed
234 // to accept meets one coming the other way, and both time out.
235 let outcome = match role {
236 Role::Client => stream.connect(),
237 Role::Server => stream.accept(),
238 };
239 outcome.map_err(|error| DtlsError::Ssl(error.to_string()))?;
240 self.stream = Some(stream);
241 Ok(())
242 }
243
244 fn peer_certificate(&self) -> Option<Vec<u8>> {
245 self.stream
246 .as_ref()?
247 .ssl()
248 .peer_certificate()?
249 .to_der()
250 .ok()
251 }
252
253 fn profile(&self) -> Option<Profile> {
254 let name = self.stream.as_ref()?.ssl().selected_srtp_profile()?.name();
255 (name == Profile::Aes128CmHmacSha1_80.as_str()).then_some(Profile::Aes128CmHmacSha1_80)
256 }
257
258 fn export(&self, len: usize) -> Result<Vec<u8>, Self::Error> {
259 let stream = self.stream.as_ref().ok_or(DtlsError::NoProfile)?;
260 let mut out = vec![0u8; len];
261 // RFC 5705's exporter with RFC 5764 §4.2's label and **no context**. A zero-length context
262 // and an absent one derive different keys, and §4.2 specifies absent — passing an empty
263 // slice here is the mistake that produces a handshake both ends complete and no packet
264 // either can decrypt.
265 stream
266 .ssl()
267 .export_keying_material(&mut out, super::EXPORTER_LABEL, None)
268 .map_err(|error| DtlsError::Ssl(error.to_string()))?;
269 Ok(out)
270 }
271}