use defmt_or_log::{debug, error, trace};
use crate::ace::HeaderMap;
use crate::error::{CredentialError, CredentialErrorDetail};
use crate::generalclaims::{GeneralClaims, Unlimited};
use crate::time::TimeConstraint;
pub(crate) const MAX_AUD_SIZE: usize = 8;
pub struct NotAllowedRenderingFailed;
pub trait ServerSecurityConfig {
const PARSES_TOKENS: bool;
const HAS_EDHOC: bool;
type GeneralClaims: GeneralClaims;
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn decrypt_symmetric_token<'buf>(
&self,
headers: &HeaderMap<'_>,
aad: &[u8],
ciphertext_buffer: &'buf mut [u8],
) -> Result<(Self::GeneralClaims, crate::ace::CwtClaimsSet<'buf>), CredentialError> {
Err(CredentialErrorDetail::KeyNotPresent.into())
}
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn verify_asymmetric_token<'b>(
&self,
headers: &HeaderMap<'_>,
signed_data: &[u8],
signature: &[u8],
signed_payload: &'b [u8],
) -> Result<(Self::GeneralClaims, crate::ace::CwtClaimsSet<'b>), CredentialError> {
Err(CredentialErrorDetail::KeyNotPresent.into())
}
fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
None
}
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn expand_id_cred_x(
&self,
id_cred_x: lakers::IdCred,
) -> Option<(lakers::Credential, Self::GeneralClaims)> {
None
}
fn nosec_authorization(&self) -> Option<Self::GeneralClaims> {
None
}
#[allow(
unused_variables,
reason = "Names are human visible part of API description"
)]
fn render_not_allowed<M: coap_message::MutableWritableMessage>(
&self,
message: &mut M,
) -> Result<(), NotAllowedRenderingFailed> {
Err(NotAllowedRenderingFailed)
}
}
pub struct DenyAll;
impl ServerSecurityConfig for DenyAll {
const PARSES_TOKENS: bool = false;
const HAS_EDHOC: bool = false;
type GeneralClaims = core::convert::Infallible;
}
pub struct AllowAll;
impl ServerSecurityConfig for AllowAll {
const PARSES_TOKENS: bool = false;
const HAS_EDHOC: bool = false;
type GeneralClaims = Unlimited<crate::scope::AllowAll>;
fn nosec_authorization(&self) -> Option<Self::GeneralClaims> {
Some(Unlimited(crate::scope::AllowAll))
}
}
pub struct ConfigBuilder {
as_key_31: Option<[u8; 32]>,
as_key_neg7: Option<([u8; 32], [u8; 32], heapless::String<MAX_AUD_SIZE>)>,
unauthenticated_scope: Option<crate::scope::UnionScope>,
own_edhoc_credential: Option<(lakers::Credential, lakers::BytesP256ElemLen)>,
known_edhoc_clients: Option<(lakers::Credential, crate::scope::UnionScope)>,
request_creation_hints: &'static [u8],
}
impl ServerSecurityConfig for ConfigBuilder {
const PARSES_TOKENS: bool = true;
const HAS_EDHOC: bool = true;
type GeneralClaims = ConfigBuilderClaims;
fn decrypt_symmetric_token<'buf>(
&self,
headers: &HeaderMap<'_>,
aad: &[u8],
ciphertext_buffer: &'buf mut [u8],
) -> Result<(Self::GeneralClaims, crate::ace::CwtClaimsSet<'buf>), CredentialError> {
use ccm::KeyInit as _;
use ccm::aead::AeadInPlace as _;
pub type Aes256Ccm = ccm::Ccm<aes::Aes256, ccm::consts::U16, ccm::consts::U13>;
const TAG_SIZE: usize = 16;
const NONCE_SIZE: usize = 13;
let key = self.as_key_31.ok_or_else(|| {
error!("Symmetrically encrypted token was sent, but no symmetric key is configured.");
CredentialErrorDetail::KeyNotPresent
})?;
let cipher = Aes256Ccm::new((&key).into());
let nonce: &[u8; NONCE_SIZE] = headers
.iv
.ok_or_else(|| {
error!("IV missing from token.");
CredentialErrorDetail::InconsistentDetails
})?
.try_into()
.map_err(|_| {
error!("Token's IV length mismatches algorithm.");
CredentialErrorDetail::InconsistentDetails
})?;
let ciphertext_len = ciphertext_buffer
.len()
.checked_sub(TAG_SIZE)
.ok_or_else(|| {
error!("Token's ciphertext too short for the algorithm's tag.");
CredentialErrorDetail::InconsistentDetails
})?;
let (ciphertext, tag) = ciphertext_buffer.split_at_mut(ciphertext_len);
cipher
.decrypt_in_place_detached(nonce.into(), aad, ciphertext, ccm::Tag::from_slice(tag))
.map_err(|_| {
error!("Token decryption failed.");
CredentialErrorDetail::VerifyFailed
})?;
let claims: crate::ace::CwtClaimsSet<'_> = minicbor::decode(ciphertext)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;
let scope = crate::scope::AifValue::parse(claims.scope)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?
.into();
let time_constraint = crate::time::TimeConstraint::from_claims_set(&claims);
Ok((
ConfigBuilderClaims {
scope,
time_constraint,
is_important: false,
},
claims,
))
}
fn verify_asymmetric_token<'b>(
&self,
headers: &HeaderMap<'_>,
signed_data: &[u8],
signature: &[u8],
signed_payload: &'b [u8],
) -> Result<(Self::GeneralClaims, crate::ace::CwtClaimsSet<'b>), CredentialError> {
use p256::ecdsa::{VerifyingKey, signature::Verifier as _};
if headers.alg != Some(-7) {
return Err(CredentialErrorDetail::UnsupportedAlgorithm.into());
}
let Some((x, y, rs_audience)) = self.as_key_neg7.as_ref() else {
return Err(CredentialErrorDetail::KeyNotPresent.into());
};
let as_key = VerifyingKey::from_encoded_point(
&p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false),
)
.map_err(|_| CredentialErrorDetail::InconsistentDetails)?;
let signature = p256::ecdsa::Signature::from_slice(signature)
.map_err(|_| CredentialErrorDetail::InconsistentDetails)?;
as_key
.verify(signed_data, &signature)
.map_err(|_| CredentialErrorDetail::VerifyFailed)?;
let claims: crate::ace::CwtClaimsSet<'_> = minicbor::decode(signed_payload)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;
if claims.aud != Some(rs_audience) {
return Err(CredentialErrorDetail::VerifyFailed.into());
}
let scope = crate::scope::AifValue::parse(claims.scope)
.map_err(|_| CredentialErrorDetail::UnsupportedExtension)?
.into();
let time_constraint = crate::time::TimeConstraint::from_claims_set(&claims);
Ok((
ConfigBuilderClaims {
scope,
time_constraint,
is_important: false,
},
claims,
))
}
fn nosec_authorization(&self) -> Option<Self::GeneralClaims> {
self.unauthenticated_scope
.clone()
.map(|scope| ConfigBuilderClaims {
scope,
time_constraint: TimeConstraint::unbounded(),
is_important: false,
})
}
fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
#[expect(
clippy::clone_on_copy,
reason = "the type should not be clone, and will not be in future lakers versions"
)]
self.own_edhoc_credential.clone()
}
fn expand_id_cred_x(
&self,
id_cred_x: lakers::IdCred,
) -> Option<(lakers::Credential, Self::GeneralClaims)> {
trace!(
"Evaluating peer's credential {}",
defmt_or_log::wrappers::Cbor(id_cred_x.as_full_value())
);
#[expect(
clippy::single_element_loop,
reason = "Expected to be extended to actual loop soon"
)]
for (credential, scope) in &[self.known_edhoc_clients.as_ref()?] {
trace!(
"Comparing to {}",
defmt_or_log::wrappers::Cbor(credential.bytes.as_slice())
);
if id_cred_x.reference_only() {
if credential.by_kid().as_ref() == Ok(&id_cred_x) {
debug!("Peer indicated use of the one preconfigured key by KID.");
#[expect(
clippy::clone_on_copy,
reason = "Lakers items are overly copy happy"
)]
return Some((
credential.clone(),
ConfigBuilderClaims {
scope: scope.clone(),
time_constraint: TimeConstraint::unbounded(),
is_important: true,
},
));
}
} else {
if credential.by_value().as_ref() == Ok(&id_cred_x) {
debug!("Peer indicated use of the one preconfigured credential by value.");
#[expect(
clippy::clone_on_copy,
reason = "Lakers items are overly copy happy"
)]
return Some((
credential.clone(),
ConfigBuilderClaims {
scope: scope.clone(),
time_constraint: TimeConstraint::unbounded(),
is_important: true,
},
));
}
}
}
if let Some(unauthorized_claims) = self.nosec_authorization() {
trace!("Unauthenticated clients are generally accepted, evaluating credential.");
if let Some(credential_by_value) = id_cred_x.get_ccs().as_ref() {
debug!("The unauthorized client provided a usable credential by value.");
#[expect(clippy::clone_on_copy, reason = "Lakers items are overly copy happy")]
return Some((credential_by_value.clone(), unauthorized_claims));
}
}
None
}
fn render_not_allowed<M: coap_message::MutableWritableMessage>(
&self,
message: &mut M,
) -> Result<(), NotAllowedRenderingFailed> {
use coap_message::Code as _;
message.set_code(M::Code::new(coap_numbers::code::UNAUTHORIZED).map_err(|_| {
error!("CoAP stack can not represent Unauthorized responses.");
NotAllowedRenderingFailed
})?);
message
.set_payload(self.request_creation_hints)
.map_err(|_| {
error!("Request creation hints do not fit in error message.");
NotAllowedRenderingFailed
})?;
Ok(())
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
ConfigBuilder::new()
}
}
impl ConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self {
as_key_31: None,
as_key_neg7: None,
unauthenticated_scope: None,
known_edhoc_clients: None,
own_edhoc_credential: None,
request_creation_hints: &[],
}
}
#[must_use]
pub fn with_aif_symmetric_as_aesccm256(self, key: [u8; 32]) -> Self {
Self {
as_key_31: Some(key),
..self
}
}
#[must_use]
pub fn with_aif_asymmetric_es256(
self,
x: [u8; 32],
y: [u8; 32],
audience: heapless::String<MAX_AUD_SIZE>,
) -> Self {
Self {
as_key_neg7: Some((x, y, audience)),
..self
}
}
#[must_use]
pub fn with_known_edhoc_credential(
self,
credential: lakers::Credential,
scope: crate::scope::UnionScope,
) -> Self {
Self {
known_edhoc_clients: Some((credential, scope)),
..self
}
}
#[must_use]
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
}
}
#[must_use]
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
}
}
#[must_use]
pub fn with_request_creation_hints(self, request_creation_hints: &'static [u8]) -> Self {
debug_assert!(
self.request_creation_hints.is_empty(),
"Overwriting previously configured unauthenticated scope"
);
Self {
request_creation_hints,
..self
}
}
}
#[derive(Debug)]
pub struct ConfigBuilderClaims {
pub scope: crate::scope::UnionScope,
pub time_constraint: crate::time::TimeConstraint,
pub is_important: bool,
}
impl GeneralClaims for ConfigBuilderClaims {
type Scope = crate::scope::UnionScope;
fn scope(&self) -> &Self::Scope {
&self.scope
}
fn time_constraint(&self) -> crate::time::TimeConstraint {
self.time_constraint
}
fn is_important(&self) -> bool {
self.is_important
}
}