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}