Skip to content

Add OSCORE Support #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open

Conversation

ln4c
Copy link

@ln4c ln4c commented Mar 20, 2025

This pull request aims to provide support for OSCORE in libcoap-rs.
It adds a new feature flag "oscore" which has to be activated to enable all functions regarding OSCORE under which we provide the following:

libcoap libcoap-rs
coap_oscore_conf_t OscoreConf::new
coap_new_oscore_recipient CoapContext::new_oscore_recipient
coap_delete_oscore_recipient CoapContext::delete_oscore_recipient
coap_new_client_session_oscore CoapClientSession::connect_oscore
coap_context_oscore_server CoapContext::oscore_server

We introduced two new structs OscoreConf and OscoreRecipient which are currently located within oscore.rs.

We also added new errors to allow for error-handling by the user in errors.rs.

There is also a minimal client and server example available, deriving from the official documentation.

Security was a priority and all unsafe calls have been annotated with an appropriate security notice, as is the standard for this project.

ln4c and others added 30 commits December 19, 2024 22:00
…ons to add oscore conf to context for servers and to add new recipients
…ence_number() using Option and pattern-matching
@pulsastrix pulsastrix self-requested a review March 20, 2025 18:51
@pulsastrix pulsastrix added this to the v0.3.0 milestone Mar 20, 2025
@pulsastrix pulsastrix added enhancement New feature or request libcoap-parity Features that libcoap offers, but libcoap-rs currently does not labels Mar 20, 2025
@pulsastrix pulsastrix linked an issue Mar 20, 2025 that may be closed by this pull request
Copy link

github-actions bot commented Mar 20, 2025

Workflow Status Report

Generated for commit b9976cf on Thu May 15 18:27:22 UTC 2025.

Test and Analyze
Docs, Coverage Report and PR Updates

In case of failure, clippy warnings and rustfmt changes (if any) will be indicated as CI check warnings in the file comparison view.

Documentation: Download

Coverage Report: Download

Note: Online versions of documentation and coverage reports may not be available indefinitely, especially after the pull request was merged.

Code Coverage Report

Coverage

Coverage target is 80%.

Expand to view coverage statistics
file coverage covered missed_lines
libcoap-sys/src/lib.rs 77.78% 133 / 171 370-381, 489-492, 500, 527-544, 669-672, 756-759
libcoap/src/context.rs 59.52% 175 / 294 130, 195-240, 251-256, 274, 303, 350-354, 387, 402-403, 593-609, 643, 648, 664, 672-834
libcoap/src/crypto/pki_rpk/key.rs 56.32% 49 / 87 179-213, 242-250, 252-255, 261-263
libcoap/src/crypto/pki_rpk/mod.rs 26.44% 87 / 329 36-275, 454-486, 507-605, 613, 616, 650-668, 678-680, 685-687, 692-694, 755, 767-903
libcoap/src/crypto/pki_rpk/pki.rs 76.15% 99 / 130 90-149, 302-315
libcoap/src/crypto/pki_rpk/rpk.rs 68.33% 41 / 60 68-98, 160-171
libcoap/src/crypto/psk/client.rs 43.31% 55 / 127 71-75, 83, 106-192, 234-242, 269-271, 276-278, 306-338
libcoap/src/crypto/psk/key.rs 55.42% 46 / 83 42-49, 163-205
libcoap/src/crypto/psk/mod.rs 0.00% 0 / 24 30-96
libcoap/src/crypto/psk/server.rs 32.43% 48 / 148 69-88, 96, 99, 116-119, 145-147, 152-154, 159-161, 189-234, 255, 271-396
libcoap/src/error.rs 0.00% 0 / 6 160-240
libcoap/src/event.rs 29.73% 11 / 37 36-168
libcoap/src/lib.rs 0.00% 0 / 33 62-165
libcoap/src/mem.rs 72.56% 119 / 164 148-163, 196-198, 210-212, 280, 296-298, 319-324, 353-355, 369, 381-393, 454-456, 473, 481-488, 507, 527, 531
libcoap/src/message/mod.rs 57.37% 183 / 319 105-108, 110-131, 139-145, 147-168, 172, 174, 176, 208, 210, 213-222, 225-243, 245-249, 265-275, 295-302, 398-399, 441, 447, 478, 488-491, 497-499, 506, 511-523, 529, 549-551, 561-563
libcoap/src/message/request.rs 33.79% 99 / 293 51, 68-202, 228-256, 264-365, 371-375, 380-381, 383-386, 389, 395-400, 430-432, 436, 439-441, 444-446, 449, 452, 455, 458, 461, 472-479
libcoap/src/message/response.rs 22.17% 45 / 203 36-160, 165, 168, 171, 174, 177, 195-325, 329-334, 360
libcoap/src/prng.rs 54.79% 40 / 73 66, 78-116, 139-149, 196
libcoap/src/protocol.rs 33.33% 54 / 162 164-166, 171-176, 178-193, 195-196, 203-208, 210-225, 227-228, 285-287, 308, 322-324, 369-374, 383-388, 444-469, 475-488, 520, 522
libcoap/src/resource.rs 67.23% 160 / 238 62, 94-96, 171-176, 183-208, 214-219, 249, 266-288, 300-303, 319-339, 351-354, 387-396, 529-531
libcoap/src/session/client.rs 87.27% 96 / 110 168, 195, 262, 270-272, 278-280, 327-333
libcoap/src/session/mod.rs 41.13% 102 / 248 62-74, 105, 115-230, 240-285, 294-310, 319-330, 359, 363, 395-415, 437, 445, 477, 484, 502-508, 583, 589
libcoap/src/session/server.rs 80.88% 55 / 68 75-77, 81, 152, 162-164, 179, 194-200
libcoap/src/transport.rs 72.73% 16 / 22 29-36, 55
libcoap/src/types.rs 54.69% 268 / 490 71-125, 131-160, 210-243, 253-265, 272-278, 284-286, 295, 371, 411, 450, 562, 571, 583, 610, 636-705, 758, 803-824, 845-868, 879-964
libcoap/tests/common/dtls.rs 93.02% 40 / 43 49, 54-55
libcoap/tests/common/mod.rs 92.00% 69 / 75 85-88, 92, 126
libcoap/tests/dtls_pki_client_server_test.rs 100.00% 65 / 65
libcoap/tests/dtls_psk_client_server_test.rs 100.00% 21 / 21
libcoap/tests/dtls_rpk_client_server_test.rs 100.00% 6 / 6
libcoap/tests/tcp_client_server_test.rs 100.00% 17 / 17
libcoap/tests/udp_client_server_test.rs 100.00% 17 / 17

