diff --git a/src/ariel-os-coap/Cargo.toml b/src/ariel-os-coap/Cargo.toml index 41c6bf970..3dd1ae9e1 100644 --- a/src/ariel-os-coap/Cargo.toml +++ b/src/ariel-os-coap/Cargo.toml @@ -33,6 +33,8 @@ static_cell = "2.1.0" # FIXME: Should go out eventually hexlit = "0.5.5" +cbor-macro = "0.1.0" +cboritem = "0.1.2" # For the udp_nal embedded-io-async = "0.6.1" diff --git a/src/ariel-os-coap/src/lib.rs b/src/ariel-os-coap/src/lib.rs index 5c5330b21..79b54f372 100644 --- a/src/ariel-os-coap/src/lib.rs +++ b/src/ariel-os-coap/src/lib.rs @@ -60,15 +60,39 @@ pub async fn coap_run(handler: impl coap_handler::Handler + coap_handler::Report .await .unwrap(); + use cbor_macro::cbor; + use hexlit::hex; + + let own_key = hex!("72cc4761dbd4c78f758931aa589d348d1ef874a7e303ede2f140dcf3e6aa4aac"); + let own_credential = lakers::Credential::parse_ccs(&hex!("A2026008A101A5010202410A2001215820BBC34960526EA4D32E940CAD2A234148DDC21791A12AFBCBAC93622046DD44F02258204519E257236B2A0CE2023F0931F1F386CA7AFDA64FCDE0108C224C51EABF6072")).expect("Credential should be processable"); + + let unauthenticated_scope: &[u8] = &cbor!([["/.well-known/core", 1], ["/poem", 1]]); + let unauthenticated_scope = coapcore::scope::AifValue::try_from(unauthenticated_scope) + .expect("hard-coded scope fits this type") + .into(); + let admin_key = lakers::Credential::parse_ccs(&hex!("A2027734322D35302D33312D46462D45462D33372D33322D333908A101A5010202412B2001215820AC75E9ECE3E50BFC8ED60399889522405C47BF16DF96660A41298CB4307F7EB62258206E5DE611388A4B8A8211334AC7D37ECB52A387D257E6DB3C2A93DF21FF3AFFC8")) + .expect("hard-coded credential fits this type"); + let admin_scope: &[u8] = &cbor!([ + ["/stdout", 17 / GET and FETCH /], + ["/.well-known/core", 1], + ["/poem", 1] + ]); + let admin_scope = coapcore::scope::AifValue::try_from(admin_scope) + .expect("hard-coded scope fits this type") + .into(); + // FIXME: Should we allow users to override that? After all, this is just convenience and may // be limiting in special applications. let handler = handler.with_wkc(); let mut handler = coapcore::OscoreEdhocHandler::new( handler, + coapcore::seccfg::ConfigBuilder::new() + .allow_unauthenticated(unauthenticated_scope) + .with_own_edhoc_credential(own_credential, own_key) + .with_known_edhoc_credential(admin_key, admin_scope), || lakers_crypto_rustcrypto::Crypto::new(ariel_os_random::crypto_rng()), ariel_os_random::crypto_rng(), - ) - .allow_arbitrary(); + ); info!("Server is ready."); diff --git a/src/lib/coapcore/Cargo.toml b/src/lib/coapcore/Cargo.toml index 2e9a9b1a0..d23fe6ecf 100644 --- a/src/lib/coapcore/Cargo.toml +++ b/src/lib/coapcore/Cargo.toml @@ -25,7 +25,6 @@ arrayvec = { version = "0.7.4", default-features = false } coap-message-implementations = { version = "0.1.2", features = ["downcast"] } coap-message-utils = "0.3.3" coap-numbers = "0.2.3" -hexlit = "0.5.5" lakers-crypto-rustcrypto = "0.7.2" liboscore = "0.2.2" liboscore-msgbackend = "0.2.2" @@ -44,8 +43,6 @@ document-features = "0.2.10" # dependencies. ccm = { version = "0.5.0", default-features = false } aes = { version = "0.8.4", default-features = false } -cbor-macro = "0.1.0" -cboritem = "0.1.2" [features] #! Cargo features diff --git a/src/lib/coapcore/src/scope.rs b/src/lib/coapcore/src/scope.rs index 8ff5986a5..15483d9aa 100644 --- a/src/lib/coapcore/src/scope.rs +++ b/src/lib/coapcore/src/scope.rs @@ -85,7 +85,7 @@ const AIF_SCOPE_MAX_LEN: usize = 64; /// /// This completely disregards proper URI splitting; this works for very simple URI references in /// the AIF. This could be mitigated by switching to a CRI based model. -#[derive(Debug, defmt::Format)] +#[derive(Debug, defmt::Format, Clone)] pub struct AifValue([u8; AIF_SCOPE_MAX_LEN]); impl TryFrom<&[u8]> for AifValue { @@ -209,7 +209,7 @@ impl> ScopeGenerator for ParsingAif { /// This is useful when combining multiple authentication methods, eg. allowing ACE tokens (that /// need an [`AifValue`] to express their arbitrary scopes) as well as a configured admin key (that /// has "all" permission, which are not expressible in an [`AifValue`]. -#[derive(Debug, defmt::Format)] +#[derive(Debug, defmt::Format, Clone)] pub enum UnionScope { AifValue(AifValue), AllowAll, diff --git a/src/lib/coapcore/src/seccfg.rs b/src/lib/coapcore/src/seccfg.rs index 35c4d078f..03468ec74 100644 --- a/src/lib/coapcore/src/seccfg.rs +++ b/src/lib/coapcore/src/seccfg.rs @@ -92,107 +92,6 @@ pub trait ServerSecurityConfig: crate::Sealed { } } -/// Type list of authorization servers. Any operation is first tried on the first item, then on the -/// second. -/// -/// It's convention to have a single A1 and then another chain in A2 or an [`DenyAll`], but that's -/// mainly becuse that version is easiy to construct -/// -/// In case of doubt, the head is used; in particular, it is only the head that gets to render the -/// unauthorized response. -pub struct AsChain { - a1: A1, - a2: A2, - _phantom: core::marker::PhantomData, -} - -impl AsChain { - /// Creates a configuration that processes all operations through the `head`, and only if that - /// fails retries with the `tail`. - pub fn chain(head: A1, tail: A2) -> Self { - AsChain { - a1: head, - a2: tail, - _phantom: Default::default(), - } - } -} - -/// An `Either` style type for encapsulating two [`ScopeGenerator`] implementations. -/// -/// Other crates should not rely on this (but making it an enum wrapped in a struct for privacy is -/// considered excessive at this point). -#[doc(hidden)] -pub enum EitherScopeGenerator { - First(SG1), - Second(SG2), - Phantom(core::convert::Infallible, core::marker::PhantomData), -} - -impl crate::scope::ScopeGenerator for EitherScopeGenerator -where - Scope: crate::scope::Scope, - SG1: crate::scope::ScopeGenerator, - SG2: crate::scope::ScopeGenerator, - SG1::Scope: Into, - SG2::Scope: Into, -{ - type Scope = Scope; - - fn from_token_scope(self, bytes: &[u8]) -> Result { - Ok(match self { - EitherScopeGenerator::First(gen) => gen.from_token_scope(bytes)?.into(), - EitherScopeGenerator::Second(gen) => gen.from_token_scope(bytes)?.into(), - EitherScopeGenerator::Phantom(infallible, _) => match infallible {}, - }) - } -} - -impl crate::Sealed for AsChain {} - -impl ServerSecurityConfig for AsChain -where - A1: ServerSecurityConfig, - A2: ServerSecurityConfig, - Scope: crate::scope::Scope, - A1::Scope: Into, - A2::Scope: Into, -{ - const PARSES_TOKENS: bool = A1::PARSES_TOKENS || A2::PARSES_TOKENS; - - type Scope = Scope; - type ScopeGenerator = EitherScopeGenerator; - - fn decrypt_symmetric_token( - &self, - headers: &HeaderMap, - aad: &[u8], - ciphertext_buffer: &mut heapless::Vec, - _priv: crate::PrivateMethod, - ) -> Result { - if let Ok(sg) = self - .a1 - .decrypt_symmetric_token(headers, aad, ciphertext_buffer, _priv) - { - return Ok(EitherScopeGenerator::First(sg)); - } - match self - .a2 - .decrypt_symmetric_token(headers, aad, ciphertext_buffer, _priv) - { - Ok(sg) => Ok(EitherScopeGenerator::Second(sg)), - Err(e) => Err(e), - } - } - - fn render_not_allowed( - &self, - message: &mut M, - ) -> Result<(), ()> { - self.a1.render_not_allowed(message) - } -} - /// The default empty configuration that denies all access. pub struct DenyAll; @@ -239,110 +138,26 @@ impl ServerSecurityConfig for AllowAll { } } -/// A scope that recognize the `tests/coap` demo's credentials. +/// An implementation of [`ServerSecurityConfig`] that can be extended using builder methods. /// -/// FIXME: This should be moved into the demo or abstracted. -pub struct GenerateArbitrary; - -impl GenerateArbitrary { - fn the_one_known_authorization(&self) -> Option { - use cbor_macro::cbor; - let slice: &[u8] = &cbor!([ - ["/stdout", 17 / GET and FETCH /], - ["/.well-known/core", 1], - ["/poem", 1] - ]); - crate::scope::AifValue::try_from(slice).ok() - } +/// This is very much in flux, and will need further exploration as to inhowmuch this can be +/// type-composed from components. +pub struct ConfigBuilder { + as_key_31: Option<[u8; 32]>, + unauthenticated_scope: Option, + own_edhoc_credential: Option<(lakers::Credential, lakers::BytesP256ElemLen)>, + known_edhoc_clients: Option<(lakers::Credential, crate::scope::UnionScope)>, + request_creation_hints: &'static [u8], } -impl crate::Sealed for GenerateArbitrary {} - -impl ServerSecurityConfig for GenerateArbitrary { - const PARSES_TOKENS: bool = false; - - type Scope = crate::scope::AifValue; - type ScopeGenerator = NullGenerator; - - fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> { - use hexlit::hex; - const R: [u8; 32] = - hex!("72cc4761dbd4c78f758931aa589d348d1ef874a7e303ede2f140dcf3e6aa4aac"); - - Some(( - lakers::Credential::parse_ccs(&hex!("A2026008A101A5010202410A2001215820BBC34960526EA4D32E940CAD2A234148DDC21791A12AFBCBAC93622046DD44F02258204519E257236B2A0CE2023F0931F1F386CA7AFDA64FCDE0108C224C51EABF6072")).expect("Credential should be processable"), - R, - )) - } - - fn expand_id_cred_x( - &self, - id_cred_x: lakers::IdCred, - ) -> Option<(lakers::Credential, Self::Scope)> { - use defmt_or_log::info; - - if id_cred_x.reference_only() { - match id_cred_x.as_encoded_value() { - &[43] => { - info!("Peer indicates use of the one preconfigured key"); - - use hexlit::hex; - const CRED_I: &[u8] = &hex!("A2027734322D35302D33312D46462D45462D33372D33322D333908A101A5010202412B2001215820AC75E9ECE3E50BFC8ED60399889522405C47BF16DF96660A41298CB4307F7EB62258206E5DE611388A4B8A8211334AC7D37ECB52A387D257E6DB3C2A93DF21FF3AFFC8"); - - Some(( - lakers::Credential::parse_ccs(CRED_I) - .expect("Static credential is not processable"), - self.the_one_known_authorization()?, - )) - } - _ => None, - } - } else { - let ccs = id_cred_x - .get_ccs() - .expect("Lakers only knows IdCred as reference or as credential"); - info!( - "Got credential CCS by value: {:?}..", - &ccs.bytes.get_slice(0, 5) - ); - Some(( - lakers::Credential::parse_ccs(ccs.bytes.as_slice()).ok()?, - self.nosec_authorization()?, - )) - } - } - - fn nosec_authorization(&self) -> Option { - use cbor_macro::cbor; - let slice: &[u8] = &cbor!([["/.well-known/core", 1], ["/poem", 1]]); - crate::scope::AifValue::try_from(slice).ok() - } -} - -/// A test SSC association that does not need to deal with key IDs and just tries a single static -/// key with a single algorithm, and parses the scope in there as AIF. -/// -/// It sends a static response (empty slice is a fine default) on unauthorized responses. -pub struct StaticSymmetric31 { - key: &'static [u8; 32], - unauthorized_response: &'static [u8], -} - -impl StaticSymmetric31 { - pub fn new(key: &'static [u8; 32], unauthorized_response: &'static [u8]) -> Self { - Self { - key, - unauthorized_response, - } - } -} -impl crate::Sealed for StaticSymmetric31 {} +impl crate::Sealed for ConfigBuilder {} -impl ServerSecurityConfig for StaticSymmetric31 { +impl ServerSecurityConfig for ConfigBuilder { + // We can't know at build time, assume yes const PARSES_TOKENS: bool = true; - type Scope = crate::scope::AifValue; - type ScopeGenerator = crate::scope::ParsingAif; + type Scope = crate::scope::UnionScope; + type ScopeGenerator = crate::scope::ParsingAif; fn decrypt_symmetric_token( &self, @@ -354,12 +169,17 @@ impl ServerSecurityConfig for StaticSymmetric31 { use ccm::aead::AeadInPlace; use ccm::KeyInit; + let key = self.as_key_31.ok_or_else(|| { + defmt_or_log::error!("ConfigBuilder is not configured with a symmetric key."); + DecryptionError::NoKeyFound + })?; + // FIXME: should be something Aes256Ccm::TagLength const TAG_SIZE: usize = 16; const NONCE_SIZE: usize = 13; pub type Aes256Ccm = ccm::Ccm; - let cipher = Aes256Ccm::new(self.key.into()); + let cipher = Aes256Ccm::new((&key).into()); let nonce: &[u8; NONCE_SIZE] = headers .iv @@ -395,6 +215,50 @@ impl ServerSecurityConfig for StaticSymmetric31 { Ok(crate::scope::ParsingAif::new()) } + fn nosec_authorization(&self) -> Option { + self.unauthenticated_scope.clone() + } + + fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> { + self.own_edhoc_credential + } + + fn expand_id_cred_x( + &self, + id_cred_x: lakers::IdCred, + ) -> Option<(lakers::Credential, Self::Scope)> { + use defmt_or_log::{debug, info}; + + debug!("Evaluating peer's credenital {}", id_cred_x.as_full_value()); + + for (credential, scope) in &[self.known_edhoc_clients.as_ref()?] { + debug!("Comparing to {}", credential.bytes.as_slice()); + if id_cred_x.reference_only() { + // ad Ok: If our credential has no KID, it can't be recognized in this branch + if credential.by_kid() == Ok(id_cred_x) { + info!("Peer indicates use of the one preconfigured key"); + return Some((credential.clone(), scope.clone())); + } + } else { + // ad Ok: This is always the case for CCSs, but inapplicable eg. for PSKs. + if credential.by_value() == Ok(id_cred_x) { + return Some((credential.clone(), scope.clone())); + } + } + } + + debug!("Fell through"); + if let Some(small_scope) = self.nosec_authorization() { + debug!("There is an unauthenticated scope"); + if let Some(credential_by_value) = id_cred_x.get_ccs() { + debug!("and get_ccs worked"); + return Some((credential_by_value.clone(), small_scope.clone())); + } + } + + None + } + fn render_not_allowed( &self, message: &mut M, @@ -402,8 +266,123 @@ impl ServerSecurityConfig for StaticSymmetric31 { use coap_message::Code; message.set_code(M::Code::new(coap_numbers::code::UNAUTHORIZED).map_err(|_| ())?); message - .set_payload(self.unauthorized_response) + .set_payload(self.request_creation_hints) .map_err(|_| ())?; Ok(()) } } + +impl ConfigBuilder { + /// Creates an empty server security configuration. + /// + /// Without any additional building steps, this is equivalent to [`DenyAll`]. + pub fn new() -> Self { + Self { + as_key_31: None, + unauthenticated_scope: None, + known_edhoc_clients: None, + own_edhoc_credential: None, + request_creation_hints: &[], + } + } + + /// Sets a single Authorization Server recognized by a shared `AES-16-128-256` (COSE algorithm + /// 31) key. + /// + /// Scopes are accepted as given by the AS using the AIF REST model as understood by + /// [`crate::scope::AifValue`]. + /// + /// # Caveats and evolution + /// + /// Currently, this type just supports a single AS; it should therefore only be called once, + /// and the latest value overwrites any earlier. Building these in type state (as `[(&as_key); + /// { N+1 }]` (once that is possible) or `(&as_key1, (&as_key2, ()))` will make sense on the + /// long run, but is not implemented yet. + /// + /// Depending on whether the keys are already referenced in a long-lived location, when + /// implementing that, it can also make sense to allow using any `AsRef<[u8; 32]>` types at + /// that point. + /// + /// Currently, keys are taken as byte sequence. With the expected flexibilization of crypto + /// backends, this may later allow a more generic type that reflects secure element key slots. + pub fn with_aif_symmetric_as_aesccm256(self, key: [u8; 32]) -> Self { + Self { + as_key_31: Some(key), + ..self + } + } + + /// Allow use of the server within the limits of the given scope by EDHOC clients provided they + /// present the given credential. + /// + /// # Caveats and evolution + /// + /// Currently, this type just supports a single credential; it should therefore only be called + /// once, and the latest value overwrites any earlier. (See + /// [`Self::with_aif_symmetric_as_aesccm256`] for plans). + pub fn with_known_edhoc_credential( + self, + credential: lakers::Credential, + scope: crate::scope::UnionScope, + ) -> Self { + Self { + known_edhoc_clients: Some((credential, scope)), + ..self + } + } + + /// Configures an EDHOC credential and private key to be presented by this server. + /// + /// # Panics + /// + /// When debug assertions are enabled, this panics if an own credential has already been + /// configured. + pub fn with_own_edhoc_credential( + self, + credential: lakers::Credential, + key: lakers::BytesP256ElemLen, + ) -> Self { + debug_assert!( + self.own_edhoc_credential.is_none(), + "Overwriting previously configured own credential scope" + ); + Self { + own_edhoc_credential: Some((credential, key)), + ..self + } + } + + /// Allow use of the server by unauthenticated clients using the given scope. + /// + /// # Panics + /// + /// When debug assertions are enabled, this panics if an unauthenticated scope has already been + /// configured. + pub fn allow_unauthenticated(self, scope: crate::scope::UnionScope) -> Self { + debug_assert!( + self.unauthenticated_scope.is_none(), + "Overwriting previously configured unauthenticated scope" + ); + Self { + unauthenticated_scope: Some(scope), + ..self + } + } + + /// Sets the payload of the "Unauthorized" response. + /// + /// # Panics + /// + /// When debug assertions are enabled, this panics if an unauthenticated scope has already been + /// configured. + pub fn with_request_creation_hints(self, request_creation_hints: &'static [u8]) -> Self { + debug_assert!( + self.request_creation_hints == [], + "Overwriting previously configured unauthenticated scope" + ); + Self { + request_creation_hints, + ..self + } + } +} diff --git a/src/lib/coapcore/src/seccontext.rs b/src/lib/coapcore/src/seccontext.rs index ddd165b0f..80b1f56b4 100644 --- a/src/lib/coapcore/src/seccontext.rs +++ b/src/lib/coapcore/src/seccontext.rs @@ -155,101 +155,24 @@ impl< H: coap_handler::Handler, Crypto: lakers::Crypto, CryptoFactory: Fn() -> Crypto, + SSC: ServerSecurityConfig, RNG: rand_core::RngCore + rand_core::CryptoRng, - > OscoreEdhocHandler + > OscoreEdhocHandler { /// Creates a new CoAP server implementation (a [Handler][coap_handler::Handler]). /// /// By default, this rejects all requests; access is allowed through builder calls such as /// [`.with_seccfg()()`][Self::with_seccfg()] or /// [`.allow_all()`][Self::allow_all()]. - pub fn new( - inner: H, - crypto_factory: CryptoFactory, - rng: RNG, - ) -> OscoreEdhocHandler { + pub fn new(inner: H, authorities: SSC, crypto_factory: CryptoFactory, rng: RNG) -> Self { Self { pool: Default::default(), inner, crypto_factory, - authorities: crate::seccfg::DenyAll, + authorities, rng, } } -} - -impl< - H: coap_handler::Handler, - Crypto: lakers::Crypto, - CryptoFactory: Fn() -> Crypto, - RNG: rand_core::RngCore + rand_core::CryptoRng, - > OscoreEdhocHandler -{ - /// Alters the server's policy so that it accepts any request without any authentication. - pub fn allow_all( - self, - ) -> OscoreEdhocHandler { - OscoreEdhocHandler { - // Starting from DenyAll allows us to diregard any old connections as they couldn't do - // anything - pool: Default::default(), - authorities: crate::seccfg::AllowAll, - inner: self.inner, - crypto_factory: self.crypto_factory, - rng: self.rng, - } - } - - /// Alters a server's policy so that behaves like coapcore has behaved in its sketch phase - pub fn allow_arbitrary( - self, - ) -> OscoreEdhocHandler { - OscoreEdhocHandler { - // Starting from DenyAll allows us to diregard any old connections as they couldn't do - // anything - pool: Default::default(), - authorities: crate::seccfg::GenerateArbitrary, - inner: self.inner, - crypto_factory: self.crypto_factory, - rng: self.rng, - } - } -} - -impl< - H: coap_handler::Handler, - Crypto: lakers::Crypto, - CryptoFactory: Fn() -> Crypto, - SSC: ServerSecurityConfig, - RNG: rand_core::RngCore + rand_core::CryptoRng, - > OscoreEdhocHandler -{ - /// Adds a new authorization server (or set thereof) to the handler, which is queried before - /// any other authorization server. - // Ideally we wouldn't statically produce UnionScope but anything smaller depending on AS1/AS2 - pub fn with_seccfg( - self, - prepended_as: AS1, - ) -> OscoreEdhocHandler< - H, - Crypto, - CryptoFactory, - crate::seccfg::AsChain, - RNG, - > - where - crate::scope::UnionScope: - From<::Scope> + From<::Scope>, - { - OscoreEdhocHandler { - authorities: crate::seccfg::AsChain::chain(prepended_as, self.authorities), - // FIXME: This discards old connections rather than .into()'ing all their scopes. - pool: Default::default(), - inner: self.inner, - crypto_factory: self.crypto_factory, - rng: self.rng, - } - } /// Produces a COwn (as a recipient identifier) that is both available and not equal to the /// peer's recipient identifier.