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