Skip to main content

liboscore/
lib.rs

1//! Wrapper around the `liboscore` C library, which implements the [OSCORE
2//! RFC](https://datatracker.ietf.org/doc/html/rfc8613), and thus symmetric encryption for the
3//! [CoAP protocol](https://datatracker.ietf.org/doc/html/rfc7252).
4//!
5//! To ensure proper setup and teardown of data structures that include references, several
6//! functions in this library take callbacks in which the accessed data is used; for example, an
7//! encrypted message is passed to [`protect_request()`] along with a callback, and the plaintext
8//! is then passed back to the caller in that callback closure inside `protect_request()`. While
9//! nothing in this library is a potentially-blocking operation (and thus worht async'ifying),
10//! there might be asynchronous code in callbacks: Therefore, asynchronous versions of the
11//! functions are provided that take asynchronous callbacks. They are functionally identical.
12//! Future versions of this crate might consider other idioms rather than callbacks (e.g.
13//! destructors) to avoid going through callbacks altogether.
14#![no_std]
15// We need these linked in
16extern crate liboscore_cryptobackend;
17extern crate liboscore_msgbackend;
18
19use core::mem::MaybeUninit;
20
21mod platform;
22
23// FIXME: pub only for tests?
24pub mod raw;
25
26mod impl_message;
27pub use impl_message::ProtectedMessage;
28
29mod oscore_option;
30pub use oscore_option::OscoreOption;
31
32mod algorithms;
33pub use algorithms::{AeadAlg, AlgorithmNotSupported, HkdfAlg};
34
35mod primitive;
36pub use primitive::{DeriveError, PrimitiveContext, PrimitiveImmutables};
37
38#[track_caller]
39#[inline]
40fn poll_once_expect_immediate<R>(future: impl core::future::Future<Output = R>) -> R {
41    let future = core::pin::pin!(future);
42    let mut context = core::task::Context::from_waker(core::task::Waker::noop());
43    match future.poll(&mut context) {
44        core::task::Poll::Ready(r) => r,
45        core::task::Poll::Pending => unreachable!("The function's only await point is in the callback, and the provided callback has none."),
46    }
47}
48
49#[non_exhaustive]
50#[derive(Debug, Copy, Clone)]
51pub enum PrepareError {
52    /// The security context can not provide protection for this message
53    SecurityContextUnavailable,
54}
55
56impl PrepareError {
57    /// Construct a Rust error type out of the C type
58    ///
59    /// This returns a result to be easily usable with the `?` operator.
60    fn new(input: raw::oscore_prepare_result) -> Result<(), Self> {
61        match input {
62            raw::oscore_prepare_result_OSCORE_PREPARE_OK => Ok(()),
63            raw::oscore_prepare_result_OSCORE_PREPARE_SECCTX_UNAVAILABLE => {
64                Err(PrepareError::SecurityContextUnavailable)
65            }
66            _ => unreachable!(),
67        }
68    }
69}
70
71#[non_exhaustive]
72#[derive(Debug, Copy, Clone)]
73pub enum FinishError {
74    Size,
75    Crypto,
76}
77
78impl FinishError {
79    /// Construct a Rust error type out of the C type
80    ///
81    /// This returns a result to be easily usable with the `?` operator.
82    fn new(input: raw::oscore_finish_result) -> Result<(), Self> {
83        match input {
84            raw::oscore_finish_result_OSCORE_FINISH_OK => Ok(()),
85            raw::oscore_finish_result_OSCORE_FINISH_ERROR_SIZE => Err(FinishError::Size),
86            raw::oscore_finish_result_OSCORE_FINISH_ERROR_CRYPTO => Err(FinishError::Crypto),
87            _ => unreachable!(),
88        }
89    }
90}
91
92#[derive(Debug, Copy, Clone)]
93pub enum ProtectError {
94    Prepare(PrepareError),
95    Finish(FinishError),
96}
97
98impl From<PrepareError> for ProtectError {
99    fn from(e: PrepareError) -> Self {
100        ProtectError::Prepare(e)
101    }
102}
103
104impl From<FinishError> for ProtectError {
105    fn from(e: FinishError) -> Self {
106        ProtectError::Finish(e)
107    }
108}
109
110#[non_exhaustive]
111#[derive(Debug, Copy, Clone)]
112pub enum UnprotectRequestError {
113    Duplicate,
114    Invalid,
115}
116
117impl UnprotectRequestError {
118    /// Construct a Rust error type out of the C type
119    ///
120    /// This returns a result to be easily usable with the `?` operator.
121    fn new(input: raw::oscore_unprotect_request_result) -> Result<(), Self> {
122        match input {
123            raw::oscore_unprotect_request_result_OSCORE_UNPROTECT_REQUEST_OK => Ok(()),
124            raw::oscore_unprotect_request_result_OSCORE_UNPROTECT_REQUEST_DUPLICATE => {
125                Err(UnprotectRequestError::Duplicate)
126            }
127            raw::oscore_unprotect_request_result_OSCORE_UNPROTECT_REQUEST_INVALID => {
128                Err(UnprotectRequestError::Invalid)
129            }
130            _ => unreachable!(),
131        }
132    }
133}
134
135#[non_exhaustive]
136#[derive(Debug, Copy, Clone)]
137pub enum UnprotectResponseError {
138    Invalid,
139}
140
141impl UnprotectResponseError {
142    /// Construct a Rust error type out of the C type
143    ///
144    /// This returns a result to be easily usable with the `?` operator.
145    fn new(input: raw::oscore_unprotect_response_result) -> Result<(), Self> {
146        match input {
147            raw::oscore_unprotect_response_result_OSCORE_UNPROTECT_RESPONSE_OK => Ok(()),
148            raw::oscore_unprotect_response_result_OSCORE_UNPROTECT_RESPONSE_INVALID => {
149                Err(UnprotectResponseError::Invalid)
150            }
151            _ => unreachable!(),
152        }
153    }
154}
155
156/// Protects an OSCORE request.
157///
158/// The message into which the ciphertext is to be written is passed in as `request`; the actual
159/// writing happens while there is a protected message configured during a callback.
160///
161/// Along with any output of the callback, this produces a request identity that will be needed
162/// later to unprotect the response.
163///
164/// For sync callbacks, see [`protect_request`].
165// FIXME we should carry the context around, but that'd require it to have a shared portion that we
166// can then clone and combine with the oscore_requestid_t.
167pub async fn async_protect_request<R>(
168    request: impl liboscore_msgbackend::WithMsgNative,
169    ctx: &mut PrimitiveContext,
170    writer: impl AsyncFnOnce(&mut ProtectedMessage) -> R,
171) -> Result<(raw::oscore_requestid_t, R), ProtectError> {
172    request
173        .async_with_msg_native(async |msg| {
174            let mut plaintext = MaybeUninit::uninit();
175            let mut request_data = MaybeUninit::uninit();
176            // Safety: Everything that needs to be initialized is
177            let prepare_ok = unsafe {
178                raw::oscore_prepare_request(
179                    msg,
180                    plaintext.as_mut_ptr(),
181                    ctx.as_mut(),
182                    request_data.as_mut_ptr(),
183                )
184            };
185            PrepareError::new(prepare_ok)?;
186            // Safety: Initialized after successful return
187            let plaintext = unsafe { plaintext.assume_init() };
188            let request_data = unsafe { request_data.assume_init() };
189
190            let mut plaintext = crate::ProtectedMessage::new(plaintext);
191            let user_carry = writer(&mut plaintext).await;
192            plaintext.flush();
193            let mut plaintext = plaintext.into_inner();
194
195            let mut returned_msg = MaybeUninit::uninit();
196            // Safety: Everything that needs to be initialized is
197            let finish_ok =
198                unsafe { raw::oscore_encrypt_message(&mut plaintext, returned_msg.as_mut_ptr()) };
199            FinishError::new(finish_ok)?;
200            // We're discarding the native message that's in returned_msg. If it were owned (which
201            // would be a valid choice for with_inmemory_write), the closure might be required to
202            // return it, but it currently isn't.
203
204            Ok((request_data, user_carry))
205        })
206        .await
207}
208
209/// Synchronous version of [`async_protect_request`], see there.
210pub fn protect_request<R>(
211    request: impl liboscore_msgbackend::WithMsgNative,
212    ctx: &mut PrimitiveContext,
213    writer: impl FnOnce(&mut ProtectedMessage) -> R,
214) -> Result<(raw::oscore_requestid_t, R), ProtectError> {
215    poll_once_expect_immediate(async_protect_request(request, ctx, async |m| writer(m)))
216}
217
218/// Unprotects an OSCORE request.
219///
220/// The message from which the ciphertext is read in as `request` (and taken mutably because it is
221/// decrypted in-place, rendering it nonsensical to any other CoAP processing); the actual
222/// processing of the plaintext happens while there is a protected message configured during a
223/// callback.
224///
225/// Along with the output of the callback, this produces a request identity that will be needed to
226/// later protect the response(s).
227///
228/// For sync callbacks, see [`unprotect_request`].
229pub async fn async_unprotect_request<R>(
230    request: impl liboscore_msgbackend::WithMsgNative,
231    oscoreoption: OscoreOption<'_>, // Here's where we need to cheat a bit: We both take the message
232    // writably, *and* we take data out of that message through
233    // another pointer. This is legal because we don't alter any
234    // option values, or more precisely, we don't alter the OSCORE
235    // option's value, but yet it's slightly uncomfortable (and
236    // users may need to resort to unsafe to call this).
237    ctx: &mut PrimitiveContext,
238    reader: impl AsyncFnOnce(&ProtectedMessage) -> R,
239) -> Result<(raw::oscore_requestid_t, R), UnprotectRequestError> {
240    request
241        .async_with_msg_native(async |nativemsg| {
242            let mut plaintext = MaybeUninit::uninit();
243            let mut request_data = MaybeUninit::uninit();
244            let decrypt_ok = unsafe {
245                raw::oscore_unprotect_request(
246                    nativemsg,
247                    plaintext.as_mut_ptr(),
248                    &oscoreoption.into_inner(),
249                    ctx.as_mut(),
250                    request_data.as_mut_ptr(),
251                )
252            };
253            // We could introduce extra handling of Invalid if our handlers had a notion of being (even
254            // security-wise) idempotent, or if we supported B.1 recovery here.
255            UnprotectRequestError::new(decrypt_ok)?;
256
257            let plaintext = unsafe { plaintext.assume_init() };
258            let request_data = unsafe { request_data.assume_init() };
259
260            let plaintext = ProtectedMessage::new(plaintext);
261
262            let user_data = reader(&plaintext).await;
263
264            unsafe { raw::oscore_release_unprotected(&mut plaintext.into_inner()) };
265
266            Ok((request_data, user_data))
267        })
268        .await
269}
270
271/// Synchronous version of [`async_unprotect_request`], see there.
272pub fn unprotect_request<R>(
273    request: impl liboscore_msgbackend::WithMsgNative,
274    oscoreoption: OscoreOption<'_>,
275    ctx: &mut PrimitiveContext,
276    reader: impl FnOnce(&ProtectedMessage) -> R,
277) -> Result<(raw::oscore_requestid_t, R), UnprotectRequestError> {
278    poll_once_expect_immediate(async_unprotect_request(
279        request,
280        oscoreoption,
281        ctx,
282        async |m| reader(m),
283    ))
284}
285
286/// Protects an OSCORE response.
287///
288/// The message into which the ciphertext is to be written is passed in as `response`; the actual
289/// writing happens while there is a protected message configured during a callback.
290///
291/// The `correlation` data (to be carried over from the corresponding request operation) is not
292/// consumed, but altered: if at all, only the first response can use the nonce of the request.
293/// Later uses will create larger responses that include an own nonce.
294pub async fn async_protect_response<R>(
295    response: impl liboscore_msgbackend::WithMsgNative,
296    ctx: &mut PrimitiveContext,
297    correlation: &mut raw::oscore_requestid_t,
298    writer: impl AsyncFnOnce(&mut ProtectedMessage) -> R,
299) -> Result<R, ProtectError> {
300    response
301        .async_with_msg_native(async |nativemsg| {
302            let mut plaintext = MaybeUninit::uninit();
303            // Safety: Everything that needs to be initialized is
304            let prepare_ok = unsafe {
305                raw::oscore_prepare_response(
306                    nativemsg,
307                    plaintext.as_mut_ptr(),
308                    ctx.as_mut(),
309                    correlation,
310                )
311            };
312            PrepareError::new(prepare_ok)?;
313            // Safety: Initialized after successful return
314            let plaintext = unsafe { plaintext.assume_init() };
315
316            let mut plaintext = crate::ProtectedMessage::new(plaintext);
317            let user_data = writer(&mut plaintext).await;
318            plaintext.flush();
319            let mut plaintext = plaintext.into_inner();
320
321            let mut returned_msg = MaybeUninit::uninit();
322            // Safety: Everything that needs to be initialized is
323            let finish_ok =
324                unsafe { raw::oscore_encrypt_message(&mut plaintext, returned_msg.as_mut_ptr()) };
325            FinishError::new(finish_ok)?;
326            // We're discarding the native message that's in returned_msg. If it were owned (which
327            // would be a valid choice for with_inmemory_write), the closure might be required to
328            // return it, but it currently isn't.
329
330            Ok(user_data)
331        })
332        .await
333}
334
335/// Synchronous version of [`async_protect_response`], see there.
336pub fn protect_response<R>(
337    response: impl liboscore_msgbackend::WithMsgNative,
338    ctx: &mut PrimitiveContext,
339    correlation: &mut raw::oscore_requestid_t,
340    writer: impl FnOnce(&mut ProtectedMessage) -> R,
341) -> Result<R, ProtectError> {
342    poll_once_expect_immediate(async_protect_response(
343        response,
344        ctx,
345        correlation,
346        async |m| writer(m),
347    ))
348}
349
350/// Unprotects an OSCORE response.
351///
352/// The message from which the ciphertext is read in as `response` (and taken mutably because it is
353/// decrypted in-place, rendering it nonsensical to any other CoAP processing); the actual
354/// processing of the plaintext happens while there is a protected message configured during a
355/// callback.
356///
357/// The `correlation` data is to be carried over from the corresponding request operation.
358//
359// A narrower set of requirements than &MutableWritableMessage, like
360// "ReadableMessageWithMutablePayload", would suffice, but none such trait is useful outside of
361// here ... though, for CBOR decoding, maybe, where we memmove around indefinite length strings
362// into place?
363pub async fn async_unprotect_response<R>(
364    response: impl liboscore_msgbackend::WithMsgNative,
365    ctx: &mut PrimitiveContext,
366    oscoreoption: OscoreOption<'_>,
367    correlation: &mut raw::oscore_requestid_t,
368    reader: impl AsyncFnOnce(&ProtectedMessage) -> R,
369) -> Result<R, UnprotectResponseError> {
370    response
371        .async_with_msg_native(async |nativemsg| {
372            let mut plaintext = MaybeUninit::uninit();
373            let decrypt_ok = unsafe {
374                raw::oscore_unprotect_response(
375                    nativemsg,
376                    plaintext.as_mut_ptr(),
377                    &oscoreoption.into_inner(),
378                    ctx.as_mut(),
379                    correlation,
380                )
381            };
382            UnprotectResponseError::new(decrypt_ok)?;
383
384            let plaintext = unsafe { plaintext.assume_init() };
385
386            let plaintext = ProtectedMessage::new(plaintext);
387
388            let user_data = reader(&plaintext).await;
389
390            unsafe { raw::oscore_release_unprotected(&mut plaintext.into_inner()) };
391
392            Ok(user_data)
393        })
394        .await
395}
396
397/// Synchronous version of [`async_unprotect_response`], see there.
398pub fn unprotect_response<R>(
399    response: impl liboscore_msgbackend::WithMsgNative,
400    ctx: &mut PrimitiveContext,
401    oscoreoption: OscoreOption<'_>,
402    correlation: &mut raw::oscore_requestid_t,
403    reader: impl FnOnce(&ProtectedMessage) -> R,
404) -> Result<R, UnprotectResponseError> {
405    poll_once_expect_immediate(async_unprotect_response(
406        response,
407        ctx,
408        oscoreoption,
409        correlation,
410        async |m| reader(m),
411    ))
412}