Skip to content

Commit

Permalink
feat(coap): add configuration with configured scopes
Browse files Browse the repository at this point in the history
Closes: ariel-os#338
  • Loading branch information
chrysn committed Feb 26, 2025
1 parent 94cd06d commit 9214c99
Show file tree
Hide file tree
Showing 8 changed files with 692 additions and 41 deletions.
489 changes: 477 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ hsi = "hsi"
extint = "extint" # External interrupt
synopsys = "synopsys" # Manufacturer name
COSE = "COSE" # A technology we use
EDN = "EDN" # EDN is the acronym of CBOR Diagnostic Notation

[files]
extend-exclude = [
Expand Down
5 changes: 4 additions & 1 deletion laze-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ modules:
- ariel-os/coap-server

- name: coap-server-config-storage
help: Configure the CoAP server to accept requests depending on runtime configuration
help: Configure the CoAP server to accept requests depending on build- and runtime configuration
depends:
- coap
- sw/storage
Expand All @@ -867,6 +867,9 @@ modules:
global:
FEATURES:
- ariel-os/coap-server-config-storage
PEERS_YML: ${appdir}/peers.yml
CARGO_ENV:
- PEERS_YML=${PEERS_YML}

- name: coap-server-config-unprotected
help: Configure the CoAP server to accept any request without authorization checks.
Expand Down
9 changes: 9 additions & 0 deletions src/ariel-os-coap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ hexlit = "0.5.5"
# For the udp_nal
embedded-io-async = "0.6.1"

[build-dependencies]
serde_yml = "0.0.12"
serde = "1"
# "blessed" by Cargo basing its build script API on it <https://blog.rust-lang.org/inside-rust/2024/12/13/this-development-cycle-in-cargo-1.84.html#build-script-api>
build-rs = "0.1.2"
cbor-edn = "0.0.8"
coap-numbers = "0.2"
minicbor = { version = "0.26", features = ["std"] }

[lints]
workspace = true

Expand Down
116 changes: 116 additions & 0 deletions src/ariel-os-coap/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use serde::Deserialize;
use std::fmt::Write;

/// Second-level item for deserializing a `peers.yml`
///
/// (The top level is a list thereof).
#[derive(Deserialize)]
struct Peer {
kccs: String,
scope: Scope,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Scope {
String(String),
Aif(std::collections::HashMap<String, Permission>),
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Permission {
Set(Vec<SinglePermission>),
Single(SinglePermission),
}

#[derive(Debug, Deserialize, Copy, Clone)]
#[allow(clippy::upper_case_acronyms, reason = "used to guide serde values")]
#[repr(u8)]
enum SinglePermission {
GET = coap_numbers::code::GET,
POST = coap_numbers::code::POST,
PUT = coap_numbers::code::PUT,
DELETE = coap_numbers::code::DELETE,
FETCH = coap_numbers::code::FETCH,
PATCH = coap_numbers::code::PATCH,
#[allow(non_camel_case_types, reason = "that's how that code is named")]
iPATCH = coap_numbers::code::IPATCH,
}

impl Permission {
fn mask(&self) -> u32 {
match self {
Permission::Set(p) => p.iter().fold(0, |old, value| old | value.mask()),
Permission::Single(p) => p.mask(),
}
}
}

impl SinglePermission {
/// The `Tperm` unsigned integer representation of the REST-specific AIF model described in
/// RFC9237.
fn mask(&self) -> u32 {
1 << (*self as u8 - 1)
}
}

fn main() {
if !build::cargo_feature("coap-server-config-storage") {
return;
}

build::rerun_if_env_changed("PEERS_YML");
let peers_yml = std::path::PathBuf::from(std::env::var("PEERS_YML").unwrap());

build::rerun_if_changed(&peers_yml);
let peers_file =
std::fs::File::open(peers_yml).expect("no peers.yml usable in specified location");

let peers: Vec<Peer> = serde_yml::from_reader(peers_file).expect("failed to parse peers.yml");

let mut chain_once_per_kccs = String::new();
for peer in peers {
let kccs = cbor_edn::StandaloneItem::parse(&peer.kccs)
.expect("data in kccs is not valid CBOR Diagnostic Notation (EDN)")
.to_cbor()
.expect("CBOR Diagnostic Notation (EDN) is not expressible in CBOR");
// FIXME: Should we pre-parse the KCCS and have the parsed credentials as const in flash? Or
// just parsed enough that there is no CBOR parsing but credential and material point to
// overlapping slices?
let scope = match peer.scope {
Scope::String(s) if s == "allow-all" => {
"coapcore::scope::UnionScope::AllowAll".to_string()
}
Scope::Aif(aif) => {
let data: Vec<_> = aif
.into_iter()
.map(|(toid, tperm)| (toid, tperm.mask()))
.collect();
let mut bytes = vec![];
minicbor::encode(data, &mut bytes).unwrap();
format!("coapcore::scope::UnionScope::AifValue(coapcore::scope::AifValue::parse(&{bytes:?}).unwrap())")
}
e => panic!("Scope configuration {e:?} is not recognized"),
};
write!(
chain_once_per_kccs,
".chain(core::iter::once((lakers::Credential::parse_ccs(
&{kccs:?}).unwrap(),
{scope},
)))"
)
.expect("writing to String is infallible");
}

