Skip to main content

mbedtls_rs/
session.rs

1use core::ffi::{c_char, c_int, c_void, CStr};
2use core::marker::PhantomData;
3use core::ptr::NonNull;
4
5use embedded_io::{Error, ErrorKind};
6
7use super::sys::*;
8use super::{
9    mbedtls_calloc, mbedtls_free, mbedtls_rng, Certificate, MBox, PrivateKey, Tls, TlsReference,
10    TlsVersion,
11};
12
13pub use asynch::*;
14
15mod asynch;
16pub mod blocking;
17
18/// An owned, nul-terminated server name allocated through MbedTLS's allocator
19/// (`mbedtls_calloc`/`mbedtls_free`), so it honours a user-overridden MbedTLS
20/// heap instead of the Rust global allocator. Used to bind a saved session to a
21/// peer identity (see [`SavedSession`]).
22pub(crate) struct ServerName {
23    ptr: NonNull<u8>,
24}
25
26impl ServerName {
27    /// Copy a `&CStr`'s bytes (including the nul) into a fresh MbedTLS-allocated
28    /// buffer. Returns `None` if the allocation fails.
29    fn from_cstr(name: &CStr) -> Option<Self> {
30        Self::from_bytes_with_nul(name.to_bytes_with_nul())
31    }
32
33    fn from_bytes_with_nul(bytes: &[u8]) -> Option<Self> {
34        // SAFETY: `mbedtls_calloc` is the MbedTLS allocator entry point; requesting
35        // `bytes.len()` elements of one byte each yields a `bytes.len()`-byte (or
36        // null) allocation, which `NonNull::new` checks before we treat it as owned.
37        let ptr =
38            NonNull::new(unsafe { mbedtls_calloc(bytes.len(), size_of::<u8>()) }.cast::<u8>())?;
39
40        // SAFETY: `ptr` points to a fresh `bytes.len()`-byte allocation, and the
41        // source slice is independent of it.
42        unsafe {
43            core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.as_ptr(), bytes.len());
44        }
45
46        Some(Self { ptr })
47    }
48
49    fn as_bytes(&self) -> &[u8] {
50        // SAFETY: `ptr` was constructed from a nul-terminated source slice in
51        // `from_bytes_with_nul` and is never mutated after construction.
52        unsafe { CStr::from_ptr(self.ptr.as_ptr().cast::<c_char>()) }.to_bytes_with_nul()
53    }
54}
55
56impl Drop for ServerName {
57    fn drop(&mut self) {
58        // SAFETY: `ptr` was allocated by `mbedtls_calloc` in `from_bytes_with_nul`,
59        // is owned solely by this `ServerName`, and is freed exactly once here.
60        unsafe {
61            mbedtls_free(self.ptr.as_ptr() as *mut c_void);
62        }
63    }
64}
65
66/// A reusable TLS session state captured from a connected session.
67///
68/// A saved session is bound to the server name that was configured when it was
69/// captured, and [`Session::connect_with_session`] refuses to resume it against
70/// a different server name (see that method). A session captured without a
71/// server name carries no peer binding and may only be resumed into an equally
72/// nameless session.
73pub struct SavedSession {
74    pub(crate) mbedtls_session: MBox<mbedtls_ssl_session>,
75    /// The server name the originating session was configured with, used to
76    /// reject cross-host resume. `None` if the session had no server name.
77    pub(crate) server_name: Option<ServerName>,
78}
79
80/// Reject reusing a saved session against a different peer identity.
81///
82/// TLS 1.2 resume acceptance does not re-validate the server certificate, so a
83/// session saved for host A presented to a server B that accepts it would skip
84/// verifying B's certificate. We enforce the binding in the wrapper for both TLS
85/// 1.2 and 1.3 (1.3 also checks in C). `None`/`Some` are treated as distinct, so
86/// a nameless saved session only resumes into a nameless session.
87pub(crate) fn check_saved_session_server_name(
88    saved: &Option<ServerName>,
89    current: *const c_char,
90) -> Result<(), SessionError> {
91    let current_bytes: Option<&[u8]> = if current.is_null() {
92        None
93    } else {
94        // SAFETY: a non-null hostname pointer read from `mbedtls_ssl_context`
95        // is a heap-allocated, nul-terminated string owned by the SSL context.
96        // The borrow only lives for the synchronous byte comparison below.
97        Some(unsafe { CStr::from_ptr(current) }.to_bytes_with_nul())
98    };
99    let saved_bytes = saved.as_ref().map(|n| n.as_bytes());
100
101    if current_bytes == saved_bytes {
102        Ok(())
103    } else {
104        Err(SessionError::MbedTls(MbedtlsError::new(
105            MBEDTLS_ERR_SSL_BAD_INPUT_DATA,
106        )))
107    }
108}
109
110/// Certificate verification mode used for a session
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112#[cfg_attr(feature = "defmt", derive(defmt::Format))]
113pub enum AuthMode {
114    /// Peer certificate is not checked (default on server) (insecure on client)
115    None,
116    /// Peer certificate is checked, however the handshake continues even if verification failed;
117    /// [mbedtls_ssl_get_verify_result()] can be called after the handshake is complete.
118    Optional,
119    /// Peer *must* present a valid certificate, handshake is aborted if verification failed. (default on client)
120    Required,
121    /// Used only for sni_authmode
122    Unset,
123}
124
125impl AuthMode {
126    fn mbedtls_authmode(&self) -> i32 {
127        (match self {
128            AuthMode::None => MBEDTLS_SSL_VERIFY_NONE,
129            AuthMode::Optional => MBEDTLS_SSL_VERIFY_OPTIONAL,
130            AuthMode::Required => MBEDTLS_SSL_VERIFY_REQUIRED,
131            AuthMode::Unset => MBEDTLS_SSL_VERIFY_UNSET,
132        }) as i32
133    }
134}
135
136/// The credentials (certificate and private key)
137/// used for client or server authentication
138#[derive(Debug, Clone)]
139#[cfg_attr(feature = "defmt", derive(defmt::Format))]
140pub struct Credentials<'a> {
141    /// Certificate (chain)
142    pub certificate: Certificate<'a>,
143    /// Private key paired with the certificate.
144    pub private_key: PrivateKey,
145}
146
147/// Configuration for a TLS session
148#[derive(Debug, Clone)]
149#[cfg_attr(feature = "defmt", derive(defmt::Format))]
150pub struct ClientSessionConfig<'a> {
151    /// Trusted CA (Certificate Authority) chain to be used for certificate
152    /// verification during the SSL/TLS handshake.
153    ///
154    /// The CA chain should contain the trusted CA certificates
155    /// that will be used to verify the server's certificate by the client during the handshake.
156    pub ca_chain: Option<Certificate<'a>>,
157    /// Optional client credentials used for authenticating the client to the server
158    pub creds: Option<Credentials<'a>>,
159    /// The server name to verify in the certificate provided by the server
160    /// Optional, because it can also be provided later
161    pub server_name: Option<&'a CStr>,
162    /// Certificate verification mode. Can be overriden.
163    /// By default, [AuthMode::Required] will be used
164    pub auth_mode: AuthMode,
165    /// The minimum TLS version that will be supported by a particular `Session` instance
166    pub min_version: TlsVersion,
167    /// ALPN protocols
168    pub alpn_protocols: Option<&'a [&'a CStr]>,
169}
170
171impl<'a> Default for ClientSessionConfig<'a> {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl<'a> ClientSessionConfig<'a> {
178    pub const fn new() -> Self {
179        Self {
180            ca_chain: None,
181            creds: None,
182            server_name: None,
183            auth_mode: AuthMode::Required,
184            min_version: TlsVersion::Tls1_2,
185            alpn_protocols: None,
186        }
187    }
188}
189
190#[derive(Debug, Clone)]
191#[cfg_attr(feature = "defmt", derive(defmt::Format))]
192pub struct ServerSessionConfig<'a> {
193    /// Trusted CA (Certificate Authority) chain to be used for certificate
194    /// verification during the SSL/TLS handshake.
195    ///
196    /// The CA chain should contain the trusted CA certificates
197    /// that will be used to verify the client's certificate by the server during the handshake.
198    pub ca_chain: Option<Certificate<'a>>,
199    /// Server credentials used for authenticating the server to the client
200    pub creds: Credentials<'a>,
201    /// Client certificate verification mode. Can be overriden.
202    /// By default, [AuthMode::None] will be used
203    pub auth_mode: AuthMode,
204    /// The minimum TLS version that will be supported by a particular `Session` instance
205    pub min_version: TlsVersion,
206    /// ALPN protocols
207    pub alpn_protocols: Option<&'a [&'a CStr]>,
208}
209
210impl<'a> ServerSessionConfig<'a> {
211    pub const fn new(creds: Credentials<'a>) -> Self {
212        Self {
213            ca_chain: None,
214            creds,
215            auth_mode: AuthMode::None,
216            min_version: TlsVersion::Tls1_2,
217            alpn_protocols: None,
218        }
219    }
220}
221
222/// Configuration for a TLS session
223#[derive(Debug, Clone)]
224#[cfg_attr(feature = "defmt", derive(defmt::Format))]
225pub enum SessionConfig<'a> {
226    Client(ClientSessionConfig<'a>),
227    Server(ServerSessionConfig<'a>),
228}
229
230impl<'a> SessionConfig<'a> {
231    fn ca_chain(&self) -> Option<&Certificate<'a>> {
232        match self {
233            SessionConfig::Client(ClientSessionConfig { ca_chain, .. }) => ca_chain.as_ref(),
234            SessionConfig::Server(ServerSessionConfig { ca_chain, .. }) => ca_chain.as_ref(),
235        }
236    }
237
238    fn creds(&self) -> Option<&Credentials<'a>> {
239        match self {
240            SessionConfig::Client(ClientSessionConfig { creds, .. }) => creds.as_ref(),
241            SessionConfig::Server(ServerSessionConfig { creds, .. }) => Some(creds),
242        }
243    }
244
245    fn auth_mode(&self) -> AuthMode {
246        match self {
247            SessionConfig::Client(ClientSessionConfig { auth_mode, .. }) => *auth_mode,
248            SessionConfig::Server(ServerSessionConfig { auth_mode, .. }) => *auth_mode,
249        }
250    }
251
252    fn min_version(&self) -> TlsVersion {
253        match self {
254            SessionConfig::Client(ClientSessionConfig { min_version, .. }) => *min_version,
255            SessionConfig::Server(ServerSessionConfig { min_version, .. }) => *min_version,
256        }
257    }
258
259    fn alpn_protocols(&self) -> Option<&'a [&'a CStr]> {
260        match self {
261            SessionConfig::Client(ClientSessionConfig { alpn_protocols, .. }) => *alpn_protocols,
262            SessionConfig::Server(ServerSessionConfig { alpn_protocols, .. }) => *alpn_protocols,
263        }
264    }
265
266    fn raw_mode(&self) -> c_int {
267        match self {
268            Self::Client { .. } => MBEDTLS_SSL_IS_CLIENT as c_int,
269            Self::Server { .. } => MBEDTLS_SSL_IS_SERVER as c_int,
270        }
271    }
272}
273
274/// RAII storage for an array of ALPN protocol names. Per mbedtls requirements,
275/// the array is always terminated with a NULL pointer. The array is allocated
276/// via mbedtls_calloc, but the pointers stored in the array refer to memory
277/// owned by the original CStr, hence the lifetime bound.
278#[derive(Debug)]
279#[cfg_attr(feature = "defmt", derive(defmt::Format))]
280struct ALPNArray<'a>(NonNull<*const c_char>, PhantomData<&'a CStr>);
281
282impl<'a> ALPNArray<'a> {
283    pub fn from_slice(slice: &'a [&'a CStr]) -> Option<Self> {
284        NonNull::new(
285            unsafe { mbedtls_calloc(slice.len() + 1, size_of::<*const c_char>()) }
286                .cast::<*const c_char>(),
287        )
288        .map(|ptr| {
289            // we allocate the memory via calloc, so it is zero-filled
290            // we fill all the entries excluding the null terminator
291            let output = unsafe { core::slice::from_raw_parts_mut(ptr.as_ptr(), slice.len()) };
292            for (index, element) in slice.iter().enumerate() {
293                output[index] = element.as_ptr()
294            }
295            Self(ptr, PhantomData)
296        })
297    }
298
299    pub fn as_ptr(&self) -> *mut *const c_char {
300        self.0.as_ptr()
301    }
302}
303
304impl<'a> Drop for ALPNArray<'a> {
305    fn drop(&mut self) {
306        unsafe {
307            mbedtls_free(self.0.as_ptr() as *mut c_void);
308        }
309    }
310}
311
312/// Session state
313struct SessionState<'a> {
314    /// The SSL context
315    ssl_context: MBox<mbedtls_ssl_context>,
316    /// The DRBG context
317    ///
318    /// While not explicitly used, we need to keep a reference to it as it is used
319    /// by the SSL context via a raw pointer
320    _drbg: MBox<mbedtls_ctr_drbg_context>,
321    /// The SSL configuration
322    ///
323    /// While not explicitly used, we need to keep a reference to it as it is used
324    /// by the SSL context via a raw pointer
325    _ssl_config: MBox<mbedtls_ssl_config>,
326    /// The CA chain
327    ///
328    /// While not explicitly used, we need to keep a reference to it as it is used
329    /// by the SSL context via a raw pointer
330    _ca_chain: Option<Certificate<'a>>,
331    /// The credentials
332    ///
333    /// While not explicitly used, we need to keep a reference to it as it is used
334    /// by the SSL context via a raw pointer
335    _creds: Option<Credentials<'a>>,
336    /// ALPN protocol array
337    ///
338    /// While not explicitly used, we need to keep a reference to it as it is used
339    /// by the SSL context via a raw pointer
340    _alpn_ptrs: Option<ALPNArray<'a>>,
341}
342
343impl<'a> SessionState<'a> {
344    /// Initialize the Session state using the given configuration
345    fn new(conf: &SessionConfig<'a>) -> Result<Self, MbedtlsError> {
346        merr!(unsafe { psa_crypto_init() })?;
347
348        let mut ssl_config = MBox::new().ok_or(MbedtlsError::new(MBEDTLS_ERR_SSL_ALLOC_FAILED))?;
349
350        merr!(unsafe {
351            mbedtls_ssl_config_defaults(
352                &mut *ssl_config,
353                conf.raw_mode(),
354                MBEDTLS_SSL_TRANSPORT_STREAM as i32,
355                MBEDTLS_SSL_PRESET_DEFAULT as i32,
356            )
357        })?;
358
359        // Set the minimum TLS version
360        // Use a direct field modified for compatibility with the `esp-idf-svc` mbedtls
361        ssl_config.private_min_tls_version = conf.min_version().mbed_tls_version();
362
363        Tls::hook_debug_logs(&mut ssl_config);
364
365        unsafe {
366            mbedtls_ssl_conf_authmode(&mut *ssl_config, conf.auth_mode().mbedtls_authmode());
367        }
368
369        if let Some(creds) = conf.creds() {
370            merr!(unsafe {
371                mbedtls_ssl_conf_own_cert(
372                    &mut *ssl_config,
373                    &*creds.certificate.crt as *const _ as *mut _,
374                    &*creds.private_key.0 as *const _ as *mut _,
375                )
376            })?;
377        }
378
379        if let Some(ca_chain) = conf.ca_chain() {
380            unsafe {
381                mbedtls_ssl_conf_ca_chain(
382                    &mut *ssl_config,
383                    &*ca_chain.crt as *const _ as *mut _,
384                    core::ptr::null_mut(),
385                );
386            }
387        }
388
389        let alpn = if let Some(alpn_protocols) = conf.alpn_protocols() {
390            let alpn = ALPNArray::from_slice(alpn_protocols)
391                .ok_or(MbedtlsError::new(MBEDTLS_ERR_SSL_ALLOC_FAILED))?;
392            merr!(unsafe { mbedtls_ssl_conf_alpn_protocols(&mut *ssl_config, alpn.as_ptr()) })?;
393            Some(alpn)
394        } else {
395            None
396        };
397
398        let mut drbg_context =
399            MBox::new().ok_or(MbedtlsError::new(MBEDTLS_ERR_SSL_ALLOC_FAILED))?;
400
401        // Init RNG
402        unsafe {
403            mbedtls_ssl_conf_rng(
404                &mut *ssl_config,
405                Some(mbedtls_rng),
406                &mut *drbg_context as *mut _ as *mut c_void,
407            );
408        }
409
410        let mut ssl_context = MBox::new().ok_or(MbedtlsError::new(MBEDTLS_ERR_SSL_ALLOC_FAILED))?;
411
412        merr!(unsafe { mbedtls_ssl_setup(&mut *ssl_context, &*ssl_config) })?;
413
414        if let SessionConfig::Client(conf) = conf {
415            if let Some(name) = conf.server_name {
416                merr!(unsafe { mbedtls_ssl_set_hostname(&mut *ssl_context, name.as_ptr()) })?;
417            }
418        }
419
420        Ok(Self {
421            ssl_context,
422            _drbg: drbg_context,
423            _ssl_config: ssl_config,
424            _ca_chain: conf.ca_chain().cloned(),
425            _creds: conf.creds().cloned(),
426            _alpn_ptrs: alpn,
427        })
428    }
429}
430
431/// Error type for session operations
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum SessionError {
434    /// MBedTLS error
435    MbedTls(MbedtlsError),
436    /// IO error
437    Io(ErrorKind),
438}
439
440impl SessionError {
441    /// Create a SessionError from an embedded-io Error
442    pub fn from_io<E: Error>(err: E) -> Self {
443        Self::Io(err.kind())
444    }
445}
446
447impl From<MbedtlsError> for SessionError {
448    fn from(e: MbedtlsError) -> Self {
449        Self::MbedTls(e)
450    }
451}
452
453impl From<ErrorKind> for SessionError {
454    fn from(e: ErrorKind) -> Self {
455        Self::Io(e)
456    }
457}
458
459impl core::fmt::Display for SessionError {
460    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
461        match self {
462            Self::MbedTls(e) => write!(f, "{}", e),
463            Self::Io(e) => write!(f, "IO({:?})", e),
464        }
465    }
466}
467
468#[cfg(feature = "defmt")]
469impl defmt::Format for SessionError {
470    fn format(&self, f: defmt::Formatter<'_>) {
471        match self {
472            Self::MbedTls(e) => defmt::write!(f, "{}", e),
473            Self::Io(e) => defmt::write!(f, "IO({:?})", debug2format!(e)),
474        }
475    }
476}
477
478impl core::error::Error for SessionError {}
479
480impl embedded_io::Error for SessionError {
481    fn kind(&self) -> embedded_io::ErrorKind {
482        match self {
483            Self::Io(e) => *e,
484            _ => embedded_io::ErrorKind::Other,
485        }
486    }
487}