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}