use coap_message::Code as _;
use defmt_or_log::trace;
use crate::error::{CredentialError, CredentialErrorDetail};
use crate::helpers::COwn;
pub(crate) const OWN_NONCE_LEN: usize = 8;
const MAX_SUPPORTED_PEER_NONCE_LEN: usize = 16;
const MAX_SUPPORTED_ACCESSTOKEN_LEN: usize = 256;
const MAX_SUPPORTED_ENCRYPT_PROTECTED_LEN: usize = 32;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(minicbor::Decode, minicbor::Encode, Default, Debug)]
#[cbor(map)]
#[non_exhaustive]
struct AceCbor<'a> {
#[cbor(b(1), with = "minicbor::bytes")]
access_token: Option<&'a [u8]>,
#[cbor(b(40), with = "minicbor::bytes")]
nonce1: Option<&'a [u8]>,
#[cbor(b(42), with = "minicbor::bytes")]
nonce2: Option<&'a [u8]>,
#[cbor(b(43), with = "minicbor::bytes")]
ace_client_recipientid: Option<&'a [u8]>,
#[cbor(b(44), with = "minicbor::bytes")]
ace_server_recipientid: Option<&'a [u8]>,
}
type UnprotectedAuthzInfoPost<'a> = AceCbor<'a>;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(minicbor::Decode, Debug)]
#[allow(
missing_docs,
reason = "Fields correspond 1:1 to the domain items of the same name"
)]
#[cbor(map)]
#[non_exhaustive]
pub struct HeaderMap<'a> {
#[n(1)]
pub alg: Option<i32>,
#[cbor(b(5), with = "minicbor::bytes")]
pub(crate) iv: Option<&'a [u8]>,
}
impl HeaderMap<'_> {
fn updated_with(&self, other: &Self) -> Self {
Self {
alg: self.alg.or(other.alg),
iv: self.iv.or(other.iv),
}
}
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(minicbor::Decode, Debug)]
#[allow(
dead_code,
reason = "Presence of the item makes CBOR derive tolerate the item"
)]
#[cbor(map)]
#[non_exhaustive]
pub(crate) struct CoseKey<'a> {
#[n(1)]
pub(crate) kty: i32, #[cbor(b(2), with = "minicbor::bytes")]
pub(crate) kid: Option<&'a [u8]>,
#[n(3)]
pub(crate) alg: Option<i32>,
#[n(-1)]
pub(crate) crv: Option<i32>, #[cbor(b(-2), with = "minicbor::bytes")]
pub(crate) x: Option<&'a [u8]>,
#[cbor(b(-3), with = "minicbor::bytes")]
pub(crate) y: Option<&'a [u8]>, }
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(minicbor::Decode, Debug)]
#[cbor(tag(16))]
#[non_exhaustive]
struct CoseEncrypt0<'a> {
#[cbor(b(0), with = "minicbor::bytes")]
protected: &'a [u8],
#[b(1)]
unprotected: HeaderMap<'a>,
#[cbor(b(2), with = "minicbor::bytes")]
encrypted: &'a [u8],
}
#[derive(minicbor::Encode)]
struct Encrypt0<'a> {
#[n(0)]
context: &'static str,
#[cbor(b(1), with = "minicbor::bytes")]
protected: &'a [u8],
#[cbor(b(2), with = "minicbor::bytes")]
external_aad: &'a [u8],
}
const AADSIZE: usize = 1 + 1 + 8 + 1 + MAX_SUPPORTED_ENCRYPT_PROTECTED_LEN + 1;
impl CoseEncrypt0<'_> {
fn prepare_decryption<'t>(
&self,
buffer: &'t mut heapless::Vec<u8, MAX_SUPPORTED_ACCESSTOKEN_LEN>,
) -> Result<(HeaderMap<'_>, impl AsRef<[u8]>, &'t mut [u8]), CredentialError> {
trace!("Preparing decryption of {:?}", self);
let protected: HeaderMap<'_> = minicbor::decode(self.protected)?;
trace!("Protected decoded as header map: {:?}", protected);
let headers = self.unprotected.updated_with(&protected);
let aad = Encrypt0 {
context: "Encrypt0",
protected: self.protected,
external_aad: &[],
};
let mut aad_encoded = heapless::Vec::<u8, AADSIZE>::new();
minicbor::encode(&aad, minicbor_adapters::WriteToHeapless(&mut aad_encoded))
.map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
trace!(
"Serialized AAD: {}",
defmt_or_log::wrappers::Cbor(&aad_encoded)
);
buffer.clear();
buffer
.extend_from_slice(self.encrypted)
.map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
Ok((headers, aad_encoded, buffer))
}
}
type EncryptedCwt<'a> = CoseEncrypt0<'a>;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(minicbor::Decode, Debug)]
#[cbor(tag(18))]
#[non_exhaustive]
struct CoseSign1<'a> {
#[cbor(b(0), with = "minicbor::bytes")]
protected: &'a [u8],
#[b(1)]
unprotected: HeaderMap<'a>,
#[cbor(b(2), with = "minicbor::bytes")]
payload: &'a [u8],
#[cbor(b(3), with = "minicbor::bytes")]
signature: &'a [u8],
}
type SignedCwt<'a> = CoseSign1<'a>;
#[derive(minicbor::Encode)]
struct SigStructureForSignature1<'a> {
#[n(0)]
context: &'static str,
#[cbor(b(1), with = "minicbor::bytes")]
body_protected: &'a [u8],
#[cbor(b(2), with = "minicbor::bytes")]
external_aad: &'a [u8],
#[cbor(b(3), with = "minicbor::bytes")]
payload: &'a [u8],
}
#[derive(minicbor::Decode, Debug)]
#[allow(
dead_code,
reason = "Presence of the item makes CBOR derive tolerate the item"
)]
#[allow(
missing_docs,
reason = "Fields correspond 1:1 to the domain items of the same name"
)]
#[cbor(map)]
#[non_exhaustive]
pub struct CwtClaimsSet<'a> {
#[n(3)]
pub aud: Option<&'a str>,
#[n(4)]
pub(crate) exp: u64,
#[n(6)]
pub(crate) iat: u64,
#[b(8)]
cnf: Cnf<'a>,
#[cbor(b(9), with = "minicbor::bytes")]
pub scope: &'a [u8],
}
#[derive(minicbor::Decode, Debug)]
#[cbor(map)]
#[non_exhaustive]
struct Cnf<'a> {
#[b(4)]
osc: Option<OscoreInputMaterial<'a>>,
#[b(1)]
cose_key: Option<minicbor_adapters::WithOpaque<'a, CoseKey<'a>>>,
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(minicbor::Decode, Debug)]
#[allow(
dead_code,
reason = "Presence of the item makes CBOR derive tolerate the item"
)]
#[cbor(map)]
#[non_exhaustive]
struct OscoreInputMaterial<'a> {
#[cbor(b(0), with = "minicbor::bytes")]
id: &'a [u8],
#[cbor(b(2), with = "minicbor::bytes")]
ms: &'a [u8],
}
impl OscoreInputMaterial<'_> {
fn derive(
&self,
nonce1: &[u8],
nonce2: &[u8],
sender_id: &[u8],
recipient_id: &[u8],
) -> Result<liboscore::PrimitiveContext, CredentialError> {
let hkdf = liboscore::HkdfAlg::from_number(5)
.map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
let aead = liboscore::AeadAlg::from_number(10)
.map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
const { assert!(OWN_NONCE_LEN < 256) };
const { assert!(MAX_SUPPORTED_PEER_NONCE_LEN < 256) };
let mut combined_salt =
heapless::Vec::<u8, { 1 + 2 + MAX_SUPPORTED_PEER_NONCE_LEN + 2 + OWN_NONCE_LEN }>::new(
);
let mut encoder =
minicbor::Encoder::new(minicbor_adapters::WriteToHeapless(&mut combined_salt));
encoder
.bytes(b"")
.and_then(|encoder| encoder.bytes(nonce1))
.and_then(|encoder| encoder.bytes(nonce2))?;
let immutables = liboscore::PrimitiveImmutables::derive(
hkdf,
self.ms,
&combined_salt,
None, aead,
sender_id,
recipient_id,
)
.map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
Ok(liboscore::PrimitiveContext::new_from_fresh_material(
immutables,
))
}
}
pub struct AceCborAuthzInfoResponse {
nonce2: [u8; OWN_NONCE_LEN],
ace_server_recipientid: COwn,
}
impl AceCborAuthzInfoResponse {
#[allow(
clippy::missing_panics_doc,
reason = "will never panic for any user input"
)]
pub(crate) fn render<M: coap_message::MutableWritableMessage>(
&self,
message: &mut M,
) -> Result<(), M::UnionError> {
let full = AceCbor {
nonce2: Some(&self.nonce2),
ace_server_recipientid: Some(self.ace_server_recipientid.as_slice()),
..Default::default()
};
message.set_code(M::Code::new(coap_numbers::code::CHANGED)?);
const { assert!(OWN_NONCE_LEN < 256) };
const { assert!(COwn::MAX_SLICE_LEN < 256) };
let required_len = 1 + 2 + 2 + OWN_NONCE_LEN + 2 + 2 + COwn::MAX_SLICE_LEN;
let payload = message.payload_mut_with_len(required_len)?;
let mut cursor = minicbor::encode::write::Cursor::new(payload);
minicbor::encode(full, &mut cursor).expect("Sufficient size was requested");
let written = cursor.position();
message.truncate(written)?;
Ok(())
}
}
pub(crate) fn process_acecbor_authz_info<GC: crate::GeneralClaims>(
payload: &[u8],
authorities: &impl crate::seccfg::ServerSecurityConfig<GeneralClaims = GC>,
nonce2: [u8; OWN_NONCE_LEN],
server_recipient_id: impl FnOnce(&[u8]) -> COwn,
) -> Result<(AceCborAuthzInfoResponse, liboscore::PrimitiveContext, GC), CredentialError> {
trace!(
"Processing authz_info {}",
defmt_or_log::wrappers::Cbor(payload)
);
let decoded: UnprotectedAuthzInfoPost<'_> = minicbor::decode(payload)?;
let AceCbor {
access_token: Some(access_token),
nonce1: Some(nonce1),
ace_client_recipientid: Some(ace_client_recipientid),
..
} = decoded
else {
return Err(CredentialErrorDetail::ProtocolViolation.into());
};
trace!(
"Decodeded authz_info as application/ace+cbor: {:?}",
decoded
);
let encrypt0: EncryptedCwt<'_> = minicbor::decode(access_token)?;
let mut buffer = heapless::Vec::new();
let (headers, aad_encoded, buffer) = encrypt0.prepare_decryption(&mut buffer)?;
if headers.alg != Some(31) {
return Err(CredentialErrorDetail::UnsupportedAlgorithm.into());
}
let (processed, parsed) =
authorities.decrypt_symmetric_token(&headers, aad_encoded.as_ref(), buffer)?;
let Cnf {
osc: Some(osc),
cose_key: None,
} = parsed.cnf
else {
return Err(CredentialErrorDetail::InconsistentDetails.into());
};
let ace_server_recipientid = server_recipient_id(ace_client_recipientid);
let derived = osc.derive(
nonce1,
&nonce2,
ace_client_recipientid,
ace_server_recipientid.as_slice(),
)?;
let response = AceCborAuthzInfoResponse {
nonce2,
ace_server_recipientid,
};
Ok((response, derived, processed))
}
#[expect(
clippy::missing_panics_doc,
reason = "panic only happens when fixed-length array gets placed into larger array"
)]
pub(crate) fn process_edhoc_token<GeneralClaims>(
ead3: &[u8],
authorities: &impl crate::seccfg::ServerSecurityConfig<GeneralClaims = GeneralClaims>,
) -> Result<(lakers::Credential, GeneralClaims), CredentialError> {
let mut buffer = heapless::Vec::<u8, MAX_SUPPORTED_ACCESSTOKEN_LEN>::new();
let (processed, parsed) = if let Ok(encrypt0) = minicbor::decode::<EncryptedCwt<'_>>(ead3) {
let (headers, aad_encoded, buffer) = encrypt0.prepare_decryption(&mut buffer)?;
authorities.decrypt_symmetric_token(&headers, aad_encoded.as_ref(), buffer)?
} else if let Ok(sign1) = minicbor::decode::<SignedCwt<'_>>(ead3) {
let protected: HeaderMap<'_> = minicbor::decode(sign1.protected)?;
trace!(
"Decoded protected header map {:?} inside sign1 container {:?}",
&protected, &sign1
);
let headers = sign1.unprotected.updated_with(&protected);
let aad = SigStructureForSignature1 {
context: "Signature1",
body_protected: sign1.protected,
external_aad: &[],
payload: sign1.payload,
};
buffer = heapless::Vec::new();
minicbor::encode(&aad, minicbor_adapters::WriteToHeapless(&mut buffer))?;
trace!("Serialized AAD: {}", defmt_or_log::wrappers::Hex(&buffer));
authorities.verify_asymmetric_token(&headers, &buffer, sign1.signature, sign1.payload)?
} else {
return Err(CredentialErrorDetail::UnsupportedExtension.into());
};
let Cnf {
osc: None,
cose_key: Some(cose_key),
} = parsed.cnf
else {
return Err(CredentialErrorDetail::InconsistentDetails.into());
};
let mut prefixed = lakers::BufferCred::new();
prefixed
.extend_from_slice(&[0xa1, 0x08, 0xa1, 0x01])
.unwrap();
prefixed
.extend_from_slice(&cose_key.opaque)
.map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
let credential = lakers::Credential::new_ccs(
prefixed,
cose_key
.parsed
.x
.ok_or(CredentialErrorDetail::InconsistentDetails)?
.try_into()
.map_err(|_| CredentialErrorDetail::InconsistentDetails)?,
);
Ok((credential, processed))
}