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}