Total coverage: 53.23%

Copy link
Member

@pulsastrix pulsastrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for the contribution!
You can find my initial comments below.

Regarding the examples: Would it be possible to add an abridged version of the examples (without the EDHOC part) to the examples subdirectory or to the module-level rustdoc in oscore.rs?
That way, we can use those as (no_run) tests.

Lastly, it would also be nice if we had a runnable test case that runs both a server and a client and performs a basic request, akin to the test cases already present for UDP and DTLS.

Comment on lines 32 to 33
// SAFETY: It is expected, that the user provides valid oscore_conf bytes. In case of
// failure this will return null which will result in an error being thrown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This safety comment does not fully explain why the function call itself is safe.

One thing that should at least be mentioned here is that conf is copied internally in the call to coap_new_oscore_conf and therefore doesn't have to live longer than that call.
Without looking at the source code of libcoap, this is not obvious to a reader, and the required lifetime of conf is important wrt. memory safety.

In general, safety comments should mainly focus on aspects of "safety" in Rust terminology (mainly memory safety and aliasing rules) and less about regular error handling that does not affect memory safety.

In a nutshell, safety comments for unsafe blocks should be a statement of the form "these invariants are expected by the content of the unsafe block, and I have made sure that these invariants hold because of [...]".
In contrast, functions and traits declared as unsafe should have a safety section in their Rustdoc of the form "to not break Rust safety guarantees, you must uphold the following invariants if you use this piece of code".

That way, readers of the code (whether they are library users or maintainers) can quickly get an understanding of what assumptions the original author has made and what they might need to uphold themselves if they call the affected code/make changes to the code.