let peers_data = format!(
"
pub(super) fn kccs() -> impl Iterator<Item=(lakers::Credential, coapcore::scope::UnionScope)> {{
core::iter::empty()
{chain_once_per_kccs}
}}
");

let peers_file = build::out_dir().join("peers.rs");
std::fs::write(peers_file, peers_data).unwrap();
}
78 changes: 50 additions & 28 deletions src/ariel-os-coap/src/stored.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use ariel_os_debug::log::{debug, info};
use cbor_macro::cbo;
use coapcore::seccfg::ServerSecurityConfig;

mod flash_peers {
include!(concat!(env!("OUT_DIR"), "/peers.rs"));
}

pub async fn server_security_config() -> impl ServerSecurityConfig {
StoredPolicy::load().await
}
Expand All @@ -18,11 +22,53 @@ impl ServerSecurityConfig for StoredPolicy {
// FIXME: Decide based on peers.rs input (but so far tokens are not implemented)
const PARSES_TOKENS: bool = false;
const HAS_EDHOC: bool = true;
type GeneralClaims = coapcore::seccfg::ConfigBuilderClaims;
type GeneralClaims = StoredClaims;

fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
Some(self.own_edhoc_credential)
}

fn expand_id_cred_x(
&self,
id_cred_x: lakers::IdCred,
) -> Option<(lakers::Credential, StoredClaims)> {
for (credential, scope) in flash_peers::kccs() {
if credential.by_kid().is_ok_and(|by_kid| by_kid == id_cred_x)
|| credential
.by_value()
.is_ok_and(|by_value| by_value == id_cred_x)
{
return Some((credential, StoredClaims { scope }));
}
}
None
}
}

/// Generates a private key and some credential matching it.
///
/// The 60 byte is kind of arbitrary; it's long enough for this, but needs to also accommodate
/// anything that gets loaded. It currently contains an Key ID b"", which is convenient because it
/// enables sending the key by reference.
fn generate_credpair() -> (heapless::Vec<u8, 60>, lakers::BytesP256ElemLen) {
use lakers::CryptoTrait;
let mut crypto = lakers_crypto_rustcrypto::Crypto::new(ariel_os_random::crypto_rng());
let (private, public) = crypto.p256_generate_key_pair();
let mut credential = heapless::Vec::from_slice(&cbo!(
r#"{
/cnf/ 8: {/ COSE_Key / 1: {
/kty/ 1: /EC2/ 2,
/kid/ 2: '',
/crv/ -1: /P-256/ 1,
/x/ -2: h'0000000000000000000000000000000000000000000000000000000000000000'
}}
}"#
))
.expect("Fits by construction");
let public_start = credential.len() - 32;
credential[public_start..].copy_from_slice(&public);
debug!("Generated private/public key pair.");
(credential, private)
}

