libcoap_rs/crypto/psk/
client.rs

1// SPDX-License-Identifier: BSD-2-Clause
2/*
3 * Copyright © The libcoap-rs Contributors, all rights reserved.
4 * This file is part of the libcoap-rs project, see the README file for
5 * general information on this project and the NOTICE.md and LICENSE files
6 * for information regarding copyright ownership and terms of use.
7 *
8 * crypto/psk/client.rs - Interfaces and types for client-side PSK support in libcoap-rs.
9 */
10
11use std::{
12    cell::RefCell,
13    ffi::{c_char, c_void, CString, NulError},
14    fmt::Debug,
15    ptr::NonNull,
16    rc::{Rc, Weak},
17};
18
19use libcoap_sys::{
20    coap_dtls_cpsk_info_t, coap_dtls_cpsk_t, coap_new_client_session_psk2, coap_proto_t, coap_session_t,
21    coap_str_const_t, COAP_DTLS_CPSK_SETUP_VERSION,
22};
23
24use crate::{
25    crypto::psk::key::PskKey, error::SessionCreationError, session::CoapClientSession, types::CoapAddress, CoapContext,
26};
27
28/// Builder for a client-side DTLS encryption context for use with pre-shared keys (PSK).
29#[derive(Debug)]
30pub struct ClientPskContextBuilder<'a> {
31    ctx: ClientPskContextInner<'a>,
32}
33
34impl<'a> ClientPskContextBuilder<'a> {
35    /// Creates a new context builder with the given `key` as the default key to use.
36    ///
37    /// # Implementation details (informative, not covered by semver guarantees)
38    ///
39    /// Providing a raw public key will set `psk_info` to the provided key in the underlying
40    /// [`coap_dtls_cpsk_t`] structure.
41    pub fn new(psk: PskKey<'a>) -> Self {
42        Self {
43            ctx: ClientPskContextInner {
44                raw_cfg: Box::new(coap_dtls_cpsk_t {
45                    version: COAP_DTLS_CPSK_SETUP_VERSION as u8,
46                    reserved: Default::default(),
47                    ec_jpake: 0,
48                    use_cid: 0,
49                    validate_ih_call_back: None,
50                    ih_call_back_arg: std::ptr::null_mut(),
51                    client_sni: std::ptr::null_mut(),
52                    psk_info: psk.into_raw_cpsk_info(),
53                }),
54                key_provider: None,
55                provided_keys: Vec::new(),
56                client_sni: None,
57            },
58        }
59    }
60
61    /// Sets the key provider that provides pre-shared keys based on the PSK hint received by the
62    /// server.
63    ///
64    /// # Implementation details (informative, not covered by semver guarantees)
65    ///
66    /// Setting a `key_provider` will set the `validate_ih_call_back` of the underlying
67    /// [`coap_dtls_cpsk_t`] to a wrapper function, which will then call the key provider.
68    ///
69    /// Keys returned by the key provider will be stored in the context for at least as long as they
70    /// are used by the respective session.
71    pub fn key_provider(mut self, key_provider: impl ClientPskHintKeyProvider<'a> + 'a) -> Self {
72        self.ctx.key_provider = Some(Box::new(key_provider));
73        self.ctx.raw_cfg.validate_ih_call_back = Some(dtls_psk_client_ih_callback);
74        self
75    }
76
77    /// Consumes this builder to construct the resulting PSK context.
78    pub fn build(self) -> ClientPskContext<'a> {
79        let ctx = Rc::new(RefCell::new(self.ctx));
80        {
81            let mut ctx_borrow = ctx.borrow_mut();
82            if ctx_borrow.raw_cfg.validate_ih_call_back.is_some() {
83                ctx_borrow.raw_cfg.ih_call_back_arg = Rc::downgrade(&ctx).into_raw() as *mut c_void;
84            }
85        }
86        ClientPskContext { inner: ctx }
87    }
88}
89
90impl<'a> From<ClientPskContext<'a>> for crate::crypto::ClientCryptoContext<'a> {
91    fn from(value: ClientPskContext<'a>) -> Self {
92        crate::crypto::ClientCryptoContext::Psk(value)
93    }
94}
95
96impl ClientPskContextBuilder<'_> {
97    /// Enables or disables support for EC JPAKE ([RFC 8236](https://datatracker.ietf.org/doc/html/rfc8236))
98    /// key exchanges in (D)TLS.
99    ///
100    /// Note: At the time of writing (based on libcoap 4.3.5), this is only supported on MbedTLS,
101    /// enabling EC JPAKE on other DTLS backends has no effect.
102    ///
103    /// # Implementation details (informative, not covered by semver guarantees)
104    ///
105    /// Equivalent to setting `ec_jpake` in the underlying [`coap_dtls_cpsk_t`] structure.
106    pub fn ec_jpake(mut self, ec_jpake: bool) -> Self {
107        self.ctx.raw_cfg.ec_jpake = ec_jpake.into();
108        self
109    }
110
111    /// Enables or disables use of DTLS connection IDs ([RFC 9146](https://datatracker.ietf.org/doc/rfc9146/)).
112    ///
113    /// # Implementation details (informative, not covered by semver guarantees)
114    ///
115    /// Equivalent to setting `use_cid` in the underlying [`coap_dtls_cpsk_t`] structure.
116    #[cfg(feature = "dtls-cid")]
117    pub fn use_cid(mut self, use_cid: bool) -> Self {
118        self.ctx.raw_cfg.use_cid = use_cid.into();
119        self
120    }
121
122    /// Sets the server name indication that should be sent to servers if the built
123    /// [`ClientPskContext`] is used.
124    ///
125    /// `client_sni` should be convertible into a byte string that does not contain null bytes.
126    /// Typically, you would provide a `&str` or `String`.
127    ///
128    /// # Errors
129    ///
130    /// Will return [`NulError`] if the provided byte string contains null bytes.
131    ///
132    /// # Implementation details (informative, not covered by semver guarantees)
133    ///
134    /// Equivalent to setting `client_sni` in the underlying [`coap_dtls_cpsk_t`] structure.
135    ///
136    /// The provided `client_sni` will be converted into a `Box<[u8]>`, which will be owned and
137    /// stored by the built context.
138    pub fn client_sni<T: Into<Vec<u8>>>(mut self, client_sni: T) -> Result<Self, NulError> {
139        // For some reason, client_sni is not immutable here.
140        // While I don't see any reason why libcoap would modify the string, it is not strictly
141        // forbidden for it to do so, so simply using CString::into_raw() is not an option (as it
142        // does not allow modifications to client_sni that change the length).
143        let sni = CString::new(client_sni.into())?
144            .into_bytes_with_nul()
145            .into_boxed_slice();
146        self.ctx.client_sni = Some(sni);
147        self.ctx.raw_cfg.client_sni = self.ctx.client_sni.as_mut().unwrap().as_mut_ptr() as *mut c_char;
148        Ok(self)
149    }
150}
151
152/// Client-side encryption context for PSK-based (D)TLS sessions.
153#[derive(Clone, Debug)]
154pub struct ClientPskContext<'a> {
155    /// Inner structure of this context.
156    inner: Rc<RefCell<ClientPskContextInner<'a>>>,
157}
158
159impl ClientPskContext<'_> {
160    /// Returns a pointer to the PSK to use for a given `identity_hint` and `session`, or
161    /// [`std::ptr::null()`] if the provided identity hint and/or session are unacceptable.
162    ///
163    /// The returned pointer is guaranteed to remain valid as long as the underlying
164    /// [`ClientPskContextInner`] is not dropped.
165    /// As the [`ClientPskContext`] is also stored in the [`CoapClientSession`] instance, this
166    /// implies that the pointer is valid for at least as long as the session is.
167    ///
168    /// **Important:** After the underlying [`ClientPskContextInner`] is dropped, the returned
169    /// pointer will no longer be valid and should no longer be dereferenced.
170    fn ih_callback(
171        &self,
172        identity_hint: Option<&[u8]>,
173        session: &CoapClientSession<'_>,
174    ) -> *const coap_dtls_cpsk_info_t {
175        let mut inner = (*self.inner).borrow_mut();
176        let key = inner
177            .key_provider
178            .as_ref()
179            .unwrap()
180            .key_for_identity_hint(identity_hint, session);
181
182        if let Some(key) = key {
183            let boxed_key_info = Box::new(key.into_raw_cpsk_info());
184            let boxed_key_ptr = Box::into_raw(boxed_key_info);
185            // TODO remove these entries prematurely if the underlying session is removed (would
186            //      require modifications to the client session drop handler).
187            inner.provided_keys.push(boxed_key_ptr);
188            boxed_key_ptr
189        } else {
190            std::ptr::null()
191        }
192    }
193
194    /// Creates a raw CoAP session object that is bound to and utilizes this encryption context.
195    ///
196    /// # Safety
197    ///
198    /// This [`ClientPskContext`] must outlive the returned [`coap_session_t`].
199    pub(crate) unsafe fn create_raw_session(
200        &self,
201        ctx: &mut CoapContext<'_>,
202        addr: &CoapAddress,
203        proto: coap_proto_t,
204    ) -> Result<NonNull<coap_session_t>, SessionCreationError> {
205        // SAFETY: self.raw_context is guaranteed to be valid, local_if can be null,
206        // raw_cfg is of valid format (as constructed by the builder).
207        {
208            let mut inner = (*self.inner).borrow_mut();
209            NonNull::new(unsafe {
210                coap_new_client_session_psk2(
211                    ctx.as_mut_raw_context(),
212                    std::ptr::null(),
213                    addr.as_raw_address(),
214                    proto,
215                    inner.raw_cfg.as_mut(),
216                )
217            })
218            .ok_or(SessionCreationError::Unknown)
219        }
220    }
221}
222
223impl<'a> ClientPskContext<'a> {
224    /// Restores a [`ClientPskContext`] from a pointer to its inner structure (i.e., from the
225    /// user-provided pointer given to DTLS callbacks).
226    ///
227    /// # Panics
228    ///
229    /// Panics if the given pointer is a null pointer or the inner structure was already dropped.
230    ///
231    /// # Safety
232    /// The provided pointer must be a valid reference to a [`RefCell<ClientPskContextInner>`]
233    /// instance created from a call to [`Weak::into_raw()`].
234    unsafe fn from_raw(raw_ctx: *const RefCell<ClientPskContextInner<'a>>) -> Self {
235        assert!(!raw_ctx.is_null(), "provided raw DTLS PSK client context was null");
236        let inner_weak = Weak::from_raw(raw_ctx);
237        let inner = inner_weak
238            .upgrade()
239            .expect("provided DTLS PSK client context was already dropped!");
240        let _ = Weak::into_raw(inner_weak);
241        ClientPskContext { inner }
242    }
243}
244
245/// Inner structure of a client-side PSK context.
246#[derive(Debug)]
247struct ClientPskContextInner<'a> {
248    /// Raw configuration object.
249    raw_cfg: Box<coap_dtls_cpsk_t>,
250    /// User-supplied key provider.
251    key_provider: Option<Box<dyn ClientPskHintKeyProvider<'a> + 'a>>,
252    /// Store for `coap_dtls_cpsk_info_t` instances that we provided in previous identity hint
253    /// callback invocations.
254    ///
255    /// The stored pointers *must* all be created from [`Box::into_raw`].
256    ///
257    /// Using `Vec<coap_dtls_cpsk_info_t>` instead is not an option, as a `Vec` resize may cause the
258    /// instances to be moved to a different place in memory, invalidating pointers provided to
259    /// libcoap.
260    provided_keys: Vec<*mut coap_dtls_cpsk_info_t>,
261    /// Server Name Indication to send to servers.
262    client_sni: Option<Box<[u8]>>,
263}
264
265impl Drop for ClientPskContextInner<'_> {
266    fn drop(&mut self) {
267        for provided_key in std::mem::take(&mut self.provided_keys).into_iter() {
268            // SAFETY: Vector has only ever been filled by instances created from to_raw_cpsk_info.
269            unsafe {
270                PskKey::from_raw_cpsk_info(*Box::from_raw(provided_key));
271            }
272        }
273        if !self.raw_cfg.ih_call_back_arg.is_null() {
274            // SAFETY: If we set this, it must have been a call to Weak::into_raw with the correct
275            //         type.
276            unsafe {
277                Weak::from_raw(self.raw_cfg.ih_call_back_arg as *mut RefCell<Self>);
278            }
279        }
280        unsafe {
281            // SAFETY: Pointer should not have been changed by anything else and refers to a CPSK
282            //         info instance created from DtlsPsk::into_raw_cpsk_info().
283            PskKey::from_raw_cpsk_info(self.raw_cfg.psk_info);
284        }
285    }
286}
287
288/// Trait for types that can provide the appropriate pre-shared key for a given PSK hint sent by the
289/// server.
290pub trait ClientPskHintKeyProvider<'a>: Debug {
291    /// Returns the appropriate pre-shared key for a given `identity_hint` and the given `session`,
292    /// or `None` if the session should be aborted/no key is available.
293    fn key_for_identity_hint(
294        &self,
295        identity_hint: Option<&[u8]>,
296        session: &CoapClientSession<'_>,
297    ) -> Option<PskKey<'a>>;
298}
299
300impl<'a, T: Debug> ClientPskHintKeyProvider<'a> for T
301where
302    T: AsRef<PskKey<'a>>,
303{
304    /// Returns the key if the supplied `identity_hint` is `None` or the key's identity matches the
305    /// hint.
306    fn key_for_identity_hint(
307        &self,
308        identity_hint: Option<&[u8]>,
309        _session: &CoapClientSession<'_>,
310    ) -> Option<PskKey<'a>> {
311        let key = self.as_ref();
312        if identity_hint.is_none() || key.identity() == identity_hint {
313            Some(key.clone())
314        } else {
315            None
316        }
317    }
318}
319
320/// Raw PSK identity hint callback that can be provided to libcoap.
321///
322/// # Safety
323///
324/// This function expects the arguments to be provided in a way that libcoap would when invoking
325/// this function as an identity hint callback.
326///
327/// Additionally, `arg` must be a valid argument to [`ClientPskContext::from_raw`].
328unsafe extern "C" fn dtls_psk_client_ih_callback(
329    hint: *mut coap_str_const_t,
330    session: *mut coap_session_t,
331    userdata: *mut c_void,
332) -> *const coap_dtls_cpsk_info_t {
333    let session = CoapClientSession::from_raw(session);
334    let client_context = ClientPskContext::from_raw(userdata as *const RefCell<ClientPskContextInner>);
335    let provided_identity =
336        NonNull::new(hint).map(|h| std::slice::from_raw_parts((*h.as_ptr()).s, (*h.as_ptr()).length));
337    client_context.ih_callback(provided_identity, &session)
338}