1
// SPDX-License-Identifier: BSD-2-Clause
2
/*
3
 * crypto/psk/client.rs - Interfaces and types for client-side PSK support in libcoap-rs.
4
 * This file is part of the libcoap-rs crate, see the README and LICENSE files for
5
 * more information and terms of use.
6
 * Copyright © 2021-2024 The NAMIB Project Developers, all rights reserved.
7
 * See the README as well as the LICENSE file for more information.
8
 */
9

            
10
use crate::crypto::psk::key::PskKey;
11
use crate::error::SessionCreationError;
12
use crate::session::CoapClientSession;
13
use crate::types::CoapAddress;
14
use crate::CoapContext;
15
use libcoap_sys::{
16
    coap_dtls_cpsk_info_t, coap_dtls_cpsk_t, coap_new_client_session_psk2, coap_proto_t, coap_session_t,
17
    coap_str_const_t, COAP_DTLS_CPSK_SETUP_VERSION,
18
};
19
use std::cell::RefCell;
20
use std::ffi::{c_char, c_void, CString, NulError};
21
use std::fmt::Debug;
22
use std::ptr::NonNull;
23
use std::rc::{Rc, Weak};
24

            
25
/// Builder for a client-side DTLS encryption context for use with pre-shared keys (PSK).
26
#[derive(Debug)]
27
pub struct ClientPskContextBuilder<'a> {
28
    ctx: ClientPskContextInner<'a>,
29
}
30

            
31
impl<'a> ClientPskContextBuilder<'a> {
32
    /// Creates a new context builder with the given `key` as the default key to use.
33
    ///
34
    /// # Implementation details (informative, not covered by semver guarantees)
35
    ///
36
    /// Providing a raw public key will set `psk_info` to the provided key in the underlying
37
    /// [`coap_dtls_cpsk_t`] structure.
38
35
    pub fn new(psk: PskKey<'a>) -> Self {
39
35
        Self {
40
35
            ctx: ClientPskContextInner {
41
35
                raw_cfg: Box::new(coap_dtls_cpsk_t {
42
35
                    version: COAP_DTLS_CPSK_SETUP_VERSION as u8,
43
35
                    reserved: Default::default(),
44
35
                    #[cfg(dtls_ec_jpake_support)]
45
35
                    ec_jpake: 0,
46
35
                    #[cfg(dtls_cid_support)]
47
35
                    use_cid: 0,
48
35
                    validate_ih_call_back: None,
49
35
                    ih_call_back_arg: std::ptr::null_mut(),
50
35
                    client_sni: std::ptr::null_mut(),
51
35
                    psk_info: psk.into_raw_cpsk_info(),
52
35
                }),
53
35
                key_provider: None,
54
35
                provided_keys: Vec::new(),
55
35
                client_sni: None,
56
35
            },
57
35
        }
58
35
    }
59

            
60
    /// Sets the key provider that provides pre-shared keys based on the PSK hint received by the
61
    /// server.
62
    ///
63
    /// # Implementation details (informative, not covered by semver guarantees)
64
    ///
65
    /// Setting a `key_provider` will set the `validate_ih_call_back` of the underlying
66
    /// [`coap_dtls_cpsk_t`] to a wrapper function, which will then call the key provider.
67
    ///
68
    /// Keys returned by the key provider will be stored in the context for at least as long as they
69
    /// are used by the respective session.
70
    pub fn key_provider(mut self, key_provider: impl ClientPskHintKeyProvider<'a> + 'a) -> Self {
71
        self.ctx.key_provider = Some(Box::new(key_provider));
72
        self.ctx.raw_cfg.validate_ih_call_back = Some(dtls_psk_client_ih_callback);
73
        self
74
    }
75

            
76
    /// Consumes this builder to construct the resulting PSK context.
77
35
    pub fn build(self) -> ClientPskContext<'a> {
78
35
        let ctx = Rc::new(RefCell::new(self.ctx));
79
35
        {
80
35
            let mut ctx_borrow = ctx.borrow_mut();
81
35
            if ctx_borrow.raw_cfg.validate_ih_call_back.is_some() {
82
                ctx_borrow.raw_cfg.ih_call_back_arg = Rc::downgrade(&ctx).into_raw() as *mut c_void;
83
35
            }
84
        }
85
35
        ClientPskContext { inner: ctx }
86
35
    }
