Skip to main content

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(&not_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(&not_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}