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 buffer
187 .extend_from_slice(self.encrypted)
188 .map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
189
190 Ok((headers, aad_encoded, buffer))
191 }
192}
193
194type EncryptedCwt<'a> = CoseEncrypt0<'a>;
195
196#[cfg_attr(feature = "defmt", derive(defmt::Format))]
198#[derive(minicbor::Decode, Debug)]
199#[cbor(tag(18))]
200#[non_exhaustive]
201struct CoseSign1<'a> {
202 #[cbor(b(0), with = "minicbor::bytes")]
203 protected: &'a [u8],
204 #[b(1)]
205 unprotected: HeaderMap<'a>,
206 #[cbor(b(2), with = "minicbor::bytes")]
208 payload: &'a [u8],
209 #[cbor(b(3), with = "minicbor::bytes")]
210 signature: &'a [u8],
211}
212
213type SignedCwt<'a> = CoseSign1<'a>;
214
215#[derive(minicbor::Encode)]
217struct SigStructureForSignature1<'a> {
218 #[n(0)]
219 context: &'static str,
220 #[cbor(b(1), with = "minicbor::bytes")]
221 body_protected: &'a [u8],
222 #[cbor(b(2), with = "minicbor::bytes")]
223 external_aad: &'a [u8],
224 #[cbor(b(3), with = "minicbor::bytes")]
225 payload: &'a [u8],
226}
227
228#[derive(minicbor::Decode, Debug)]
233#[allow(
234 dead_code,
235 reason = "Presence of the item makes CBOR derive tolerate the item"
236)]
237#[allow(
238 missing_docs,
239 reason = "Fields correspond 1:1 to the domain items of the same name"
240)]
241#[cbor(map)]
242#[non_exhaustive]
243pub struct CwtClaimsSet<'a> {
244 #[n(3)]
245 pub aud: Option<&'a str>,
246 #[n(4)]
247 pub(crate) exp: u64,
248 #[n(6)]
249 pub(crate) iat: u64,
250 #[b(8)]
251 cnf: Cnf<'a>,
252 #[cbor(b(9), with = "minicbor::bytes")]
253 pub scope: &'a [u8],
254}
255
256#[derive(minicbor::Decode, Debug)]
271#[cbor(map)]
272#[non_exhaustive]
273struct Cnf<'a> {
274 #[b(4)]
275 osc: Option<OscoreInputMaterial<'a>>,
276 #[b(1)]
277 cose_key: Option<minicbor_adapters::WithOpaque<'a, CoseKey<'a>>>,
278}
279
280#[cfg_attr(feature = "defmt", derive(defmt::Format))]
288#[derive(minicbor::Decode, Debug)]
289#[allow(
290 dead_code,
291 reason = "Presence of the item makes CBOR derive tolerate the item"
292)]
293#[cbor(map)]
294#[non_exhaustive]
295struct OscoreInputMaterial<'a> {
296 #[cbor(b(0), with = "minicbor::bytes")]
297 id: &'a [u8],
298 #[cbor(b(2), with = "minicbor::bytes")]
299 ms: &'a [u8],
300}
301
302impl OscoreInputMaterial<'_> {
303 fn derive(
313 &self,
314 nonce1: &[u8],
315 nonce2: &[u8],
316 sender_id: &[u8],
317 recipient_id: &[u8],
318 ) -> Result<liboscore::PrimitiveContext, CredentialError> {
319 let hkdf = liboscore::HkdfAlg::from_number(5)
321 .map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
322 let aead = liboscore::AeadAlg::from_number(10)
323 .map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
324
325 const { assert!(OWN_NONCE_LEN < 256) };
328 const { assert!(MAX_SUPPORTED_PEER_NONCE_LEN < 256) };
329 let mut combined_salt =
330 heapless::Vec::<u8, { 1 + 2 + MAX_SUPPORTED_PEER_NONCE_LEN + 2 + OWN_NONCE_LEN }>::new(
331 );
332 let mut encoder =
333 minicbor::Encoder::new(minicbor_adapters::WriteToHeapless(&mut combined_salt));
334 encoder
336 .bytes(b"")
337 .and_then(|encoder| encoder.bytes(nonce1))
338 .and_then(|encoder| encoder.bytes(nonce2))?;
339
340 let immutables = liboscore::PrimitiveImmutables::derive(
341 hkdf,
342 self.ms,
343 &combined_salt,
344 None, aead,
346 sender_id,
347 recipient_id,
348 )
349 .map_err(|_| CredentialErrorDetail::UnsupportedAlgorithm)?;
351
352 Ok(liboscore::PrimitiveContext::new_from_fresh_material(
354 immutables,
355 ))
356 }
357}
358
359pub struct AceCborAuthzInfoResponse {
365 nonce2: [u8; OWN_NONCE_LEN],
366 ace_server_recipientid: COwn,
367}
368
369impl AceCborAuthzInfoResponse {
370 #[allow(
377 clippy::missing_panics_doc,
378 reason = "will never panic for any user input"
379 )]
380 pub(crate) fn render<M: coap_message::MutableWritableMessage>(
381 &self,
382 message: &mut M,
383 ) -> Result<(), M::UnionError> {
384 let full = AceCbor {
385 nonce2: Some(&self.nonce2),
386 ace_server_recipientid: Some(self.ace_server_recipientid.as_slice()),
387 ..Default::default()
388 };
389
390 message.set_code(M::Code::new(coap_numbers::code::CHANGED)?);
391
392 const { assert!(OWN_NONCE_LEN < 256) };
393 const { assert!(COwn::MAX_SLICE_LEN < 256) };
394 let required_len = 1 + 2 + 2 + OWN_NONCE_LEN + 2 + 2 + COwn::MAX_SLICE_LEN;
395 let payload = message.payload_mut_with_len(required_len)?;
396
397 let mut cursor = minicbor::encode::write::Cursor::new(payload);
398 minicbor::encode(full, &mut cursor).expect("Sufficient size was requested");
399 let written = cursor.position();
400 message.truncate(written)?;
401
402 Ok(())
403 }
404}
405
406pub(crate) fn process_acecbor_authz_info<GC: crate::GeneralClaims>(
436 payload: &[u8],
437 authorities: &impl crate::seccfg::ServerSecurityConfig<GeneralClaims = GC>,
438 nonce2: [u8; OWN_NONCE_LEN],
439 server_recipient_id: impl FnOnce(&[u8]) -> COwn,
440) -> Result<(AceCborAuthzInfoResponse, liboscore::PrimitiveContext, GC), CredentialError> {
441 trace!(
442 "Processing authz_info {}",
443 defmt_or_log::wrappers::Cbor(payload)
444 );
445
446 let decoded: UnprotectedAuthzInfoPost = minicbor::decode(payload)?;
447 let AceCbor {
450 access_token: Some(access_token),
451 nonce1: Some(nonce1),
452 ace_client_recipientid: Some(ace_client_recipientid),
453 ..
454 } = decoded
455 else {
456 return Err(CredentialErrorDetail::ProtocolViolation.into());
457 };
458
459 trace!(
460 "Decodeded authz_info as application/ace+cbor: {:?}",
461 decoded
462 );
463
464 let encrypt0: EncryptedCwt = minicbor::decode(access_token)?;
465
466 let mut buffer = heapless::Vec::new();
467 let (headers, aad_encoded, buffer) = encrypt0.prepare_decryption(&mut buffer)?;
468
469 if headers.alg != Some(31) {
474 return Err(CredentialErrorDetail::UnsupportedAlgorithm.into());
475 }
476
477 let (processed, parsed) =
478 authorities.decrypt_symmetric_token(&headers, aad_encoded.as_ref(), buffer)?;
479
480 let Cnf {
485 osc: Some(osc),
486 cose_key: None,
487 } = parsed.cnf
488 else {
489 return Err(CredentialErrorDetail::InconsistentDetails.into());
490 };
491
492 let ace_server_recipientid = server_recipient_id(ace_client_recipientid);
493
494 let derived = osc.derive(
495 nonce1,
496 &nonce2,
497 ace_client_recipientid,
498 ace_server_recipientid.as_slice(),
499 )?;
500
501 let response = AceCborAuthzInfoResponse {
502 nonce2,
503 ace_server_recipientid,
504 };
505
506 Ok((response, derived, processed))
507}
508
509#[expect(
517 clippy::missing_panics_doc,
518 reason = "panic only happens when fixed-length array gets placed into larger array"
519)]
520pub(crate) fn process_edhoc_token<GeneralClaims>(
521 ead3: &[u8],
522 authorities: &impl crate::seccfg::ServerSecurityConfig<GeneralClaims = GeneralClaims>,
523) -> Result<(lakers::Credential, GeneralClaims), CredentialError> {
524 let mut buffer = heapless::Vec::<u8, MAX_SUPPORTED_ACCESSTOKEN_LEN>::new();
525
526 let (processed, parsed) = if let Ok(encrypt0) = minicbor::decode::<EncryptedCwt>(ead3) {
530 let (headers, aad_encoded, buffer) = encrypt0.prepare_decryption(&mut buffer)?;
531
532 authorities.decrypt_symmetric_token(&headers, aad_encoded.as_ref(), buffer)?
533 } else if let Ok(sign1) = minicbor::decode::<SignedCwt>(ead3) {
534 let protected: HeaderMap = minicbor::decode(sign1.protected)?;
535 trace!(
536 "Decoded protected header map {:?} inside sign1 container {:?}",
537 &protected, &sign1
538 );
539 let headers = sign1.unprotected.updated_with(&protected);
540
541 let aad = SigStructureForSignature1 {
542 context: "Signature1",
543 body_protected: sign1.protected,
544 external_aad: &[],
545 payload: sign1.payload,
546 };
547 buffer = heapless::Vec::new();
548 minicbor::encode(&aad, minicbor_adapters::WriteToHeapless(&mut buffer))?;
549 trace!("Serialized AAD: {}", defmt_or_log::wrappers::Hex(&buffer));
550
551 authorities.verify_asymmetric_token(&headers, &buffer, sign1.signature, sign1.payload)?
552 } else {
553 return Err(CredentialErrorDetail::UnsupportedExtension.into());
554 };
555
556 let Cnf {
557 osc: None,
558 cose_key: Some(cose_key),
559 } = parsed.cnf
560 else {
561 return Err(CredentialErrorDetail::InconsistentDetails.into());
562 };
563
564 let mut prefixed = lakers::BufferCred::new();
565 prefixed
567 .extend_from_slice(&[0xa1, 0x08, 0xa1, 0x01])
568 .unwrap();
569 prefixed
570 .extend_from_slice(&cose_key.opaque)
571 .map_err(|_| CredentialErrorDetail::ConstraintExceeded)?;
572 let credential = lakers::Credential::new_ccs(
573 prefixed,
574 cose_key
575 .parsed
576 .x
577 .ok_or(CredentialErrorDetail::InconsistentDetails)?
578 .try_into()
579 .map_err(|_| CredentialErrorDetail::InconsistentDetails)?,
580 );
581
582 Ok((credential, processed))
583}