libcoap_rs/crypto/psk/
server.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/server.rs - Interfaces and types for server-side PSK support in libcoap-rs.
9 */
10
11use std::{
12    borrow::Borrow,
13    cell::RefCell,
14    collections::{BTreeMap, HashMap},
15    ffi::{c_void, CStr},
16    fmt::Debug,
17    hash::Hash,
18    os::raw::c_char,
19    ptr::NonNull,
20    rc::{Rc, Weak},
21};
22
23use libcoap_sys::{
24    coap_bin_const_t, coap_context_set_psk2, coap_context_t, coap_dtls_spsk_info_t, coap_dtls_spsk_t, coap_session_t,
25    COAP_DTLS_SPSK_SETUP_VERSION,
26};
27
28use crate::{crypto::psk::key::PskKey, error::ContextConfigurationError, session::CoapServerSession};
29
30/// Builder for a server-side DTLS encryption context for use with pre-shared keys (PSK).
31#[derive(Debug)]
32pub struct ServerPskContextBuilder<'a> {
33    ctx: ServerPskContextInner<'a>,
34}
35
36impl<'a> ServerPskContextBuilder<'a> {
37    /// Creates a new context builder with the given `key` as the default key to use.
38    ///
39    /// # Implementation details (informative, not covered by semver guarantees)
40    ///
41    /// Providing a raw public key will set `psk_info` to the provided key in the underlying
42    /// [`coap_dtls_spsk_t`] structure.
43    pub fn new(key: PskKey<'a>) -> Self {
44        Self {
45            ctx: ServerPskContextInner {
46                id_key_provider: None,
47                sni_key_provider: None,
48                provided_keys: Vec::new(),
49                raw_cfg: Box::new(coap_dtls_spsk_t {
50                    version: COAP_DTLS_SPSK_SETUP_VERSION as u8,
51                    reserved: Default::default(),
52                    ec_jpake: 0,
53                    validate_id_call_back: None,
54                    id_call_back_arg: std::ptr::null_mut(),
55                    validate_sni_call_back: None,
56                    sni_call_back_arg: std::ptr::null_mut(),
57                    psk_info: key.into_raw_spsk_info(),
58                }),
59            },
60        }
61    }
62
63    /// Sets the key provider that provides a PSK for a given identity.
64    ///
65    /// # Implementation details (informative, not covered by semver guarantees)
66    ///
67    /// Setting a `id_key_provider` will set the `validate_id_call_back` of the underlying
68    /// [`coap_dtls_spsk_t`] to a wrapper function, which will then call the key provider.
69    pub fn id_key_provider(mut self, id_key_provider: impl ServerPskIdentityKeyProvider<'a> + 'a) -> Self {
70        self.ctx.id_key_provider = Some(Box::new(id_key_provider));
71        self.ctx.raw_cfg.validate_id_call_back = Some(dtls_psk_server_id_callback);
72        self
73    }
74
75    /// Sets the key provider that provides keys for a SNI provided by a client.
76    ///
77    /// # Implementation details (informative, not covered by semver guarantees)
78    ///
79    /// Setting a `sni_key_provider` will set the `validate_sni_call_back` of the underlying
80    /// [`coap_dtls_spsk_t`] to a wrapper function, which will then call the key provider.
81    ///
82    /// Keys returned by the key provider will be stored in the context for at least as long as they
83    /// are used by the respective session.
84    pub fn sni_key_provider(mut self, sni_key_provider: impl ServerPskSniKeyProvider<'a> + 'a) -> Self {
85        self.ctx.sni_key_provider = Some(Box::new(sni_key_provider));
86        self.ctx.raw_cfg.validate_sni_call_back = Some(dtls_psk_server_sni_callback);
87        self
88    }
89
90    /// Consumes this builder to construct the resulting PSK context.
91    pub fn build(self) -> ServerPskContext<'a> {
92        let ctx = Rc::new(RefCell::new(self.ctx));
93        {
94            let mut ctx_borrow = ctx.borrow_mut();
95            if ctx_borrow.raw_cfg.validate_id_call_back.is_some() {
96                ctx_borrow.raw_cfg.id_call_back_arg = Rc::downgrade(&ctx).into_raw() as *mut c_void
97            }
98            if ctx_borrow.raw_cfg.validate_sni_call_back.is_some() {
99                ctx_borrow.raw_cfg.sni_call_back_arg = Rc::downgrade(&ctx).into_raw() as *mut c_void
100            }
101        }
102        ServerPskContext { inner: ctx }
103    }
104}
105
106impl ServerPskContextBuilder<'_> {
107    /// Enables or disables support for EC JPAKE ([RFC 8236](https://datatracker.ietf.org/doc/html/rfc8236))
108    /// key exchanges in (D)TLS.
109    ///
110    /// Note: At the time of writing (based on libcoap 4.3.5), this is only supported on MbedTLS,
111    /// enabling EC JPAKE on other DTLS backends has no effect.
112    ///
113    /// # Implementation details (informative, not covered by semver guarantees)
114    ///
115    /// Equivalent to setting `ec_jpake` in the underlying [`coap_dtls_spsk_t`] structure.
116    pub fn ec_jpake(mut self, ec_jpake: bool) -> Self {
117        self.ctx.raw_cfg.ec_jpake = ec_jpake.into();
118        self
119    }
120}
121
122#[derive(Debug)]
123struct ServerPskContextInner<'a> {
124    /// Raw configuration object.
125    raw_cfg: Box<coap_dtls_spsk_t>,
126    /// Store for `coap_dtls_spsk_info_t` instances that we provided in previous SNI or ID
127    /// callback invocations.
128    ///
129    /// The stored pointers *must* all be created from Box::into_raw().
130    ///
131    /// Using `Vec<coap_dtls_spsk_info_t>` instead is not an option, as a Vec resize may cause the
132    /// instances to be moved to a different place in memory, invalidating pointers provided to
133    /// libcoap.
134    provided_keys: Vec<*mut coap_dtls_spsk_info_t>,
135    /// User-supplied SNI key provider.
136    sni_key_provider: Option<Box<dyn ServerPskSniKeyProvider<'a> + 'a>>,
137    /// User-supplied identity key provider.
138    id_key_provider: Option<Box<dyn ServerPskIdentityKeyProvider<'a> + 'a>>,
139}
140
141impl Drop for ServerPskContextInner<'_> {
142    fn drop(&mut self) {
143        for provided_key in std::mem::take(&mut self.provided_keys).into_iter() {
144            // SAFETY: Vector has only ever been filled by instances created from to_raw_spsk_info.
145            unsafe {
146                PskKey::from_raw_spsk_info(*Box::from_raw(provided_key));
147            }
148        }
149        if !self.raw_cfg.id_call_back_arg.is_null() {
150            // SAFETY: If we set this, it must have been a call to Weak::into_raw with the correct
151            //         type.
152            unsafe {
153                Weak::from_raw(self.raw_cfg.id_call_back_arg as *mut Self);
154            }
155        }
156        if !self.raw_cfg.sni_call_back_arg.is_null() {
157            // SAFETY: If we set this, it must have been a call to Weak::into_raw with the correct
158            //         type.
159            unsafe {
160                Weak::from_raw(self.raw_cfg.sni_call_back_arg as *mut Self);
161            }
162        }
163        unsafe {
164            // SAFETY: Pointer should not have been changed by anything else and refers to a CPSK
165            //         info instance created from DtlsPsk::into_raw_cpsk_info().
166            PskKey::from_raw_spsk_info(self.raw_cfg.psk_info);
167        }
168    }
169}
170
171/// Server-side encryption context for PSK-based (D)TLS sessions.
172#[derive(Clone, Debug)]
173pub struct ServerPskContext<'a> {
174    /// Inner structure of this context.
175    inner: Rc<RefCell<ServerPskContextInner<'a>>>,
176}
177
178impl ServerPskContext<'_> {
179    /// Returns a pointer to the PSK key data to use for a given `identity` and `session`, or
180    /// [`std::ptr::null()`] if the provided identity hint and/or session are unacceptable.
181    ///
182    /// The returned pointer is guaranteed to remain valid as long as the underlying
183    /// [`ServerPskContextInner`] is not dropped.
184    /// As the [`ServerPskContext`] is also stored in the [`CoapServerSession`] instance, this
185    /// implies that the pointer is valid for at least as long as the session is.
186    ///
187    /// **Important:** After the underlying [`ServerPskContextInner`] is dropped, the returned
188    /// pointer will no longer be valid and should no longer be dereferenced.
189    fn id_callback(&self, identity: &[u8], session: &CoapServerSession<'_>) -> *const coap_bin_const_t {
190        let mut inner = (*self.inner).borrow_mut();
191        let key = inner
192            .id_key_provider
193            .as_ref()
194            .unwrap()
195            .key_for_identity(identity, session);
196
197        if let Some(key) = key {
198            let boxed_key_info = Box::new(key.into_raw_spsk_info());
199            let boxed_key_ptr = Box::into_raw(boxed_key_info);
200            // TODO remove these entries prematurely if the underlying session is removed (would
201            //      require modifications to the event handler).
202            inner.provided_keys.push(boxed_key_ptr);
203            // SAFETY: Pointer is obviously valid.
204            &unsafe { *boxed_key_ptr }.key
205        } else {
206            std::ptr::null()
207        }
208    }
209
210    /// Returns a pointer to the PSK (potentially with identity hint) to use for a given `sni` (and
211    /// `session`), or [`std::ptr::null()`] if the provided identity hint and/or session are
212    /// unacceptable.
213    ///
214    /// The returned pointer is guaranteed to remain valid as long as the underlying
215    /// [`ServerPskContextInner`] is not dropped.
216    /// As the [`ServerPskContext`] is also stored in the [`CoapServerSession`] instance, this
217    /// implies that the pointer is valid for at least as long as the session is.
218    ///
219    /// **Important:** After the underlying [`ServerPskContextInner`] is dropped, the returned
220    /// pointer will no longer be valid and should no longer be dereferenced.
221    fn sni_callback(&self, sni: &CStr, session: &CoapServerSession<'_>) -> *const coap_dtls_spsk_info_t {
222        let mut inner = (*self.inner).borrow_mut();
223        let key = inner.sni_key_provider.as_ref().unwrap().key_for_sni(sni, session);
224
225        if let Some(key) = key {
226            let boxed_key_info = Box::new(key.into_raw_spsk_info());
227            let boxed_key_ptr = Box::into_raw(boxed_key_info);
228            inner.provided_keys.push(boxed_key_ptr);
229            // SAFETY: Pointer is obviously valid.
230            boxed_key_ptr
231        } else {
232            std::ptr::null()
233        }
234    }
235
236    /// Applies this encryption configuration to the given raw `coap_context_t`.
237    ///
238    /// # Errors
239    ///
240    /// Will return [`ContextConfigurationError::Unknown`] if the call to the underlying libcoap
241    /// function fails.
242    ///
243    /// # Safety
244    /// This [ServerPskContext] must outlive the provided CoAP context, the provided pointer must be
245    /// valid.
246    pub(crate) unsafe fn apply_to_context(
247        &self,
248        mut ctx: NonNull<coap_context_t>,
249    ) -> Result<(), ContextConfigurationError> {
250        let mut inner = self.inner.borrow_mut();
251        // SAFETY: context is valid as per caller contract, raw_cfg is a valid configuration as
252        // ensured by the builder.
253        match unsafe { coap_context_set_psk2(ctx.as_mut(), inner.raw_cfg.as_mut()) } {
254            1 => Ok(()),
255            _ => Err(ContextConfigurationError::Unknown),
256        }
257    }
258}
259
260impl<'a> ServerPskContext<'a> {
261    /// Restores a [`ServerPskContext`] from a pointer to its inner structure (i.e. from the
262    /// user-provided pointer given to DTLS callbacks).
263    ///
264    /// # Panics
265    ///
266    /// Panics if the given pointer is a null pointer or the inner structure was already dropped.
267    ///
268    /// # Safety
269    /// The provided pointer must be a valid reference to a [`RefCell<ServerPskContextInner>`]
270    /// instance created from a call to [`Weak::into_raw()`].
271    unsafe fn from_raw(raw_ctx: *const RefCell<ServerPskContextInner<'a>>) -> Self {
272        assert!(!raw_ctx.is_null(), "provided raw DTLS PSK server context was null");
273        let inner_weak = Weak::from_raw(raw_ctx);
274        let inner = inner_weak
275            .upgrade()
276            .expect("provided DTLS PSK server context was already dropped!");
277        let _ = Weak::into_raw(inner_weak);
278        ServerPskContext { inner }
279    }
280}
281
282/// Trait for types that can provide pre-shared keys for a key identity given by a client to a
283/// server.
284pub trait ServerPskIdentityKeyProvider<'a>: Debug {
285    /// Provides the key for the key `identity` given by the client that is connected through
286    /// `session`, or `None` if the identity unacceptable or no key is available.
287    fn key_for_identity(&self, identity: &[u8], session: &CoapServerSession<'_>) -> Option<PskKey<'a>>;
288}
289
290impl<'a, T: Debug> ServerPskIdentityKeyProvider<'a> for T
291where
292    T: AsRef<[PskKey<'a>]>,
293{
294    /// Returns the first key whose identity is equal to the one requested.
295    /// If not found, returns the first key that has no key ID set.
296    fn key_for_identity(&self, identity: &[u8], _session: &CoapServerSession<'_>) -> Option<PskKey<'a>> {
297        let keys = self.as_ref();
298        keys.iter()
299            .find(|k| k.identity().is_some_and(|kid| kid == identity))
300            .or_else(|| keys.iter().find(|k| k.identity().is_none()))
301            .cloned()
302    }
303}
304
305/// Trait for things that can provide PSK DTLS keys for a given Server Name Indication.
306pub trait ServerPskSniKeyProvider<'a>: Debug {
307    /// Provide a key for the server name indication given as `sni`, or `None` if the SNI is not
308    /// valid and no key is available.
309    ///
310    /// Note that libcoap will remember the returned key and re-use it for future handshakes with
311    /// the same SNI (even if the peer is not the same), the return value should therefore not
312    /// depend on the provided `session`.
313    fn key_for_sni(&self, sni: &CStr, session: &CoapServerSession<'_>) -> Option<PskKey<'a>>;
314}
315
316impl<'a, T: AsRef<[u8]> + Debug, U: AsRef<PskKey<'a>> + Debug> ServerPskSniKeyProvider<'a> for Vec<(T, U)> {
317    /// Return the second tuple object if the first one matches the given SNI.
318    fn key_for_sni(&self, sni: &CStr, _session: &CoapServerSession<'_>) -> Option<PskKey<'a>> {
319        self.iter()
320            .find_map(|(key_sni, key)| (key_sni.as_ref() == sni.to_bytes()).then_some(key.as_ref().clone()))
321    }
322}
323
324impl<'a, T: AsRef<[u8]> + Debug, U: AsRef<PskKey<'a>> + Debug> ServerPskSniKeyProvider<'a> for [(T, U)] {
325    /// Return the second tuple object if the first one matches the given SNI.
326    fn key_for_sni(&self, sni: &CStr, _session: &CoapServerSession<'_>) -> Option<PskKey<'a>> {
327        self.iter()
328            .find_map(|(key_sni, key)| (key_sni.as_ref() == sni.to_bytes()).then_some(key.as_ref().clone()))
329    }
330}
331
332impl<'a, T: Borrow<[u8]> + Debug + Eq + Hash, U: AsRef<PskKey<'a>> + Debug> ServerPskSniKeyProvider<'a>
333    for HashMap<T, U>
334{
335    /// Return the map value if the key matches the given SNI.
336    fn key_for_sni(&self, sni: &CStr, _session: &CoapServerSession<'_>) -> Option<PskKey<'a>> {
337        self.get(sni.to_bytes()).map(|v| v.as_ref()).cloned()
338    }
339}
340
341impl<'a, T: Borrow<[u8]> + Debug + Ord, U: AsRef<PskKey<'a>> + Debug> ServerPskSniKeyProvider<'a> for BTreeMap<T, U> {
342    /// Return the map value if the key matches the given SNI.
343    fn key_for_sni(&self, sni: &CStr, _session: &CoapServerSession<'_>) -> Option<PskKey<'a>> {
344        self.get(sni.to_bytes()).map(|v| v.as_ref()).cloned()
345    }
346}
347
348/// Raw PSK identity callback that can be provided to libcoap.
349///
350/// # Safety
351///
352/// This function expects the arguments to be provided in a way that libcoap would when invoking
353/// this function as an identity callback.
354///
355/// Additionally, `arg` must be a valid argument to [`ServerPskContext::from_raw`].
356unsafe extern "C" fn dtls_psk_server_id_callback(
357    identity: *mut coap_bin_const_t,
358    session: *mut coap_session_t,
359    userdata: *mut c_void,
360) -> *const coap_bin_const_t {
361    let identity = std::slice::from_raw_parts((*identity).s, (*identity).length);
362    // We must not increase the refcount here, as doing so would require locking the global context,
363    // which is not possible during a DTLS callback.
364    // SAFETY: While we are in this callback, libcoap's context is locked by our current thread.
365    //         therefore, it is impossible that the reference counter would be decreased by any
366    //         other means, and constructing the server side session without increasing the refcount
367    //         is fine.
368    let session = CoapServerSession::from_raw_without_refcount(session);
369    let server_context = ServerPskContext::from_raw(userdata as *const RefCell<ServerPskContextInner>);
370    server_context.id_callback(identity, &session)
371}
372
373/// Raw PSK SNI callback that can be provided to libcoap.
374///
375/// # Safety
376///
377/// This function expects the arguments to be provided in a way that libcoap would when invoking
378/// this function as an PSK SNI callback.
379///
380/// Additionally, `arg` must be a valid argument to [`ServerPskContext::from_raw`].
381unsafe extern "C" fn dtls_psk_server_sni_callback(
382    sni: *const c_char,
383    session: *mut coap_session_t,
384    userdata: *mut c_void,
385) -> *const coap_dtls_spsk_info_t {
386    let sni = CStr::from_ptr(sni);
387    // We must not increase the refcount here, as doing so would require locking the global context,
388    // which is not possible during a DTLS callback.
389    // SAFETY: While we are in this callback, libcoap's context is locked by our current thread.
390    //         therefore, it is impossible that the reference counter would be decreased by any
391    //         other means, and constructing the server side session without increasing the refcount
392    //         is fine.
393    let session = CoapServerSession::from_raw_without_refcount(session);
394    let server_context = ServerPskContext::from_raw(userdata as *const RefCell<ServerPskContextInner>);
395    server_context.sni_callback(sni, &session)
396}