embedded_mbedtls/
ssl.rs

1// Copyright Open Logistics Foundation
2//
3// Licensed under the Open Logistics Foundation License 1.3.
4// For details on the licensing terms, see the LICENSE file.
5// SPDX-License-Identifier: OLFL-1.3
6
7//! TLS and DTLS interface module which defines the [`SslConnection`] type which is the main type
8//! to interact with this library and the underlying [`SslContext`] type which contains all
9//! hardware abstractions
10
11#[cfg(feature = "alloc")]
12use alloc::boxed::Box;
13
14use core::ops::DerefMut;
15use core::time::Duration;
16
17use core::ffi::{self, c_void};
18use core::net::SocketAddr;
19use embedded_mbedtls_sys::{
20    mbedtls_ssl_config, mbedtls_ssl_config_init, mbedtls_ssl_context, mbedtls_ssl_init,
21    MBEDTLS_ERR_SSL_WANT_READ, MBEDTLS_ERR_SSL_WANT_WRITE,
22};
23use embedded_nal::UdpClientStack;
24use embedded_timers::clock::Clock;
25use rand_core::{CryptoRng, RngCore};
26
27use crate::{error::Error, rng::rng_try_fill_bytes_callback_fn, timing, udp};
28
29/// Hardware context of an [`SslConnection`]
30///
31/// The context contains the underlying network stack, Clock and RNG context. This type needs to be
32/// defined separately from the `SslConnection` because it needs a pinned memory location so that
33/// raw pointers can be passed to the underlying C functions.
34pub struct SslContext<'a, Net, C: Clock, R: RngCore + CryptoRng> {
35    config: mbedtls_ssl_config,
36    net_context: Net,
37    timer_context: timing::MbedtlsTimer<'a, C>,
38    csrng: R,
39}
40
41impl<'a, U: UdpClientStack, C: Clock, R: RngCore + CryptoRng>
42    SslContext<'a, udp::UdpContext<U>, C, R>
43{
44    /// Create a new `SslContext` for client-side DTLS
45    pub fn new_udp_client_side(
46        net_stack: U,
47        clock: &'a C,
48        csrng: R,
49        server_addr: SocketAddr,
50    ) -> Self {
51        let mut config = mbedtls_ssl_config::default();
52        unsafe { mbedtls_ssl_config_init(&mut config) };
53        let net_context = udp::UdpContext::new(net_stack, server_addr);
54        let timer_context = timing::MbedtlsTimer::new(clock);
55        SslContext {
56            config,
57            net_context,
58            timer_context,
59            csrng,
60        }
61    }
62}
63
64impl<'a, Net, C: Clock, R: RngCore + CryptoRng> Drop for SslContext<'a, Net, C, R> {
65    fn drop(&mut self) {
66        unsafe {
67            embedded_mbedtls_sys::mbedtls_ssl_config_free(&mut self.config);
68        }
69    }
70}
71
72/// An SSL connection, i.e. the main type to interact with this library
73///
74/// To set up the underlying Mbed TLS C library, we need raw pointers so the [`SslContext`]
75/// needs a pinned/stable memory location. Basically, we use two approaches to achieve this:
76/// 1. We use a `&'a mut SslContext<...>` for the lifetime of the `SslConnection`, i.e. the
77///    underlying `SslContext` can not move for that lifetime. This is implemented in e.g.
78///    [`SslConnection::new_dtls_client`]. This approach has the benefit of being heapless (it does
79///    not require `alloc`) which is often desirable for embedded devices.
80/// 2. We use a `Box<SslConnection<...>>`, i.e. the `SslContext` lies on the heap so it does not
81///    move in memory anymore. This is implemented in e.g.
82///    `SslConnection::new_dtls_client_heap_context` which is only available when the `alloc`
83///    feature is activated. This approach has the benefit that the `SslConnection` may be set up
84///    in an initializer function and can be moved around freely afterwards.
85pub struct SslConnection<
86    'a,
87    Net,
88    C: Clock + 'a,
89    R: RngCore + CryptoRng,
90    CTX: DerefMut<Target = SslContext<'a, Net, C, R>>,
91> {
92    mbedtls_ctx: mbedtls_ssl_context,
93    ssl_ctx: CTX,
94}
95
96impl<'a, 'b: 'a, U: UdpClientStack, C: Clock, R: RngCore + CryptoRng>
97    SslConnection<'b, udp::UdpContext<U>, C, R, &'a mut SslContext<'b, udp::UdpContext<U>, C, R>>
98{
99    /// Create a new DTLS `SslConnection` from an [`SslContext`]
100    ///
101    /// When the `alloc` feature is activated, the `new_dtls_client_heap_context` constructor
102    /// can be used with a _boxed_ context.
103    /// This enables you to freely move the connection together with the context.
104    /// (otherwise the context can't be moved after the connection is created,
105    /// because its address needs to be pinned in either case)
106    ///
107    /// ```
108    /// # use core::net::{IpAddr, Ipv4Addr, SocketAddr};
109    /// # use embedded_nal::UdpClientStack;
110    /// # use embedded_timers::clock::Clock;
111    /// # use rand_core::{CryptoRng, RngCore};
112    /// #
113    /// #
114    /// use embedded_mbedtls::ssl::{SslConnection, SslContext, Preset};
115    ///
116    /// # fn _setup_ssl_stack<U: UdpClientStack, R: RngCore + CryptoRng>(
117    /// #     net_stack: U,
118    /// #     clock: &impl Clock,
119    /// #     rng: R,
120    /// # ) {
121    /// #    let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 22);
122    /// let mut ctx = SslContext::new_udp_client_side(net_stack, clock, rng, server_addr);
123    /// let connection = SslConnection::new_dtls_client(&mut ctx, Preset::Default).unwrap();
124    ///
125    /// // Now the connection is ready to use!
126    /// # }
127    /// ```
128    pub fn new_dtls_client(
129        ssl_context: &'a mut SslContext<'b, udp::UdpContext<U>, C, R>,
130        preset: Preset,
131    ) -> Result<Self, Error> {
132        Self::new_generic_dtls_client(ssl_context, preset)
133    }
134}
135
136#[cfg(feature = "alloc")]
137impl<'a, U: UdpClientStack, C: Clock, R: RngCore + CryptoRng>
138    SslConnection<'a, udp::UdpContext<U>, C, R, Box<SslContext<'a, udp::UdpContext<U>, C, R>>>
139{
140    /// Create a new DTLS `SslConnection`, moving the [`SslContext`] into a `Box`
141    ///
142    /// This allows to move the [`SslConnection`] instance freely. Especially, it allows to return
143    /// from an initializer function in which the `SslContext` was set up.
144    ///
145    /// ```
146    /// # type _BoxedDtls<'a, U, C, R> = SslConnection<
147    /// #     'a,
148    /// #     embedded_mbedtls::udp::UdpContext<U>,
149    /// #     C,
150    /// #     R,
151    /// #     Box<SslContext<'a, embedded_mbedtls::udp::UdpContext<U>, C, R>>,
152    /// # >;
153    /// # use core::net::{IpAddr, Ipv4Addr, SocketAddr};
154    /// # use embedded_nal::UdpClientStack;
155    /// # use embedded_timers::clock::Clock;
156    /// # use rand_core::{CryptoRng, RngCore};
157    /// #
158    /// #
159    /// use embedded_mbedtls::ssl::{SslConnection, SslContext, Preset};
160    ///
161    /// # fn _setup_ssl_heap<'a, U: UdpClientStack, C: Clock, R: RngCore + CryptoRng>(
162    /// #     net_stack: U,
163    /// #     clock: &'a C,
164    /// #     rng: R,
165    /// # ) -> _BoxedDtls<'a, U, C, R> {
166    /// #     let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 22);
167    /// #
168    /// let ctx = SslContext::new_udp_client_side(net_stack, clock, rng, server_addr);
169    /// let connection = SslConnection::new_dtls_client_heap_context(ctx, Preset::Default).unwrap();
170    ///
171    /// // now the connection can be moved freely:
172    /// return connection
173    /// # }
174    /// ```
175    pub fn new_dtls_client_heap_context(
176        ssl_context: SslContext<'a, udp::UdpContext<U>, C, R>,
177        preset: Preset,
178    ) -> Result<Self, Error> {
179        Self::new_generic_dtls_client(Box::new(ssl_context), preset)
180    }
181}
182
183impl<
184        'a,
185        U: UdpClientStack,
186        C: Clock,
187        R: RngCore + CryptoRng,
188        CTX: DerefMut<Target = SslContext<'a, udp::UdpContext<U>, C, R>>,
189    > SslConnection<'a, udp::UdpContext<U>, C, R, CTX>
190{
191    /// Create a new DTLS `SslConnection` from an [`SslContext`]
192    ///
193    /// _Note_: Internal function which should __not__ be made public.
194    /// With this function the `ssl_context` is only constrained with by `DerefMut<Target = SslContext<...>>`,
195    /// which makes it possible to pass a type which implements DerefMut itself and won't guarantee
196    /// that the `ssl_context` is never moved. This is crucial since the C parts hold raw pointers
197    /// to members of [`SslContext`].
198    fn new_generic_dtls_client(ssl_context: CTX, preset: Preset) -> Result<Self, Error> {
199        let mut context = mbedtls_ssl_context::default();
200        unsafe { mbedtls_ssl_init(&mut context) };
201
202        let mut this = SslConnection {
203            mbedtls_ctx: context,
204            ssl_ctx: ssl_context,
205        };
206
207        use embedded_mbedtls_sys::MBEDTLS_SSL_IS_CLIENT;
208        use embedded_mbedtls_sys::MBEDTLS_SSL_TRANSPORT_DATAGRAM;
209        let ret = unsafe {
210            embedded_mbedtls_sys::mbedtls_ssl_config_defaults(
211                &mut this.ssl_ctx.config as *mut mbedtls_ssl_config,
212                MBEDTLS_SSL_IS_CLIENT as i32,
213                MBEDTLS_SSL_TRANSPORT_DATAGRAM as i32,
214                preset.into(),
215            )
216        };
217        if ret < 0 {
218            return Err(ret.into());
219        }
220
221        unsafe {
222            embedded_mbedtls_sys::mbedtls_ssl_conf_rng(
223                &mut this.ssl_ctx.config,
224                Some(rng_try_fill_bytes_callback_fn::<R>),
225                &mut this.ssl_ctx.csrng as *mut R as *mut c_void,
226            );
227        }
228
229        let ret = unsafe {
230            embedded_mbedtls_sys::mbedtls_ssl_setup(&mut this.mbedtls_ctx, &this.ssl_ctx.config)
231        };
232        if ret < 0 {
233            return Err(ret.into());
234        }
235
236        unsafe {
237            embedded_mbedtls_sys::mbedtls_ssl_set_bio(
238                &mut this.mbedtls_ctx,
239                &mut this.ssl_ctx.net_context as *mut udp::UdpContext<U> as *mut c_void,
240                Some(crate::udp::udp_send::<U>),
241                Some(crate::udp::udp_recv::<U>),
242                None, // Some(crate::net::net_recv_timeout::<U, C>),
243            );
244            embedded_mbedtls_sys::mbedtls_ssl_set_timer_cb(
245                &mut this.mbedtls_ctx,
246                &mut this.ssl_ctx.timer_context as *mut timing::MbedtlsTimer<C> as *mut c_void,
247                Some(timing::set_timer::<C>),
248                Some(timing::get_timer::<C>),
249            );
250        }
251
252        Ok(this)
253    }
254
255    /// Set retransmit timeout values for the DTLS handshake
256    ///
257    /// This method is located in the `impl SslConnection` block which uses the
258    /// [`UdpContext`](udp::UdpContext) as net context. This `impl` block is DTLS-specific, i.e.
259    /// the method will only be available on DTLS connections because it will have no effect on a
260    /// TLS connection.
261    ///
262    /// The Mbed TLS documentation says the following about choosing timeout values:
263    ///
264    /// > Default values are from RFC 6347 section 4.2.4.1.
265    ///
266    /// > The ‘min’ value should typically be slightly above the expected round-trip time to your peer,
267    /// plus whatever time it takes for the peer to process the message.
268    /// For example, if your RTT is about 600ms and you peer needs up to 1s
269    /// to do the cryptographic operations in the handshake,
270    /// then you should set `min` slightly above 1600.
271    /// Lower values of `min` might cause spurious resends which waste network resources,
272    /// while larger value of `min` will increase overall latency on unreliable network links.
273    ///
274    /// > Messages are retransmitted up to `log2(ceil(max/min))` times.
275    /// For example, if `min = 1s` and `max = 5s`, the retransmit plan goes:
276    /// send … 1s -> resend … 2s -> resend … 4s -> resend … 5s -> give up and return a timeout error.
277    ///
278    /// If the chosen `Duration` is out-of-bounds regarding the underlying `u32`, we choose huge
279    /// fallbacks to avoid panics.
280    pub fn conf_handshake_timeout(&mut self, min: Duration, max: Duration) {
281        unsafe {
282            embedded_mbedtls_sys::mbedtls_ssl_conf_handshake_timeout(
283                &mut self.ssl_ctx.config,
284                min.as_millis().try_into().unwrap_or(u32::MAX / 4),
285                max.as_millis().try_into().unwrap_or(u32::MAX),
286            );
287        }
288    }
289}
290
291impl<'a, Net, C, R, CTX> SslConnection<'a, Net, C, R, CTX>
292where
293    C: Clock,
294    R: RngCore + CryptoRng,
295    CTX: DerefMut<Target = SslContext<'a, Net, C, R>>,
296{
297    /// Perform the ssl handshake (non-blocking)
298    pub fn handshake(&mut self) -> nb::Result<(), Error> {
299        unsafe {
300            use embedded_mbedtls_sys::mbedtls_ssl_handshake;
301            let ret = mbedtls_ssl_handshake(&mut self.mbedtls_ctx);
302            if matches!(ret, MBEDTLS_ERR_SSL_WANT_READ | MBEDTLS_ERR_SSL_WANT_WRITE) {
303                return Err(nb::Error::WouldBlock);
304            }
305            if ret < 0 {
306                return Err(nb::Error::Other(ret.into()));
307            }
308        }
309
310        Ok(())
311    }
312
313    /// Configure pre-shared keys (PSKs) and their identities to be used in PSK-based cipher suites.
314    ///
315    /// Only one PSK can be registered. If no more PSKs can be configured,
316    /// [`SslFeatureUnavailable`](crate::error::MbedtlsError::SslFeatureUnavailable) is returned.
317    pub fn configure_psk(&mut self, psk: &[u8], psk_identity: &[u8]) -> Result<(), Error> {
318        unsafe {
319            let ret = embedded_mbedtls_sys::mbedtls_ssl_conf_psk(
320                &mut self.ssl_ctx.config,
321                psk.as_ptr(),
322                psk.len(),
323                psk_identity.as_ptr(),
324                psk_identity.len(),
325            );
326
327            if ret < 0 {
328                Err(ret.into())
329            } else {
330                Ok(())
331            }
332        }
333    }
334
335    /// Try to write application data bytes (non-blocking)
336    ///
337    /// Returns the number of bytes actually written (may be less than data.len()).
338    ///
339    /// ## Note:
340    ///
341    /// If the requested length is greater than the maximum fragment length (either the built-in
342    /// limit or the one set or negotiated with the peer), then:
343    /// - with TLS, less bytes than requested are written.
344    /// - with DTLS, a [`SslBadInputData`](crate::error::MbedtlsError::SslBadInputData) Error is
345    /// returned.
346    ///
347    /// Attempting to write 0 bytes will result in an empty TLS application record being sent.
348    pub fn write(&mut self, data: &[u8]) -> nb::Result<usize, Error> {
349        use embedded_mbedtls_sys::{
350            MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS, MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS,
351        };
352        unsafe {
353            use embedded_mbedtls_sys::mbedtls_ssl_write;
354            let ret = mbedtls_ssl_write(&mut self.mbedtls_ctx, data.as_ptr(), data.len());
355            if matches!(
356                ret,
357                MBEDTLS_ERR_SSL_WANT_READ
358                    | MBEDTLS_ERR_SSL_WANT_WRITE
359                    | MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS
360                    | MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS
361            ) {
362                return Err(nb::Error::WouldBlock);
363            }
364            if ret < 0 {
365                return Err(nb::Error::Other(ret.into()));
366            }
367            Ok(ret as usize)
368        }
369    }
370
371    /// Read at most `buf.len()` application data bytes (non-blocking)
372    ///
373    /// Returns the number of bytes actually read.
374    pub fn read(&mut self, buf: &mut [u8]) -> nb::Result<usize, Error> {
375        unsafe {
376            use embedded_mbedtls_sys::mbedtls_ssl_read;
377            use embedded_mbedtls_sys::{
378                MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS, MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS,
379            };
380
381            let ret = mbedtls_ssl_read(&mut self.mbedtls_ctx, buf.as_mut_ptr(), buf.len());
382            if matches!(
383                ret,
384                MBEDTLS_ERR_SSL_WANT_READ
385                    | MBEDTLS_ERR_SSL_WANT_WRITE
386                    | MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS
387                    | MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS
388            ) {
389                return Err(nb::Error::WouldBlock);
390            }
391            let len = match ret {
392                x if x >= 0 => x,
393                e => {
394                    return Err(nb::Error::Other(e.into()));
395                }
396            };
397
398            Ok(len as usize)
399        }
400    }
401
402    /// Notify the peer that the connection is being closed (non-blocking)
403    ///
404    /// On error, the connection has to be reset to be used again.
405    pub fn close_notify(&mut self) -> nb::Result<(), Error> {
406        let ret = unsafe { embedded_mbedtls_sys::mbedtls_ssl_close_notify(&mut self.mbedtls_ctx) };
407
408        if ret == 0 {
409            return Ok(());
410        }
411        if matches!(ret, MBEDTLS_ERR_SSL_WANT_READ | MBEDTLS_ERR_SSL_WANT_WRITE) {
412            return Err(nb::Error::WouldBlock);
413        }
414        Err(nb::Error::Other(ret.into()))
415    }
416
417    /// Reset an already initialized SSL connection for re-use
418    pub fn session_reset(&mut self) -> Result<(), Error> {
419        let ret = unsafe { embedded_mbedtls_sys::mbedtls_ssl_session_reset(&mut self.mbedtls_ctx) };
420        if ret < 0 {
421            Err(ret.into())
422        } else {
423            Ok(())
424        }
425    }
426}
427
428impl<'a, Net, C, R, CTX> Drop for SslConnection<'a, Net, C, R, CTX>
429where
430    C: Clock,
431    R: RngCore + CryptoRng,
432    CTX: DerefMut<Target = SslContext<'a, Net, C, R>>,
433{
434    fn drop(&mut self) {
435        unsafe {
436            embedded_mbedtls_sys::mbedtls_ssl_free(&mut self.mbedtls_ctx);
437        }
438    }
439}
440
441/// Cryptography profile preset which is used to configure an [`SslConnection`]
442#[derive(Debug, Clone, Copy)]
443pub enum Preset {
444    /// Default cryptography profile
445    Default,
446    /// "NSA Suite B Cryptography" profile
447    ///
448    /// See [RFC 6460](https://datatracker.ietf.org/doc/html/rfc6460) for more information.
449    SuiteB,
450}
451
452impl From<Preset> for core::ffi::c_int {
453    fn from(value: Preset) -> Self {
454        match value {
455            Preset::Default => embedded_mbedtls_sys::MBEDTLS_SSL_PRESET_DEFAULT as ffi::c_int,
456            Preset::SuiteB => embedded_mbedtls_sys::MBEDTLS_SSL_PRESET_SUITEB as ffi::c_int,
457        }
458    }
459}
460
461#[cfg(test)]
462mod test {
463    use core::net::{IpAddr, Ipv4Addr, SocketAddr};
464    use embedded_nal::UdpClientStack;
465    use embedded_timers::clock::Clock;
466    use rand_core::{CryptoRng, RngCore};
467
468    use crate::udp;
469
470    use super::{SslConnection, SslContext};
471
472    fn _setup_ssl_stack<U: UdpClientStack, R: RngCore + CryptoRng>(
473        net_stack: U,
474        clock: &impl Clock,
475        rng: R,
476    ) {
477        let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 22);
478
479        let mut ctx = SslContext::new_udp_client_side(net_stack, clock, rng, server_addr);
480        let _connection = SslConnection::new_dtls_client(&mut ctx, super::Preset::Default).unwrap();
481
482        // Now the connection is ready to use!
483    }
484
485    type _BoxedDtls<'a, U, C, R> =
486        SslConnection<'a, udp::UdpContext<U>, C, R, Box<SslContext<'a, udp::UdpContext<U>, C, R>>>;
487
488    #[cfg(feature = "alloc")]
489    fn _setup_ssl_heap<'a, U: UdpClientStack, C: Clock, R: RngCore + CryptoRng>(
490        net_stack: U,
491        clock: &'a C,
492        rng: R,
493    ) -> _BoxedDtls<'a, U, C, R> {
494        let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 22);
495
496        let ctx = SslContext::new_udp_client_side(net_stack, clock, rng, server_addr);
497        let connection =
498            SslConnection::new_dtls_client_heap_context(ctx, super::Preset::Default).unwrap();
499
500        connection
501    }
502}