Skip to main content

mbedtls_rs/
lib.rs

1#![no_std]
2#![allow(clippy::uninlined_format_args)]
3
4use core::cell::RefCell;
5use core::ffi::{c_char, c_int, c_uchar, c_void, CStr};
6use core::marker::PhantomData;
7use core::mem::size_of;
8use core::ops::{Deref, DerefMut};
9use core::ptr::NonNull;
10
11use critical_section::Mutex;
12
13#[cfg(not(target_os = "espidf"))]
14pub(crate) use crate::sys::{mbedtls_calloc, mbedtls_free};
15use crate::sys::{
16    mbedtls_ctr_drbg_context, mbedtls_ctr_drbg_free, mbedtls_ctr_drbg_init, mbedtls_pk_context,
17    mbedtls_pk_free, mbedtls_pk_init, mbedtls_ssl_conf_dbg, mbedtls_ssl_config,
18    mbedtls_ssl_config_free, mbedtls_ssl_config_init, mbedtls_ssl_context, mbedtls_ssl_free,
19    mbedtls_ssl_init, mbedtls_ssl_protocol_version,
20    mbedtls_ssl_protocol_version_MBEDTLS_SSL_VERSION_TLS1_2,
21    mbedtls_ssl_protocol_version_MBEDTLS_SSL_VERSION_TLS1_3, mbedtls_ssl_session,
22    mbedtls_ssl_session_free, mbedtls_ssl_session_init, mbedtls_x509_crt, mbedtls_x509_crt_free,
23    mbedtls_x509_crt_init,
24};
25
26use rand_core::CryptoRng;
27
28pub use cert::*;
29pub use session::*;
30
31pub(crate) mod fmt; // MUST be the first so that the other modules can see it
32
33mod cert;
34mod session;
35
36/// Re-export of the mbedtls-rs-sys crate so that users do not have to
37/// explicitly depend on it if they want to use the raw MbedTLS bindings.
38pub mod sys {
39    pub use mbedtls_rs_sys::*;
40}
41
42/// An erased pointer to the user-provided RNG, stored in the global [`RNG`].
43///
44/// The RNG is stored as a raw `NonNull` rather than a reference: a pointer
45/// makes no aliasing/validity claim, and the `&mut` is materialized only inside
46/// the `mbedtls_rng` callback. [`Tls::new`] takes a `&'static mut`, so its
47/// stored pointer is always valid. [`Tls::new_local_borrows`] takes a shorter
48/// `&'d mut` and is `unsafe` precisely because of this slot: if that borrow ends
49/// while the pointer is still installed (only reachable by leaking the `Tls`),
50/// a later callback dereference would be UB. See `new_local_borrows`' safety
51/// contract.
52struct RngPtr(NonNull<dyn CryptoRng + Send>);
53
54// SAFETY: the pointee is `Send` (the trait object bound), and all access is
55// serialized through the `critical_section` `Mutex` below. The pointer is set
56// from a live `&mut` in a `Tls` constructor and cleared in `Tls::drop`.
57unsafe impl Send for RngPtr {}
58
59static RNG: Mutex<RefCell<Option<RngPtr>>> = Mutex::new(RefCell::new(None));
60
61/// An error returned when creating a `Tls` instance
62#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
63#[cfg_attr(feature = "defmt", derive(defmt::Format))]
64pub enum TlsError {
65    AlreadyCreated,
66}
67
68/// A TLS instance
69///
70/// Represents an instance of the MbedTLS library.
71/// Only one such instance can be active at any point in time.
72pub struct Tls<'d>(PhantomData<&'d mut ()>);
73
74impl Tls<'static> {
75    /// Create a new instance of the `Tls` type from a `'static` RNG.
76    ///
77    /// Note that there could be only one active `Tls` instance at any point in
78    /// time, and the function will return an error if there is already an active
79    /// instance.
80    ///
81    /// This is safe because the RNG borrow is `'static`: it stays valid for the
82    /// rest of the program, so even leaking the returned `Tls` (e.g. via
83    /// `core::mem::forget`) cannot leave the global RNG slot dangling. For a
84    /// shorter-lived borrow, see [`Tls::new_local_borrows`].
85    pub fn new(rng: &'static mut (dyn CryptoRng + Send)) -> Result<Self, TlsError> {
86        // No `unsafe`: a `&'static mut` is already valid for the program's
87        // lifetime, so `NonNull::from` erases nothing the caller could invalidate.
88        Self::store_rng(NonNull::from(rng))
89    }
90}
91
92impl<'d> Tls<'d> {
93    /// Create a new instance of the `Tls` type from a non-`'static` RNG.
94    ///
95    /// Note that there could be only one active `Tls` instance at any point in
96    /// time, and the function will return an error if there is already an active
97    /// instance.
98    ///
99    /// Prefer [`Tls::new`] with a `'static` RNG where possible; this variant
100    /// exists for borrowed RNGs (e.g. a hardware TRNG peripheral) that cannot be
101    /// `'static`.
102    ///
103    /// # Safety
104    /// The RNG is stored behind a lifetime-erased pointer for as long as the
105    /// global RNG slot is installed (until this `Tls` is dropped). The caller
106    /// must ensure the returned `Tls` is dropped - NOT leaked via
107    /// `core::mem::forget` / `ManuallyDrop` - before `rng`'s borrow ends, and
108    /// must not combine a leaked `Tls` with direct calls into the raw
109    /// `mbedtls-rs-sys` RNG/PSA bindings; otherwise a later callback may
110    /// dereference a dangling pointer. Using only the safe `Session` API upholds
111    /// this automatically (a `Session` keeps the `Tls` alive via `TlsReference`).
112    pub unsafe fn new_local_borrows(rng: &'d mut (dyn CryptoRng + Send)) -> Result<Self, TlsError> {
113        // SAFETY: erase only the pointee lifetime on a *pointer* (ptr -> ptr, no
114        // validity claim); the caller upholds the no-leak contract documented
115        // above. The transmute preserves the data pointer and vtable.
116        let rng = unsafe {
117            core::mem::transmute::<NonNull<dyn CryptoRng + Send + '_>, NonNull<dyn CryptoRng + Send>>(
118                NonNull::from(rng),
119            )
120        };
121        Self::store_rng(rng)
122    }
123}
124
125impl<'d> Tls<'d> {
126    /// Install the erased RNG pointer into the global slot, enforcing the
127    /// single-active-instance invariant. Safe: storing a `NonNull` asserts
128    /// nothing about the pointee; validity is the contract of whichever
129    /// constructor produced the pointer.
130    fn store_rng(rng: NonNull<dyn CryptoRng + Send>) -> Result<Self, TlsError> {
131        critical_section::with(|cs| {
132            if RNG.borrow(cs).borrow().is_some() {
133                return Err(TlsError::AlreadyCreated);
134            }
135
136            *RNG.borrow(cs).borrow_mut() = Some(RngPtr(rng));
137
138            Ok(Self(PhantomData))
139        })
140    }
141
142    pub(crate) fn release(&mut self) {
143        critical_section::with(|cs| {
144            *RNG.borrow(cs).borrow_mut() = None;
145        });
146    }
147
148    /// Set the MbedTLS debug level (0 - 5).
149    ///
150    /// No-op unless the `tls-debug` feature is enabled (the `tls`/`openthread`
151    /// bundles enable it) so `MBEDTLS_DEBUG_C` is compiled in; also a no-op on
152    /// ESP-IDF.
153    #[allow(unused)]
154    pub fn set_debug(&mut self, level: u32) {
155        #[cfg(all(not(target_os = "espidf"), feature = "tls-debug"))]
156        // SAFETY: this block is compiled only when `tls-debug` is enabled, which turns on
157        // `mbedtls-rs-sys/tls-debug` (MBEDTLS_DEBUG_C) so `mbedtls_debug_set_threshold` is
158        // defined and linked; the call passes one `c_int` by value (no pointers/aliasing).
159        unsafe {
160            use crate::sys::mbedtls_debug_set_threshold;
161
162            mbedtls_debug_set_threshold(level as c_int);
163        }
164    }
165
166    /// Get a reference to the `Tls` instance
167    ///
168    /// Each `Session` needs a reference to (the) active `Tls` instance
169    /// throughout its lifetime.
170    pub fn reference(&self) -> TlsReference<'_> {
171        TlsReference(PhantomData)
172    }
173
174    /// Hook MbedTLS SSL debug logging into the Rust log system
175    ///
176    /// # Arguments
177    /// - `ssl_config`: The MbedTLS SSL configuration to hook the debug logging into
178    pub(crate) fn hook_debug_logs(ssl_config: &mut mbedtls_ssl_config) {
179        /// Output the MbedTLS debug messages to the log
180        #[no_mangle]
181        unsafe extern "C" fn mbedtls_dbg_print(
182            _arg: *mut c_void,
183            lvl: i32,
184            file: *const c_char,
185            line: i32,
186            msg: *const c_char,
187        ) {
188            let file = CStr::from_ptr(file);
189            let msg = CStr::from_ptr(msg);
190
191            let file = file.to_str().unwrap_or("???").trim();
192            let msg = msg.to_str().unwrap_or("???").trim();
193
194            match lvl {
195                0 => warn!("(MbedTLS) {} (at {}:{})", msg, file, line),
196                1 => info!("(MbedTLS) {} (at {}:{})", msg, file, line),
197                2 => debug!("(MbedTLS) {} (at {}:{})", msg, file, line),
198                _ => trace!("(MbedTLS) {} (at {}:{})", msg, file, line),
199            }
200        }
201
202        unsafe {
203            mbedtls_ssl_conf_dbg(
204                &mut *ssl_config,
205                Some(mbedtls_dbg_print),
206                core::ptr::null_mut(),
207            );
208        }
209    }
210}
211
212impl<'d> Drop for Tls<'d> {
213    fn drop(&mut self) {
214        self.release();
215    }
216}
217
218/// A reference to (the) active `Tls` instance
219///
220/// Used instead of just `&'a Tls` so that the invariant `'d` lifetime of the `Tls` instance
221/// is not exposed in the `Session` type.
222#[allow(unused)]
223#[derive(Debug, Copy, Clone)]
224#[cfg_attr(feature = "defmt", derive(defmt::Format))]
225pub struct TlsReference<'a>(PhantomData<&'a ()>);
226
227/// The minimum TLS version that will be supported by a particular `Session` instance
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
229#[cfg_attr(feature = "defmt", derive(defmt::Format))]
230pub enum TlsVersion {
231    /// TLS 1.2
232    Tls1_2,
233    /// TLS 1.3
234    Tls1_3,
235}
236
237impl TlsVersion {
238    fn mbed_tls_version(&self) -> mbedtls_ssl_protocol_version {
239        match self {
240            TlsVersion::Tls1_2 => mbedtls_ssl_protocol_version_MBEDTLS_SSL_VERSION_TLS1_2,
241            TlsVersion::Tls1_3 => mbedtls_ssl_protocol_version_MBEDTLS_SSL_VERSION_TLS1_3,
242        }
243    }
244}
245
246/// An internal trait to be implemented on MbedTLS structures.
247///
248/// The trait models the initialization and deinitialization
249/// sequence available on a number of MBedTLS structures.
250trait MInit {
251    /// Initialize the structure
252    fn init(&mut self) {}
253
254    /// Deinitialize the structure
255    fn deinit(&mut self) {}
256}
257
258impl MInit for mbedtls_ctr_drbg_context {
259    fn init(&mut self) {
260        unsafe {
261            mbedtls_ctr_drbg_init(self);
262        }
263    }
264
265    fn deinit(&mut self) {
266        unsafe {
267            mbedtls_ctr_drbg_free(self);
268        }
269    }
270}
271
272impl MInit for mbedtls_ssl_context {
273    fn init(&mut self) {
274        unsafe {
275            mbedtls_ssl_init(self);
276        }
277    }
278
279    fn deinit(&mut self) {
280        unsafe {
281            mbedtls_ssl_free(self);
282        }
283    }
284}
285
286impl MInit for mbedtls_ssl_config {
287    fn init(&mut self) {
288        unsafe {
289            mbedtls_ssl_config_init(self);
290        }
291    }
292
293    fn deinit(&mut self) {
294        unsafe {
295            mbedtls_ssl_config_free(self);
296        }
297    }
298}
299
300impl MInit for mbedtls_ssl_session {
301    fn init(&mut self) {
302        unsafe {
303            mbedtls_ssl_session_init(self);
304        }
305    }
306
307    fn deinit(&mut self) {
308        unsafe {
309            mbedtls_ssl_session_free(self);
310        }
311    }
312}
313
314impl MInit for mbedtls_x509_crt {
315    fn init(&mut self) {
316        unsafe {
317            mbedtls_x509_crt_init(self);
318        }
319    }
320
321    fn deinit(&mut self) {
322        unsafe {
323            mbedtls_x509_crt_free(self);
324        }
325    }
326}
327
328impl MInit for mbedtls_pk_context {
329    fn init(&mut self) {
330        unsafe {
331            mbedtls_pk_init(self);
332        }
333    }
334
335    fn deinit(&mut self) {
336        unsafe {
337            mbedtls_pk_free(self);
338        }
339    }
340}
341
342/// A uniquely-owned box-like wrapper type for MbedTLS structures that need to be allocated/deallocated
343/// using `mbedtls_calloc`/`mbedtls_free`, and initialized/deinitialized using the `MInit` trait
344#[derive(Debug)]
345#[cfg_attr(feature = "defmt", derive(defmt::Format))]
346struct MBox<T>(NonNull<T>)
347where
348    T: MInit;
349
350impl<T> MBox<T>
351where
352    T: MInit,
353{
354    /// Create a new MBox
355    ///
356    /// # Returns
357    /// - Ok(MBox<T>) if the allocation was successful
358    /// - Err(TlsError::OutOfMemory) if the allocation failed
359    fn new() -> Option<Self> {
360        NonNull::new(unsafe { mbedtls_calloc(1, size_of::<T>()) }.cast::<T>()).map(|mut ptr| {
361            unsafe { ptr.as_mut() }.init();
362
363            Self(ptr)
364        })
365    }
366
367    /// Get a reference to the inner value
368    fn as_ref(&self) -> &T {
369        unsafe { self.0.as_ref() }
370    }
371
372    /// Get a mutable reference to the inner value
373    fn as_mut(&mut self) -> &mut T {
374        unsafe { self.0.as_mut() }
375    }
376
377    /// Get the raw pointer to the inner value.
378    ///
379    /// The pointer is the original allocation pointer preserved in the
380    /// `NonNull` (not derived from a Rust reference), so writing through it
381    /// (e.g. by C code via FFI) is sound. Taking `&mut self` ensures the caller
382    /// is not simultaneously holding a shared reference to the same object via
383    /// `Deref`. Used by the async session path, which must hand MbedTLS a `*mut`
384    /// it can write through.
385    fn as_mut_ptr(&mut self) -> *mut T {
386        self.0.as_ptr()
387    }
388}
389
390impl<T> Deref for MBox<T>
391where
392    T: MInit,
393{
394    type Target = T;
395
396    fn deref(&self) -> &Self::Target {
397        self.as_ref()
398    }
399}
400
401impl<T> DerefMut for MBox<T>
402where
403    T: MInit,
404{
405    fn deref_mut(&mut self) -> &mut Self::Target {
406        self.as_mut()
407    }
408}
409
410impl<T> Drop for MBox<T>
411where
412    T: MInit,
413{
414    fn drop(&mut self) {
415        self.as_mut().deinit();
416
417        unsafe {
418            mbedtls_free(self.0.as_ptr() as *mut c_void);
419        }
420    }
421}
422
423/// A reference-counted `Rc`-like wrapper type for MbedTLS structures that need to be allocated/deallocated
424/// using `mbedtls_calloc`/`mbedtls_free`, and initialized/deinitialized using the `MInit` trait
425#[derive(Debug)]
426#[cfg_attr(feature = "defmt", derive(defmt::Format))]
427struct MRc<T>(NonNull<(T, usize)>)
428where
429    T: MInit;
430
431impl<T> MRc<T>
432where
433    T: MInit,
434{
435    /// Create a new MRc
436    fn new() -> Option<Self> {
437        NonNull::new(unsafe { mbedtls_calloc(1, size_of::<(T, usize)>()) }.cast::<(T, usize)>())
438            .map(|mut ptr| {
439                let this = unsafe { ptr.as_mut() };
440
441                this.0.init();
442                this.1 = 1;
443
444                Self(ptr)
445            })
446    }
447
448    /// Get a reference to the inner value
449    fn as_ref(&self) -> &T {
450        &unsafe { self.0.as_ref() }.0
451    }
452
453    /// Get a raw pointer to the inner value, with write provenance.
454    ///
455    /// The pointer is projected from the original `(T, usize)` allocation
456    /// pointer in the `NonNull` (not derived from a Rust reference), so writing
457    /// through it (e.g. by C code via FFI) is sound. `addr_of_mut!` projects the
458    /// `.0` field without ever forming an intermediate reference, and must be
459    /// used rather than `self.0.as_ptr().cast::<T>()` because the field order of
460    /// the `(T, usize)` tuple is not guaranteed.
461    ///
462    /// SAFETY: unlike `MBox`, `MRc` is `Clone`, so `&mut self` alone does not
463    /// guarantee unique access to the inner `T` (a clone could be reading it).
464    /// The caller must ensure no other live reference to the inner `T` exists
465    /// for the duration of the write. This holds at construction, where the
466    /// `MRc` is freshly allocated, has refcount 1 and has not yet been cloned -
467    /// which is the only context this is used in. Writing through this pointer
468    /// while a clone concurrently dereferences the value would be UB.
469    fn as_mut_ptr(&mut self) -> *mut T {
470        unsafe { core::ptr::addr_of_mut!((*self.0.as_ptr()).0) }
471    }
472}
473
474impl<T> Clone for MRc<T>
475where
476    T: MInit,
477{
478    fn clone(&self) -> Self {
479        let mut ptr = self.0;
480
481        unsafe { ptr.as_mut() }.1 += 1;
482
483        Self(ptr)
484    }
485}
486
487impl<T> Deref for MRc<T>
488where
489    T: MInit,
490{
491    type Target = T;
492
493    fn deref(&self) -> &Self::Target {
494        self.as_ref()
495    }
496}
497
498impl<T> Drop for MRc<T>
499where
500    T: MInit,
501{
502    fn drop(&mut self) {
503        unsafe { self.0.as_mut() }.1 -= 1;
504
505        if unsafe { self.0.as_mut() }.1 == 0 {
506            unsafe { self.0.as_mut() }.0.deinit();
507
508            unsafe {
509                mbedtls_free(self.0.as_ptr() as *mut c_void);
510            }
511        }
512    }
513}
514
515pub(crate) unsafe extern "C" fn mbedtls_rng(
516    _param: *mut c_void,
517    buf: *mut c_uchar,
518    len: usize,
519) -> c_int {
520    use crate::sys::MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED;
521
522    if len == 0 {
523        return 0;
524    }
525    if buf.is_null() {
526        return MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED;
527    }
528
529    let buf = core::slice::from_raw_parts_mut(buf, len);
530
531    critical_section::with(|cs| {
532        match RNG.borrow(cs).borrow_mut().as_mut() {
533            // SAFETY: the pointer was set from a live `&mut` in a `Tls`
534            // constructor (`store_rng`) and is cleared in `Tls::drop`, so on the
535            // normal path (a live `Tls`) it is valid; access is serialized by the
536            // surrounding `Mutex`. The one unsound path is a leaked `Tls` created
537            // via `new_local_borrows` (see `RngPtr`): if the owner did
538            // `mem::forget` and dropped the RNG, this slot is stale. That is a
539            // caller contract, not something this callback can detect.
540            Some(rng) => {
541                rng.0.as_mut().fill_bytes(buf);
542                0
543            }
544            // No `Tls` is active: report an entropy failure rather than panicking
545            // (the old `unwrap()` aborted out of this `extern "C"` callback). This
546            // path is reachable via `mbedtls_psa_external_get_random`, which the
547            // PSA layer can call with no live `Session`.
548            None => MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED,
549        }
550    })
551}
552
553#[no_mangle]
554unsafe extern "C" fn mbedtls_psa_external_get_random(
555    _ctx: *mut (),
556    output: *mut c_uchar,
557    out_size: usize,
558    output_len: *mut usize,
559) -> c_int {
560    // PSA status codes (`psa_status_t`). MbedTLS treats this hook's return as a
561    // PSA status, not an `MBEDTLS_ERR_*` code, and the generated bindings do not
562    // expose the `PSA_*` macros, so they are defined locally here.
563    const PSA_SUCCESS: c_int = 0;
564    const PSA_ERROR_INSUFFICIENT_ENTROPY: c_int = -148;
565    const PSA_ERROR_INVALID_ARGUMENT: c_int = -135;
566
567    if output_len.is_null() {
568        return PSA_ERROR_INVALID_ARGUMENT;
569    }
570    // A zero-size request may legitimately pass a null `output`.
571    if output.is_null() && out_size != 0 {
572        return PSA_ERROR_INVALID_ARGUMENT;
573    }
574    if out_size == 0 {
575        *output_len = 0;
576        return PSA_SUCCESS;
577    }
578
579    if mbedtls_rng(core::ptr::null_mut(), output, out_size) == 0 {
580        *output_len = out_size;
581        PSA_SUCCESS
582    } else {
583        PSA_ERROR_INSUFFICIENT_ENTROPY
584    }
585}
586
587#[cfg(target_os = "espidf")]
588extern "C" {
589    #[link_name = "calloc"]
590    pub(crate) fn mbedtls_calloc(num: usize, size: usize) -> *mut c_void;
591    #[link_name = "free"]
592    pub(crate) fn mbedtls_free(ptr: *mut c_void);
593}