87
}
88

            
89
impl<'a> From<ClientPskContext<'a>> for crate::crypto::ClientCryptoContext<'a> {
90
35
    fn from(value: ClientPskContext<'a>) -> Self {
91
35
        crate::crypto::ClientCryptoContext::Psk(value)
92
35
    }
93
}
94

            
95
impl ClientPskContextBuilder<'_> {
96
    /// Enables or disables support for EC JPAKE ([RFC 8236](https://datatracker.ietf.org/doc/html/rfc8236))
97
    /// key exchanges in (D)TLS.
98
    ///
99
    /// # Implementation details (informative, not covered by semver guarantees)
100
    ///
101
    /// Equivalent to setting `ec_jpake` in the underlying [`coap_dtls_cpsk_t`] structure.
102
    #[cfg(dtls_ec_jpake_support)]
103
    pub fn ec_jpake(mut self, ec_jpake: bool) -> Self {
104
        self.ctx.raw_cfg.ec_jpake = ec_jpake.into();
105
        self
106
    }
107

            
108
    /// Enables or disables use of DTLS connection IDs ([RFC 9146](https://datatracker.ietf.org/doc/rfc9146/)).
109
    ///
110
    /// # Implementation details (informative, not covered by semver guarantees)
111
    ///
112
    /// Equivalent to setting `use_cid` in the underlying [`coap_dtls_cpsk_t`] structure.
113
    #[cfg(dtls_cid_support)]
114
    pub fn use_cid(mut self, use_cid: bool) -> Self {
115
        self.ctx.raw_cfg.use_cid = use_cid.into();
116
        self
117
    }
118

            
119
    /// Sets the server name indication that should be sent to servers if the built
120
    /// [`ClientPskContext`] is used.
121
    ///
122
    /// `client_sni` should be convertible into a byte string that does not contain null bytes.
123
    /// Typically, you would provide a `&str` or `String`.
124
    ///
125
    /// # Errors
126
    ///
127
    /// Will return [`NulError`] if the provided byte string contains null bytes.
128
    ///
129
    /// # Implementation details (informative, not covered by semver guarantees)
130
    ///
131
    /// Equivalent to setting `client_sni` in the underlying [`coap_dtls_cpsk_t`] structure.
132
    ///
133
    /// The provided `client_sni` will be converted into a `Box<[u8]>`, which will be owned and
134
    /// stored by the built context.
135
    pub fn client_sni<T: Into<Vec<u8>>>(mut self, client_sni: T) -> Result<Self, NulError> {
136
        // For some reason, client_sni is not immutable here.
137
        // While I don't see any reason why libcoap would modify the string, it is not strictly
138
        // forbidden for it to do so, so simply using CString::into_raw() is not an option (as it
139
        // does not allow modifications to client_sni that change the length).
140
        let sni = CString::new(client_sni.into())?
141
            .into_bytes_with_nul()
142
            .into_boxed_slice();
143
        self.ctx.client_sni = Some(sni);
144
        self.ctx.raw_cfg.client_sni = self.ctx.client_sni.as_mut().unwrap().as_mut_ptr() as *mut c_char;
145
        Ok(self)
146
    }
147
}
148

            
149
/// Client-side encryption context for PSK-based (D)TLS sessions.
150
#[derive(Clone, Debug)]
151
pub struct ClientPskContext<'a> {
152
    /// Inner structure of this context.
153
    inner: Rc<RefCell<ClientPskContextInner<'a>>>,
154
}
155

            
156
impl ClientPskContext<'_> {
157
    /// Returns a pointer to the PSK to use for a given `identity_hint` and `session`, or
158
    /// [`std::ptr::null()`] if the provided identity hint and/or session are unacceptable.
159
    ///
160
    /// The returned pointer is guaranteed to remain valid as long as the underlying
