Skip to content

Commit

Permalink
examples: Establish CoAP playground
Browse files Browse the repository at this point in the history
  • Loading branch information
chrysn authored and kaspar030 committed Apr 26, 2024
1 parent 2255f77 commit 8f1fd41
Show file tree
Hide file tree
Showing 10 changed files with 2,431 additions and 217 deletions.
1,349 changes: 1,132 additions & 217 deletions Cargo.lock

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions examples/coap/Cargo.toml
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 = []
14 changes: 14 additions & 0 deletions examples/coap/README.md
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.
145 changes: 145 additions & 0 deletions examples/coap/fauxhoc.py
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())
10 changes: 10 additions & 0 deletions examples/coap/laze.yml
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
138 changes: 138 additions & 0 deletions examples/coap/src/main.rs
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);
}
Loading

0 comments on commit 8f1fd41

Please sign in to comment.