1use coap_message::Code as _;
9use defmt_or_log::trace;
10
11use crate::error::{CredentialError, CredentialErrorDetail};
12
13use crate::helpers::COwn;
14
15pub(crate) const OWN_NONCE_LEN: usize = 8;
17
18const MAX_SUPPORTED_PEER_NONCE_LEN: usize = 16;
20
21const MAX_SUPPORTED_ACCESSTOKEN_LEN: usize = 256;
23const MAX_SUPPORTED_ENCRYPT_PROTECTED_LEN: usize = 32;
25
26#[cfg_attr(feature = "defmt", derive(defmt::Format))]
31#[derive(minicbor::Decode, minicbor::Encode, Default, Debug)]
32#[cbor(map)]
33#[non_exhaustive]
34struct AceCbor<'a> {
35 #[cbor(b(1), with = "minicbor::bytes")]
36 access_token: Option<&'a [u8]>,
37 #[cbor(b(40), with = "minicbor::bytes")]
38 nonce1: Option<&'a [u8]>,
39 #[cbor(b(42), with = "minicbor::bytes")]
40 nonce2: Option<&'a [u8]>,
41 #[cbor(b(43), with = "minicbor::bytes")]
42 ace_client_recipientid: Option<&'a [u8]>,
43 #[cbor(b(44), with = "minicbor::bytes")]
44 ace_server_recipientid: Option<&'a [u8]>,
45}
46
47type UnprotectedAuthzInfoPost<'a> = AceCbor<'a>;
55
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
61#[derive(minicbor::Decode, Debug)]
62#[allow(
63 missing_docs,
64 reason = "Fields correspond 1:1 to the domain items of the same name"
65)]
66#[cbor(map)]
67#[non_exhaustive]
68pub struct HeaderMap<'a> {
69 #[n(1)]
70 pub alg: Option<i32>,
72 #[cbor(b(5), with = "minicbor::bytes")]
73 pub(crate) iv: Option<&'a [u8]>,
74}
75
76impl HeaderMap<'_> {
77 fn updated_with(&self, other: &Self) -> Self {
79 Self {
80 alg: self.alg.or(other.alg),
81 iv: self.iv.or(other.iv),
82 }
83 }
84}
85
86#[cfg_attr(feature = "defmt", derive(defmt::Format))]
94#[derive(minicbor::Decode, Debug)]
95#[allow(
96 dead_code,
97 reason = "Presence of the item makes CBOR derive tolerate the item"
98)]
99#[cbor(map)]
100#[non_exhaustive]
101pub(crate) struct CoseKey<'a> {
102 #[n(1)]
103 pub(crate) kty: i32, #[cbor(b(2), with = "minicbor::bytes")]
105 pub(crate) kid: Option<&'a [u8]>,
106 #[n(3)]
107 pub(crate) alg: Option<i32>, #[n(-1)]
110 pub(crate) crv: Option<i32>, #[cbor(b(-2), with = "minicbor::bytes")]
112 pub(crate) x: Option<&'a [u8]>,
113 #[cbor(b(-3), with = "minicbor::bytes")]
114 pub(crate) y: Option<&'a [u8]>, }
116
117#[cfg_attr(feature = "defmt", derive(defmt::Format))]
119#[derive(minicbor::Decode, Debug)]
120#[cbor(tag(16))]
121#[non_exhaustive]
122struct CoseEncrypt0<'a> {
123 #[cbor(b(0), with = "minicbor::bytes")]
124 protected: &'a [u8],
125 #[b(1)]
126 unprotected: HeaderMap<'a>,
127 #[cbor(b(2), with = "minicbor::bytes")]
128 encrypted: &'a [u8],
129}
130
131#[derive(minicbor::Encode)]
133struct Encrypt0<'a> {
134 #[n(0)]
135 context: &'static str,
136 #[cbor(b(1), with = "minicbor::bytes")]
137 protected: &'a [u8],
138 #[cbor(b(2), with = "minicbor::bytes")]
139 external_aad: &'a [u8],
140}
141const AADSIZE: usize = 1 + 1 + 8 + 1 + MAX_SUPPORTED_ENCRYPT_PROTECTED_LEN + 1;
144
145impl CoseEncrypt0<'_> {
146 fn prepare_decryption<'t>(
158 &self,
159 buffer: &'t mut heapless::Vec<u8, MAX_SUPPORTED_ACCESSTOKEN_LEN>,
160 ) -> Result<(HeaderMap<'_>, impl AsRef<[u8]>, &'t mut [u8]), CredentialError> {
161 trace!("Preparing decryption of {:?}", self);
162
163 let protected: HeaderMap = minicbor::decode(self.protected)?;
166 trace!("Protected decoded as header map: {:?}", protected);
167 let headers = self.unprotected.updated_with(&protected);
168
169 let aad = Encrypt0 {
170 context: "Encrypt0",
171 protected: self.protected,
172 external_aad: &[],
173 };
174 let mut aad_encoded = heapless::Vec::<u8, AADSIZE>::new();
175 minicbor::encode(&aad, minicbor_adapters::WriteToHeapless(&mut aad_encoded))
176 .map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
177 trace!(
178 "Serialized AAD: {}",
179 defmt_or_log::wrappers::Cbor(&aad_encoded)
180 );
181
182 buffer.clear();
183 #[expect(
187 clippy::ignored_unit_patterns,
188 reason = "heapless has non-recommended error type"
189 )]
190 buffer
191 .extend_from_slice(self.encrypted)
192 .map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
193
194 Ok((headers, aad_encoded, buffer))
195 }
196}
197
198type EncryptedCwt<'a> = CoseEncrypt0<'a>;
199
200#[cfg_attr(feature = "defmt", derive(defmt::Format))]
202#[derive(minicbor::Decode, Debug)]
203#[cbor(tag(18))]
204#[non_exhaustive]
205struct CoseSign1<'a> {
206 #[cbor(b(0), with = "minicbor::bytes")]
207 protected: &'a [u8],
208 #[b(1)]
209 unprotected: HeaderMap<'a>,
210 #[cbor(b(2), with = "minicbor::bytes")]
212 payload: &'a [u8],
213 #[cbor(b(3), with = "minicbor::bytes")]
214 signature: &'a [u8],
215}
216
217type SignedCwt<'a> = CoseSign1<'a>;
218
219#[derive(minicbor::Encode)]
221struct SigStructureForSignature1<'a> {
222 #[n(0)]
223 context: &'static str,
224 #[cbor(b(1), with = "minicbor::bytes")]
225 body_protected: &'a [u8],
226 #[cbor(b(2), with = "minicbor::bytes")]
227 external_aad: &'a [u8],
228 #[cbor(b(3), with = "minicbor::bytes")]
229 payload: &'a [u8],
230}
231
232#[derive(minicbor::Decode, Debug)]
237#[allow(
238 dead_code,
239 reason = "Presence of the item makes CBOR derive tolerate the item"
240)]
241#[allow(
242 missing_docs,
243 reason = "Fields correspond 1:1 to the domain items of the same name"
244)]
245#[cbor(map)]
246#[non_exhaustive]
247pub struct CwtClaimsSet<'a> {
248 #[n(3)]
249 pub aud: Option<&'a str>,
250 #[n(4)]
251 pub(crate) exp: u64,
252 #[n(6)]
253 pub(crate) iat: u64,
254 #[b(8)]
255 cnf: Cnf<'a>,
256 #[cbor(b(9), with = "minicbor::bytes")]
257 pub scope: &'a [u8],
258}
259
260#[derive(minicbor::Decode, Debug)]
275#[cbor(map)]
276#[non_exhaustive]
277struct Cnf<'a> {
278 #[b(4)]
279 osc: Option<OscoreInputMaterial<'a>>,
280 #[b(1)]
281 cose_key: Option<minicbor_adapters::WithOpaque<'a, CoseKey<'a>>>,
282}
283
284#[cfg_attr(feature = "defmt", derive(defmt::Format))]
292#[derive(minicbor::Decode, Debug)]
293#[allow(
294 dead_code,
295 reason = "Presence of the item makes CBOR derive tolerate the item"
296)]
297#[cbor(map)]
298#[non_exhaustive]
299struct OscoreInputMaterial<'a> {
300 #[cbor(b(0), with = "minicbor::bytes")]
301 id: &'a [u8],
302 #[cbor(b(2), with = "minicbor::bytes")]
303 ms: &'a [u8],
304}
305
306impl OscoreInputMaterial<'_> {
307 fn derive(
317 &self,
318 nonce1: &[u8],
319 nonce2: &[u8],
320 sender_id: &[u8],
321 recipient_id: &[u8],
322 ) -> Result<liboscore::PrimitiveContext, CredentialError> {
323 let hkdf = liboscore::HkdfAlg::from_number(5)
325 .map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
326 let aead = liboscore::AeadAlg::from_number(10)
327 .map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
328
329 const { assert!(OWN_NONCE_LEN < 256) };
332 const { assert!(MAX_SUPPORTED_PEER_NONCE_LEN < 256) };
333 let mut combined_salt =
334 heapless::Vec::<u8, { 1 + 2 + MAX_SUPPORTED_PEER_NONCE_LEN + 2 + OWN_NONCE_LEN }>::new(
335 );
336 let mut encoder =
337 minicbor::Encoder::new(minicbor_adapters::WriteToHeapless(&mut combined_salt));
338 encoder
340 .bytes(b"")
341 .and_then(|encoder| encoder.bytes(nonce1))
342 .and_then(|encoder| encoder.bytes(nonce2))?;
343
344 let immutables = liboscore::PrimitiveImmutables::derive(
345 hkdf,
346 self.ms,
347 &combined_salt,
348 None, aead,
350 sender_id,
351 recipient_id,
352 )
353 .map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
355
356 Ok(liboscore::PrimitiveContext::new_from_fresh_material(
358 immutables,
359 ))
360 }
361}
362
363pub struct AceCborAuthzInfoResponse {
369 nonce2: [u8; OWN_NONCE_LEN],
370 ace_server_recipientid: COwn,
371}
372
373impl AceCborAuthzInfoResponse {
374 #[allow(
381 clippy::missing_panics_doc,
382 reason = "will never panic for any user input"
383 )]
384 pub(crate) fn render<M: coap_message::MutableWritableMessage>(
385 &self,
386 message: &mut M,
387 ) -> Result<(), M::UnionError> {
388 let full = AceCbor {
389 nonce2: Some(&self.nonce2),
390 ace_server_recipientid: Some(self.ace_server_recipientid.as_slice()),
391 ..Default::default()
392 };
393
394 message.set_code(M::Code::new(coap_numbers::code::CHANGED)?);
395
396 const { assert!(OWN_NONCE_LEN < 256) };
397 const { assert!(COwn::MAX_SLICE_LEN < 256) };
398 let required_len = 1 + 2 + 2 + OWN_NONCE_LEN + 2 + 2 + COwn::MAX_SLICE_LEN;
399 let payload = message.payload_mut_with_len(required_len)?;
400
401 let mut cursor = minicbor::encode::write::Cursor::new(payload);
402 minicbor::encode(full, &mut cursor).expect("Sufficient size was requested");
403 let written = cursor.position();
404 message.truncate(written)?;
405
406 Ok(())
407 }
408}
409
410pub(crate) fn process_acecbor_authz_info<GC: crate::GeneralClaims>(
440 payload: &[u8],
441 authorities: &impl crate::seccfg::ServerSecurityConfig<GeneralClaims = GC>,
442 nonce2: [u8; OWN_NONCE_LEN],
443 server_recipient_id: impl FnOnce(&[u8]) -> COwn,
444) -> Result<(AceCborAuthzInfoResponse, liboscore::PrimitiveContext, GC), CredentialError> {
445 trace!(
446 "Processing authz_info {}",
447 defmt_or_log::wrappers::Cbor(payload)
448 );
449
450 let decoded: UnprotectedAuthzInfoPost = minicbor::decode(payload)?;
451 let AceCbor {
454 access_token: Some(access_token),
455 nonce1: Some(nonce1),
456 ace_client_recipientid: Some(ace_client_recipientid),
457 ..
458 } = decoded
459 else {
460 return Err(CredentialErrorDetail::ProtocolViolation.into());
461 };
462
463 trace!(
464 "Decodeded authz_info as application/ace+cbor: {:?}",
465 decoded
466 );
467
468 let encrypt0: EncryptedCwt = minicbor::decode(access_token)?;
469
470 let mut buffer = heapless::Vec::new();
471 let (headers, aad_encoded, buffer) = encrypt0.prepare_decryption(&mut buffer)?;
472
473 if headers.alg != Some(31) {
478 return Err(CredentialErrorDetail::UnsupportedAlgorithm.into());
479 }
480
481 let (processed, parsed) =
482 authorities.decrypt_symmetric_token(&headers, aad_encoded.as_ref(), buffer)?;
483
484 let Cnf {
489 osc: Some(osc),
490 cose_key: None,
491 } = parsed.cnf
492 else {
493 return Err(CredentialErrorDetail::InconsistentDetails.into());
494 };
495
496 let ace_server_recipientid = server_recipient_id(ace_client_recipientid);
497
498 let derived = osc.derive(
499 nonce1,
500 &nonce2,
501 ace_client_recipientid,
502 ace_server_recipientid.as_slice(),
503 )?;
504
505 let response = AceCborAuthzInfoResponse {
506 nonce2,
507 ace_server_recipientid,
508 };
509
510 Ok((response, derived, processed))
511}
512
513#[expect(
521 clippy::missing_panics_doc,
522 reason = "panic only happens when fixed-length array gets placed into larger array"
523)]
524pub(crate) fn process_edhoc_token<GeneralClaims>(
525 ead3: &[u8],
526 authorities: &impl crate::seccfg::ServerSecurityConfig<GeneralClaims = GeneralClaims>,
527) -> Result<(lakers::Credential, GeneralClaims), CredentialError> {
528 let mut buffer = heapless::Vec::<u8, MAX_SUPPORTED_ACCESSTOKEN_LEN>::new();
529
530 let (processed, parsed) = if let Ok(encrypt0) = minicbor::decode::<EncryptedCwt>(ead3) {
534 let (headers, aad_encoded, buffer) = encrypt0.prepare_decryption(&mut buffer)?;
535
536 authorities.decrypt_symmetric_token(&headers, aad_encoded.as_ref(), buffer)?
537 } else if let Ok(sign1) = minicbor::decode::<SignedCwt>(ead3) {
538 let protected: HeaderMap = minicbor::decode(sign1.protected)?;
539 trace!(
540 "Decoded protected header map {:?} inside sign1 container {:?}",
541 &protected, &sign1
542 );
543 let headers = sign1.unprotected.updated_with(&protected);
544
545 let aad = SigStructureForSignature1 {
546 context: "Signature1",
547 body_protected: sign1.protected,
548 external_aad: &[],
549 payload: sign1.payload,
550 };
551 buffer = heapless::Vec::new();
552 minicbor::encode(&aad, minicbor_adapters::WriteToHeapless(&mut buffer))?;
553 trace!("Serialized AAD: {}", defmt_or_log::wrappers::Hex(&buffer));
554
555 authorities.verify_asymmetric_token(&headers, &buffer, sign1.signature, sign1.payload)?
556 } else {
557 return Err(CredentialErrorDetail::UnsupportedExtension.into());
558 };
559
560 let Cnf {
561 osc: None,
562 cose_key: Some(cose_key),
563 } = parsed.cnf
564 else {
565 return Err(CredentialErrorDetail::InconsistentDetails.into());
566 };
567
568 let mut prefixed = lakers::BufferCred::new();
569 prefixed
571 .extend_from_slice(&[0xa1, 0x08, 0xa1, 0x01])
572 .unwrap();
573 prefixed
574 .extend_from_slice(&cose_key.opaque)
575 .map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
576 let credential = lakers::Credential::new_ccs(
577 prefixed,
578 cose_key
579 .parsed
580 .x
581 .ok_or(CredentialErrorDetail::InconsistentDetails)?
582 .try_into()
583 .map_err(|_| CredentialErrorDetail::InconsistentDetails)?,
584 );
585
586 Ok((credential, processed))
587}