-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
2,431 additions
and
217 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
[package] | ||
name = "coap" | ||
version = "0.1.0" | ||
authors = ["Christian Amsüss <chrysn@fsfe.org>"] | ||
edition.workspace = true | ||
publish = false | ||
|
||
[dependencies] | ||
embassy-executor = { workspace = true, default-features = false } | ||
embassy-net = { workspace = true, features = ["udp"] } | ||
embassy-time = { workspace = true, default-features = false } | ||
embedded-io-async = "0.6.1" | ||
heapless = { workspace = true } | ||
riot-rs = { path = "../../src/riot-rs", features = [ | ||
"override-network-config", | ||
"random", | ||
"csprng", | ||
] } | ||
riot-rs-boards = { path = "../../src/riot-rs-boards" } | ||
|
||
# for the udp_nal mod | ||
embedded-nal-async = "0.7" | ||
# actually patched with https://github.com/smoltcp-rs/smoltcp/pull/904 but | ||
# patch.crates-io takes care of that | ||
smoltcp = { version = "0.11", default-features = false } | ||
embedded-nal-coap = "0.1.0-alpha.2" | ||
coap-request = "0.2.0-alpha.2" | ||
coap-message = "0.3.1" | ||
embassy-futures = "0.1.1" | ||
coap-message-demos = { version = "0.4.0", default-features = false } | ||
coap-request-implementations = "0.1.0-alpha.4" | ||
lakers = { version = "0.5.1", default-features = false } | ||
lakers-crypto-rustcrypto = "0.5.1" | ||
coap-handler = "0.2.0" | ||
coap-message-utils = "0.3.2" | ||
coap-handler-implementations = "0.5.0" | ||
hexlit = "0.5.5" | ||
coap-numbers = "0.2.3" | ||
minicbor = "0.23.0" | ||
|
||
liboscore = { git = "https://gitlab.com/oscore/liboscore/", rev = "e7a4ecd037cbb9c7f085047fec5896f4bdc68d50" } | ||
liboscore-msgbackend = { git = "https://gitlab.com/oscore/liboscore/", features = [ | ||
"alloc", | ||
], rev = "e7a4ecd037cbb9c7f085047fec5896f4bdc68d50" } | ||
coap-message-implementations = "0.1.1" | ||
static-alloc = "0.2.5" | ||
|
||
[features] | ||
default = ["proto-ipv4"] # shame | ||
# actually embedded-nal features, we have to match them here while developing udp_nal in here | ||
proto-ipv4 = [] | ||
proto-ipv6 = [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# coap demo | ||
|
||
## About | ||
|
||
This application is a work in progress demo of running CoAP with OSCORE/EDHOC security on RIOT-rs. | ||
|
||
## Roadmap | ||
|
||
Eventually, this should be a 20 line demo. | ||
|
||
Until the CoAP roadmap is complete, | ||
this serves as a work bench, test bed, demo zone and playground at the same time. | ||
This application will grow as parts of the roadmap are added, | ||
and shrink as they become default or are wrapped into components of RIOT-rs. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
""" | ||
fake-it-until-you-make-it wrapper for sending an EDHOC+OSCORE request | ||
Its first stage did roughly | ||
~~~ | ||
$ ./aiocoap-client 'coap://10.42.0.61/.well-known/edhoc' -m POST --content-format application/cbor-seq --payload "[true, 3, 2, h'0000000000000000000000000000000000000000000000000000000000000000', 0]" | ||
~~~ | ||
and the later guessed the C_R and sent a garbled OSCORE request that triggered | ||
the EDHOC CoAP pathways. | ||
Now it does EDHOC using lakers to a point where the name doesn't fit any more… | ||
""" | ||
|
||
import asyncio | ||
import cbor2 | ||
from aiocoap import * | ||
from aiocoap import numbers, oscore | ||
|
||
import lakers | ||
|
||
# Someone told us that these are the credentials of devices that are our legitimate peers | ||
eligible_responders_ccs = { | ||
bytes.fromhex( | ||
"A2026008A101A5010202410A2001215820BBC34960526EA4D32E940CAD2A234148DDC21791A12AFBCBAC93622046DD44F02258204519E257236B2A0CE2023F0931F1F386CA7AFDA64FCDE0108C224C51EABF6072" | ||
) | ||
} | ||
eligible_responders = {} # mapping ID_CRED_R to CRED_R | ||
# when ID_CRED_R is the KID. 8/1/2 is cnf/COSE_Key/kid, IIUC those should be present in suitable CCSs | ||
eligible_responders |= { | ||
parsed[8][1][2]: ccs | ||
for (parsed, ccs) in ((cbor2.loads(ccs), ccs) for ccs in eligible_responders_ccs) | ||
} | ||
# when ID_CRED_R is CRED_R | ||
eligible_responders |= {ccs: ccs for ccs in eligible_responders_ccs} | ||
|
||
CRED_I = bytes.fromhex( | ||
"A2027734322D35302D33312D46462D45462D33372D33322D333908A101A5010202412B2001215820AC75E9ECE3E50BFC8ED60399889522405C47BF16DF96660A41298CB4307F7EB62258206E5DE611388A4B8A8211334AC7D37ECB52A387D257E6DB3C2A93DF21FF3AFFC8" | ||
) | ||
I = bytes.fromhex("fb13adeb6518cee5f88417660841142e830a81fe334380a953406a1305e8706b") | ||
|
||
|
||
class EdhocSecurityContext( | ||
oscore.CanProtect, oscore.CanUnprotect, oscore.SecurityContextUtils | ||
): | ||
def __init__(self, initiator, c_ours, c_theirs): | ||
# initiator could also be responder, and only this line would need to change | ||
# FIXME Only ByReference implemented in edhoc.rs so far | ||
self.message_3, _i_prk_out = initiator.prepare_message_3( | ||
lakers.CredentialTransfer.ByReference, None | ||
) | ||
|
||
if initiator.selected_cipher_suite() == 2: | ||
self.alg_aead = oscore.algorithms["AES-CCM-16-64-128"] | ||
self.hashfun = oscore.hashfunctions["sha256"] | ||
else: | ||
raise RuntimeError("Unknown suite") | ||
|
||
# we check critical EADs, no out-of-band agreement, so 8 it is | ||
oscore_salt_length = 8 | ||
# I figure that one would be ageed out-of-band as well | ||
self.id_context = None | ||
self.recipient_replay_window = oscore.ReplayWindow(32, lambda: None) | ||
|
||
master_secret = initiator.edhoc_exporter(0, [], self.alg_aead.key_bytes) | ||
master_salt = initiator.edhoc_exporter(1, [], oscore_salt_length) | ||
print(f"Derived {master_secret=} {master_salt=}") | ||
|
||
self.sender_id = cbor2.dumps(c_theirs) | ||
self.recipient_id = cbor2.dumps(c_ours) | ||
if self.sender_id == self.recipient_id: | ||
raise ValueError("Bad IDs: identical ones were picked") | ||
|
||
self.derive_keys(master_salt, master_secret) | ||
|
||
self.sender_sequence_number = 0 | ||
self.recipient_replay_window.initialize_empty() | ||
|
||
def post_seqnoincrease(self): | ||
pass | ||
|
||
def protect(self, message, request_id=None, *, kid_context=True): | ||
outer_message, request_id = super().protect( | ||
message, request_id=request_id, kid_context=kid_context | ||
) | ||
if self.message_3 is not None: | ||
outer_message.opt.edhoc = True | ||
outer_message.payload = self.message_3 + outer_message.payload | ||
self.message_3 = None | ||
return outer_message, request_id | ||
|
||
|
||
async def main(): | ||
ctx = await Context.create_client_context() | ||
|
||
priv, pub = lakers.p256_generate_key_pair() | ||
|
||
c_i = 0x08 | ||
initiator = lakers.EdhocInitiator() | ||
message_1 = initiator.prepare_message_1(c_i=c_i) | ||
|
||
msg1 = Message( | ||
code=POST, | ||
uri="coap://10.42.0.61/.well-known/edhoc", | ||
payload=cbor2.dumps(True) + message_1, | ||
# payload=b"".join(cbor2.dumps(x) for x in [True, 3, 2, b'\0' * 32, 0]), | ||
) | ||
msg2 = await ctx.request(msg1).response_raising | ||
|
||
(c_r, id_cred_r, ead_2) = initiator.parse_message_2(msg2.payload) | ||
# https://github.com/openwsn-berkeley/lakers/issues/256 -- conveniently, | ||
# after it is fixed, the line becomes a no-op, so this can stay in until a | ||
# Lakers release to PyPI happened. | ||
id_cred_r = bytes(id_cred_r) | ||
|
||
print(f"Received MSG2 with {c_r=} {id_cred_r=} {ead_2=}") | ||
|
||
cred_r = eligible_responders[id_cred_r] | ||
initiator.verify_message_2( | ||
I, CRED_I, cred_r | ||
) # odd that we provide that here rather than in the next function | ||
|
||
oscore_context = EdhocSecurityContext(initiator, c_i, c_r) | ||
|
||
ctx.client_credentials["coap://10.42.0.61/*"] = oscore_context | ||
|
||
msg3 = Message( | ||
code=GET, | ||
uri="coap://10.42.0.61/.well-known/core", | ||
) | ||
|
||
print((await ctx.request(msg3).response_raising).payload) | ||
|
||
normalrequest = Message( | ||
code=GET, | ||
uri="coap://10.42.0.61/poem", | ||
) | ||
print((await ctx.request(normalrequest).response).payload) | ||
|
||
await ctx.shutdown() | ||
|
||
|
||
if __name__ == "__main__": | ||
asyncio.run(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
apps: | ||
- name: coap | ||
env: | ||
global: | ||
CARGO_ENV: | ||
- CONFIG_ISR_STACKSIZE=16384 | ||
selects: | ||
- ?release | ||
- network | ||
- random |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
#![no_main] | ||
#![no_std] | ||
#![feature(type_alias_impl_trait)] | ||
#![feature(used_with_arg)] | ||
|
||
use riot_rs::{debug::println, embassy::network}; | ||
|
||
use embassy_net::udp::{PacketMetadata, UdpSocket}; | ||
|
||
// Moving work from https://github.com/embassy-rs/embassy/pull/2519 in here for the time being | ||
mod udp_nal; | ||
|
||
mod seccontext; | ||
|
||
#[riot_rs::task(autostart)] | ||
async fn coap_run() { | ||
let stack = network::network_stack().await.unwrap(); | ||
|
||
// FIXME trim to CoAP requirements | ||
let mut rx_meta = [PacketMetadata::EMPTY; 16]; | ||
let mut rx_buffer = [0; 4096]; | ||
let mut tx_meta = [PacketMetadata::EMPTY; 16]; | ||
let mut tx_buffer = [0; 4096]; | ||
|
||
let socket = UdpSocket::new( | ||
stack, | ||
&mut rx_meta, | ||
&mut rx_buffer, | ||
&mut tx_meta, | ||
&mut tx_buffer, | ||
); | ||
|
||
println!("Starting up CoAP server"); | ||
|
||
// Can't that even bind to the Any address?? | ||
// let local_any = "0.0.0.0:5683".parse().unwrap(); // shame | ||
let local_any = "10.42.0.61:5683".parse().unwrap(); // shame | ||
let unconnected = udp_nal::UnconnectedUdp::bind_multiple(socket, local_any) | ||
.await | ||
.unwrap(); | ||
|
||
run(unconnected).await; | ||
} | ||
|
||
// FIXME: So far, this is necessary boiler plate; see ../README.md#networking for details | ||
#[riot_rs::config(network)] | ||
fn network_config() -> embassy_net::Config { | ||
use embassy_net::Ipv4Address; | ||
|
||
embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { | ||
address: embassy_net::Ipv4Cidr::new(Ipv4Address::new(10, 42, 0, 61), 24), | ||
dns_servers: heapless::Vec::new(), | ||
gateway: Some(Ipv4Address::new(10, 42, 0, 1)), | ||
}) | ||
} | ||
|
||
// Rest is from coap-message-demos/examples/std_embedded_nal_coap.rs | ||
|
||
/// This function works on *any* UdpFullStack, including embedded ones -- only main() is what makes | ||
/// this use POSIX sockets. (It does make use of a std based RNG, but that could be passed in just | ||
/// as well for no_std operation). | ||
async fn run<S>(mut sock: S) | ||
where | ||
S: embedded_nal_async::UnconnectedUdp, | ||
{ | ||
use coap_handler_implementations::HandlerBuilder; | ||
|
||
let log = None; | ||
|
||
let mut secpool: seccontext::SecContextPool = Default::default(); | ||
|
||
use hexlit::hex; | ||
const R: &[u8] = &hex!("72cc4761dbd4c78f758931aa589d348d1ef874a7e303ede2f140dcf3e6aa4aac"); | ||
let own_identity = ( | ||
&lakers::CredentialRPK::new(lakers::EdhocMessageBuffer::new_from_slice(&hex!("A2026008A101A5010202410A2001215820BBC34960526EA4D32E940CAD2A234148DDC21791A12AFBCBAC93622046DD44F02258204519E257236B2A0CE2023F0931F1F386CA7AFDA64FCDE0108C224C51EABF6072")).expect("Credential should be small enough")).expect("Credential should be processable"), | ||
R, | ||
); | ||
|
||
let mut handler = coap_message_demos::full_application_tree(log); | ||
|
||
let mut handler = seccontext::OscoreEdhocHandler::new(own_identity, handler); | ||
|
||
println!("Server is ready."); | ||
|
||
let coap = embedded_nal_coap::CoAPShared::<3>::new(); | ||
let (client, server) = coap.split(); | ||
|
||
// going with an embassy_futures join instead of an async_std::task::spawn b/c CoAPShared is not | ||
// Sync, and async_std expects to work in multiple threads | ||
embassy_futures::join::join( | ||
async { | ||
server | ||
.run(&mut sock, &mut handler, &mut riot_rs::random::fast_rng()) | ||
.await | ||
.expect("UDP error") | ||
}, | ||
run_client_operations(client), | ||
) | ||
.await; | ||
} | ||
|
||
/// In parallel to server operation, this function performs some operations as a client. | ||
/// | ||
/// This doubles as an experimentation ground for the client side of embedded_nal_coap and | ||
/// coap-request in general. | ||
async fn run_client_operations<const N: usize>( | ||
client: embedded_nal_coap::CoAPRuntimeClient<'_, N>, | ||
) { | ||
// shame | ||
let demoserver = "10.42.0.1:1234".parse().unwrap(); | ||
|
||
use coap_request::Stack; | ||
println!("Sending GET to {}...", demoserver); | ||
let response = client | ||
.to(demoserver) | ||
.request( | ||
coap_request_implementations::Code::get() | ||
.with_path("/other/separate") | ||
.processing_response_payload_through(|p| { | ||
println!("Got payload {:?}", p); | ||
}), | ||
) | ||
.await; | ||
println!("Response {:?}", response); | ||
|
||
let req = coap_request_implementations::Code::post().with_path("/uppercase"); | ||
|
||
println!("Sending POST..."); | ||
let mut response = client.to(demoserver); | ||
let response = response.request( | ||
req.with_request_payload_slice(b"Set time to 1955-11-05") | ||
.processing_response_payload_through(|p| { | ||
println!("Uppercase is {}", core::str::from_utf8(p).unwrap()) | ||
}), | ||
); | ||
let response = response.await; | ||
println!("Response {:?}", response); | ||
} |
Oops, something went wrong.