For example (although in most cases it doesn't have to be as verbose as this one):

Suggested change
// SAFETY: It is expected, that the user provides valid oscore_conf bytes. In case of
// failure this will return null which will result in an error being thrown.
// SAFETY:
// - The parts of the byte string referenced by conf are defensively copied if used
// by the newly created oscore_conf
// - conf may point to an arbitrary range of bytes (invalid data will not cause UB)
// - save_seq_num_func is specifically designed to work as a callback for this
// function.
// - save_seq_num_func_param may be a null pointer (save_seq_num_func does
// not use it).

Comment on lines 437 to 456
// SAFETY: Properly initialized CoapContext always has a valid raw_context that is not deleted until
// the CoapContextInner is dropped. OscoreConf raw_conf should be valid, else return an error.
//
// coap_context_oscore_server will also always free the raw_conf, regardless of the result:
// [libcoap docs](https://libcoap.net/doc/reference/4.3.5/group__oscore.html#ga71ddf56bcd6d6650f8235ee252fde47f)
unsafe {
result = coap_context_oscore_server(inner_ref.raw_context, oscore_conf.as_mut_raw_conf()?);
};

// Invalidate the OscoreConf raw_conf as its freed by the call above.
oscore_conf.raw_conf_valid = false;

// Check whether adding the config to the context failed.
if result == 0 {
return Err(OscoreServerCreationError::Unknown);
}

// Add the initial_recipient (if present).
if let Some(initial_recipient) = oscore_conf.initial_recipient.clone() {
let initial_recipient = OscoreRecipient::new(initial_recipient.as_str());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be easier to represent by having a function OscoreConf::into_raw_conf(self) -> (*mut coap_oscore_conf_t, Option<OscoreRecipient>) that consumes the config?

That way, you can avoid managing a separate field to check that the pointer is still usable (the pointer being valid would be guaranteed/an invariant for the lifetime of the OscoreConf instance.

Comment on lines 499 to 514
// SAFETY: If adding the recipient to the context fails we drop its underlying raw
// struct manually as it's not handled by the raw_context. There is one case where
// libcoap would already free the raw struct, which is filtered out above as we
// prevent adding a duplicate recipient_id to the context and return.
unsafe {
result = coap_new_oscore_recipient(inner_ref.raw_context, recipient.get_c_struct());
};

// Drop the raw struct if adding it failed (except for duplicate id)...
// SAFETY: This should be safe to use here as 'coap_new_oscore_recipient()' would only
// free() the raw struct on failure due to a duplicate recipient_id, which is filtered
// out above.
if result == 0 {
recipient.drop();
return Err(OscoreRecipientError::Unknown);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like with the OscoreConf: have a method OscoreRecipient::into_raw(self) ->*mut coap_bin_const_t to convert the OscoreRecipient into the raw pointer, that way you don't need to manage the internal state of the raw pointer.

}

// ...or else save the recipient to keep alive the pointer to its raw struct.
inner_ref.recipients.push(recipient);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw pointers are not freed if the struct containing them is dropped, so you might not even have to keep the OscoreRecipient stored at all.

In fact, you might also get away with not having an OscoreRecipient struct at all and just taking a recipient_id: &str as arguments for this method and delete_oscore_recipient() instead.

Comment on lines 105 to 111
// The user only supplies the recipients ID, we will build the recipients C struct here.
let recipient = coap_bin_const_t {
length: recipient_id.len(),
s: recipient_id.as_ptr(),
};

let recipient: *mut coap_bin_const_t = Box::into_raw(Box::new(recipient));
Copy link
Member

@pulsastrix pulsastrix Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointers created by Box::into_raw can not necessarily be freed by libcoap.
To more more specific, providing a different global allocator to Rust code will cause calls to free() a Rust-made pointer in libcoap to fail, see https://doc.rust-lang.org/std/boxed/index.html#memory-layout

Use libcoap's own coap_new_bin_const() function instead. That way, it is guaranteed that the created pointer can also be freed by libcoap:

Suggested change
// The user only supplies the recipients ID, we will build the recipients C struct here.
let recipient = coap_bin_const_t {
length: recipient_id.len(),
s: recipient_id.as_ptr(),
};
let recipient: *mut coap_bin_const_t = Box::into_raw(Box::new(recipient));
// The user only supplies the recipients ID, we will build the recipients C struct here.
// SAFETY: provided pointer and length point to a valid byte string usable by coap_new_bin_const()
let recipient = unsafe { coap_new_bin_const(recipient_id.as_ptr(), recipient_id.len()) };

EDIT: Note that if you apply this change, you also have to replace instances of Box::from_raw for this pointer with coap_delete_bin_const().

Comment on lines 130 to 141
/// Drops the recipient from memory.
/// This will trigger a double free if coap_bin_const_t has already been freed!
/// WARNING: THIS SHOULD NEVER BE CALLED UNLESS YOU'RE SURE THE coap_bin_const_t HAS NOT BEEN FREED BEFORE!
pub(crate) fn drop(&self) {
// SAFETY: Currently, this is only used in 'add_new_oscore_recipient()' in case the recipient
// is not added to the context. There is currently only one exception, which is filtered out,
// because trying to add a duplicate recipient to the oscore context would already trigger a
// free() in libcoap.
unsafe {
let _ = Box::from_raw(self.get_c_struct());
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As indicated by your warning message, this must be marked unsafe (but also see my comments in context.rs on why you might not need the OscoreRecipient struct at all).

Comment on lines 214 to 231
// SAFETY: self.raw_context is guaranteed to be valid, local_if can be null.
// OscoreConf raw_conf should be valid, else we return an error.
//
// coap_new_client_session_oscore should free the raw_conf:
// [libcoap docs](https://libcoap.net/doc/reference/4.3.5/group__oscore.html#ga65ac1a57ebc037b4d14538c8e21c28a7)
let session = unsafe {
coap_new_client_session_oscore(
ctx.as_mut_raw_context(),
std::ptr::null(),
CoapAddress::from(addr).as_raw_address(),
coap_proto_t_COAP_PROTO_UDP,
oscore_conf.as_mut_raw_conf()?,
)
};

// Invalidate the OscoreConf raw_conf as it's freed by the call above, so we don't try to
// free it again in the future, which would cause a double free().
oscore_conf.raw_conf_valid = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in context.rs: replace as_mut_raw_conf() with a method that consumes OscoreConf to avoid manually managing the state of the oscore_conf instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request libcoap-parity Features that libcoap offers, but libcoap-rs currently does not
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OSCORE support
3 participants