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}