161
    /// [`ClientPskContextInner`] is not dropped.
162
    /// As the [`ClientPskContext`] is also stored in the [`CoapClientSession`] instance, this
163
    /// implies that the pointer is valid for at least as long as the session is.
164
    ///
165
    /// **Important:** After the underlying [`ClientPskContextInner`] is dropped, the returned
166
    /// pointer will no longer be valid and should no longer be dereferenced.
167
    fn ih_callback(
168
        &self,
169
        identity_hint: Option<&[u8]>,
170
        session: &CoapClientSession<'_>,
171
    ) -> *const coap_dtls_cpsk_info_t {
172
        let mut inner = (*self.inner).borrow_mut();
173
        let key = inner
174
            .key_provider
175
            .as_ref()
176
            .unwrap()
177
            .key_for_identity_hint(identity_hint, session);
178

            
179
        if let Some(key) = key {
180
            let boxed_key_info = Box::new(key.into_raw_cpsk_info());
181
            let boxed_key_ptr = Box::into_raw(boxed_key_info);
182
            // TODO remove these entries prematurely if the underlying session is removed (would
183
            //      require modifications to the client session drop handler).
184
            inner.provided_keys.push(boxed_key_ptr);
185
            boxed_key_ptr
186
        } else {
187
            std::ptr::null()
188
        }
189
    }
190

            
191
    /// Creates a raw CoAP session object that is bound to and utilizes this encryption context.
192
    ///
193
    /// # Safety
194
    ///
195
    /// This [`ClientPskContext`] must outlive the returned [`coap_session_t`].
196
35
    pub(crate) unsafe fn create_raw_session(
197
35
        &self,
198
35
        ctx: &mut CoapContext<'_>,
199
35
        addr: &CoapAddress,
200
35
        proto: coap_proto_t,
201
35
    ) -> Result<NonNull<coap_session_t>, SessionCreationError> {
202
35
        // SAFETY: self.raw_context is guaranteed to be valid, local_if can be null,
203
35
        // raw_cfg is of valid format (as constructed by the builder).
204
35
        {
205
35
            let mut inner = (*self.inner).borrow_mut();
206
35
            NonNull::new(unsafe {
207
35
                coap_new_client_session_psk2(
208
35
                    ctx.as_mut_raw_context(),
209
35
                    std::ptr::null(),
210
35
                    addr.as_raw_address(),
211
35
                    proto,
212
35
                    inner.raw_cfg.as_mut(),
213
35
                )
214
35
            })
215
35
            .ok_or(SessionCreationError::Unknown)
216
35
        }
217
35
    }
218
}
219

            
220
impl<'a> ClientPskContext<'a> {
221
    /// Restores a [`ClientPskContext`] from a pointer to its inner structure (i.e., from the
222
    /// user-provided pointer given to DTLS callbacks).
223
    ///
224
    /// # Panics
225
    ///
226
    /// Panics if the given pointer is a null pointer or the inner structure was already dropped.
227
    ///
228
    /// # Safety
229
    /// The provided pointer must be a valid reference to a [`RefCell<ClientPskContextInner>`]
230
    /// instance created from a call to [`Weak::into_raw()`].
231
    unsafe fn from_raw(raw_ctx: *const RefCell<ClientPskContextInner<'a>>) -> Self {
232
        assert!(!raw_ctx.is_null(), "provided raw DTLS PSK client context was null");
233
        let inner_weak = Weak::from_raw(raw_ctx);
234
        let inner = inner_weak
235
            .upgrade()
236
            .expect("provided DTLS PSK client context was already dropped!");
237
        let _ = Weak::into_raw(inner_weak);
238
        ClientPskContext { inner }
239
    }
240
}
241

            
242
/// Inner structure of a client-side PSK context.
243
#[derive(Debug)]
244
struct ClientPskContextInner<'a> {
245
    /// Raw configuration object.
246
    raw_cfg: Box<coap_dtls_cpsk_t>,
247
    /// User-supplied key provider.
248
    key_provider: Option<Box<dyn ClientPskHintKeyProvider<'a> + 'a>>,