impl StoredPolicy {
Expand All @@ -32,38 +78,14 @@ impl StoredPolicy {
// becomes a thing.
const OWN_CREDENTIAL_KEY: &str = "ariel-os-coap.own-edhoc-credential";

let mut storage = ariel_os_storage::lock().await;
let (credential, key) = match storage
.get(OWN_CREDENTIAL_KEY)
let (credential, key) = match ariel_os_storage::get(OWN_CREDENTIAL_KEY)
.await
.expect("flash error prevents startup")
{
Some(credpair) => credpair,
None => {
use lakers::CryptoTrait;
let mut crypto =
lakers_crypto_rustcrypto::Crypto::new(ariel_os_random::crypto_rng());
let (private, public) = crypto.p256_generate_key_pair();
// 60 byte is kind of arbitrary; it's long enough for this, but needs to also
// accommodate anything that gets loaded.
let mut credential = heapless::Vec::<u8, 60>::from_slice(&cbo!(
r#"{
/cnf/ 8: {/ COSE_Key / 1: {
1: 2,
/ empty key ID is handy because it enables sending by reference /
2: '',
-1: 1,
-2: h'0000000000000000000000000000000000000000000000000000000000000000'
}}
}"#
))
.expect("Fits by construction");
let public_start = credential.len() - 32;
credential[public_start..].copy_from_slice(&public);
debug!("Generated private/public key pair.");
let credpair = (credential, private);
storage
.insert(OWN_CREDENTIAL_KEY, credpair.clone())
let credpair = generate_credpair();
ariel_os_storage::insert(OWN_CREDENTIAL_KEY, credpair.clone())
.await
.expect("flash error prevents startup");
credpair
Expand Down
13 changes: 13 additions & 0 deletions tests/coap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ This application is a work in progress demo of running CoAP with OSCORE/EDHOC se
* Add `-s coap-server-config-unprotected` to the laze invocation; this replaces the demokeys setup.
* All resources are now only accessible without `--credentials`. (The "fauxhoc" script does not work in that mode).

* CoAP with more than just demo keys:
* Add `-s coap-server-config-storage` to the laze invocation; this replaces the demokeys setup.
* Alter the client.diag file have the `peer_cred` reflect the "CoAP server identity" line it procduces at startup
<!-- FIXME: should be trivial after https://github.com/knurling-rs/defmt/pull/916 -->
after running the hex values there through https://cbor.me's bytes to diagnostic converter.

The build system now reads `peers.yml`, which currently encodes the same authorization for the demo key as the demo setup,
but in a user configurable way:
You can add your own private key there, or replace the demo key, and configure resources that should be accessible.

Instead of using a hard-coded key, the device generates one at first startup,
and reports the credential that contains its public key in the standard output.

## Roadmap

Eventually, all of this should be covered by 20-line examples.
Expand Down
22 changes: 22 additions & 0 deletions tests/coap/peers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# The format of this may yet change (especially since it should eventually
# source the user's public keys from well-known locations as per
# <https://ariel-os.github.io/ariel-os/dev/docs/book/tooling/coap.html#outlook-interacting-with-an-ariel-os-coap-server-from-the-host>);
# the general gist is that it lists who may do what on the device.

- kccs: |
# The CWT Claims Set that needs to be used (by value or by reference) by
# the client to gain access to the device.
#
# It is expressed in CBOR diagnostic notation (which at the YAML level is
# just a string), and compatible with aiocoap's credentials.
{2: "42-50-31-FF-EF-37-32-39", 8: {1: {1: 2, 2: h'2b', -1: 1, -2: h'ac75e9ece3e50bfc8ed60399889522405c47bf16df96660a41298cb4307f7eb6', -3: h'6e5de611388a4b8a8211334ac7d37ecb52a387d257e6db3c2a93df21ff3affc8'}}}
scope:
# Authorizations assigned to clients authenticating with the credential
# above. Keys are paths on the device, values are single or lists of CoAP
# methods that may be performed.
#
# Instead of a dictionary, the scope can also be a single string
# "allow-all".
/stdout: [GET, FETCH]
/.well-known/core: GET
/poem: GET

0 comments on commit 9214c99

Please sign in to comment.