249
    /// Store for `coap_dtls_cpsk_info_t` instances that we provided in previous identity hint
250
    /// callback invocations.
251
    ///
252
    /// The stored pointers *must* all be created from [`Box::into_raw`].
253
    ///
254
    /// Using `Vec<coap_dtls_cpsk_info_t>` instead is not an option, as a `Vec` resize may cause the
255
    /// instances to be moved to a different place in memory, invalidating pointers provided to
256
    /// libcoap.
257
    provided_keys: Vec<*mut coap_dtls_cpsk_info_t>,
258
    /// Server Name Indication to send to servers.
259
    client_sni: Option<Box<[u8]>>,
260
}
261

            
262
impl Drop for ClientPskContextInner<'_> {
263
35
    fn drop(&mut self) {
264
35
        for provided_key in std::mem::take(&mut self.provided_keys).into_iter() {
265
            // SAFETY: Vector has only ever been filled by instances created from to_raw_cpsk_info.
266
            unsafe {
267
                PskKey::from_raw_cpsk_info(*Box::from_raw(provided_key));
268
            }
269
        }
270
35
        if !self.raw_cfg.ih_call_back_arg.is_null() {
271
            // SAFETY: If we set this, it must have been a call to Weak::into_raw with the correct
272
            //         type.
273
            unsafe {
274
                Weak::from_raw(self.raw_cfg.ih_call_back_arg as *mut RefCell<Self>);
275
            }
276
35
        }
277
35
        unsafe {
278
35
            // SAFETY: Pointer should not have been changed by anything else and refers to a CPSK
279
35
            //         info instance created from DtlsPsk::into_raw_cpsk_info().
280
35
            PskKey::from_raw_cpsk_info(self.raw_cfg.psk_info);
281
35
        }
282
35
    }
283
}
284

            
285
/// Trait for types that can provide the appropriate pre-shared key for a given PSK hint sent by the
286
/// server.
287
pub trait ClientPskHintKeyProvider<'a>: Debug {
288
    /// Returns the appropriate pre-shared key for a given `identity_hint` and the given `session`,
289
    /// or `None` if the session should be aborted/no key is available.
290
    fn key_for_identity_hint(
291
        &self,
292
        identity_hint: Option<&[u8]>,
293
        session: &CoapClientSession<'_>,
294
    ) -> Option<PskKey<'a>>;
295
}
296

            
297
impl<'a, T: Debug> ClientPskHintKeyProvider<'a> for T
298
where
299
    T: AsRef<PskKey<'a>>,
300
{
301
    /// Returns the key if the supplied `identity_hint` is `None` or the key's identity matches the
302
    /// hint.
303
    fn key_for_identity_hint(
304
        &self,
305
        identity_hint: Option<&[u8]>,
306
        _session: &CoapClientSession<'_>,
307
    ) -> Option<PskKey<'a>> {
308
        let key = self.as_ref();
309
        if identity_hint.is_none() || key.identity() == identity_hint {
310
            Some(key.clone())
311
        } else {
312
            None
313
        }
314
    }
315
}
316

            
317
/// Raw PSK identity hint callback that can be provided to libcoap.
318
///
319
/// # Safety
320
///
321
/// This function expects the arguments to be provided in a way that libcoap would when invoking
322
/// this function as an identity hint callback.
323
///
324
/// Additionally, `arg` must be a valid argument to [`ClientPskContext::from_raw`].
325
unsafe extern "C" fn dtls_psk_client_ih_callback(
326
    hint: *mut coap_str_const_t,
327
    session: *mut coap_session_t,
328
    userdata: *mut c_void,
329
) -> *const coap_dtls_cpsk_info_t {
330
    let session = CoapClientSession::from_raw(session);
331
    let client_context = ClientPskContext::from_raw(userdata as *const RefCell<ClientPskContextInner>);
332
    let provided_identity =
333
        NonNull::new(hint).map(|h| std::slice::from_raw_parts((*h.as_ptr()).s, (*h.as_ptr()).length));
334
    client_context.ih_callback(provided_identity, &session)
335
}