lakers_shared/
cred.rs

1use super::*;
2
3pub type BufferCred = EdhocBuffer<192>; // arbitrary size
4pub type BufferKid = EdhocBuffer<16>; // variable size, up to 16 bytes
5pub type BufferIdCred = EdhocBuffer<192>; // variable size, can contain either the contents of a BufferCred or a BufferKid
6pub type BytesKeyAES128 = [u8; 16];
7pub type BytesKeyEC2 = [u8; 32];
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10#[repr(C)]
11pub enum CredentialKey {
12    Symmetric(BytesKeyAES128),
13    EC2Compact(BytesKeyEC2),
14    // Add other key types as needed
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18#[repr(C)]
19pub enum CredentialType {
20    CCS,
21    #[allow(non_camel_case_types)]
22    CCS_PSK,
23    // Add other credential types as needed
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum IdCredType {
28    KID = 4,
29    KCCS = 14,
30}
31
32impl From<u8> for IdCredType {
33    fn from(value: u8) -> Self {
34        match value {
35            4 => IdCredType::KID,
36            14 => IdCredType::KCCS,
37            _ => panic!("Invalid IdCredType"),
38        }
39    }
40}
41
42/// A value of ID_CRED_x: a credential identifier.
43///
44/// Possible values include key IDs, credentials by value and others.
45///
46/// ```rust
47/// # use hexlit::hex;
48/// # use lakers_shared::IdCred;
49/// let short_kid = IdCred::from_encoded_value(&hex!("17")).unwrap(); // 23
50/// assert_eq!(short_kid.as_full_value(), &hex!("a1044117")); // {4: h'17'}
51/// let long_kid = IdCred::from_encoded_value(&hex!("43616263")).unwrap(); // 'abc'
52/// assert_eq!(long_kid.as_full_value(), &hex!("a10443616263")); // {4: 'abc'}
53/// ```
54#[derive(Clone, Copy, Debug, Default, PartialEq)]
55#[repr(C)]
56pub struct IdCred {
57    /// The value is always stored in the ID_CRED_x form as a serialized one-element dictionary;
58    /// while this technically wastes two bytes, it has the convenient property of having the full
59    /// value available as a slice.
60    pub bytes: BufferIdCred, // variable size, can contain either the contents of a BufferCred or a BufferKid
61}
62
63impl IdCred {
64    pub fn new() -> Self {
65        Self {
66            bytes: BufferIdCred::new(),
67        }
68    }
69
70    pub fn from_full_value(value: &[u8]) -> Result<Self, EDHOCError> {
71        Ok(Self {
72            bytes: BufferIdCred::new_from_slice(value)
73                .map_err(|_| EDHOCError::CredentialTooLongError)?,
74        })
75    }
76
77    /// Instantiate an IdCred from an encoded value.
78    pub fn from_encoded_value(value: &[u8]) -> Result<Self, EDHOCError> {
79        let bytes = match value {
80            // kid that has been encoded as CBOR integer
81            &[x] if Self::bstr_representable_as_int(x) => {
82                BufferIdCred::new_from_slice(&[0xa1, KID_LABEL, 0x41, x])
83                    .map_err(|_| EDHOCError::CredentialTooLongError)? // TODO: how to avoid map_err overuse?
84            }
85            // kid that has been encoded as CBOR byte string; supporting up to 23 long because
86            // those are easy
87            &[0x40..=0x57, ..] => {
88                let tail = &value[1..];
89                if let &[single_byte] = tail {
90                    if Self::bstr_representable_as_int(single_byte) {
91                        // We require precise encoding
92                        return Err(EDHOCError::ParsingError);
93                    }
94                }
95                if usize::from(value[0] - 0x40) != tail.len() {
96                    // Missing or trailing bytes. This is impossible when called from within Lakers
97                    // where the value is a `.any_as_encoded()`.
98                    return Err(EDHOCError::ParsingError);
99                }
100                let mut bytes = BufferIdCred::new_from_slice(&[0xa1, KID_LABEL])
101                    .map_err(|_| EDHOCError::CredentialTooLongError)?;
102                bytes
103                    .extend_from_slice(value)
104                    .map_err(|_| EDHOCError::CredentialTooLongError)?;
105                bytes
106            }
107            // CCS by value
108            &[0xa1, KCCS_LABEL, ..] => BufferIdCred::new_from_slice(value)
109                .map_err(|_| EDHOCError::CredentialTooLongError)?,
110            _ => return Err(EDHOCError::ParsingError),
111        };
112
113        Ok(Self { bytes })
114    }
115
116    /// View the full value of the ID_CRED_x: the CBOR encoding of a 1-element CBOR map
117    ///
118    /// This is the value that is used when ID_CRED_x has no impact on message size, see RFC 9528 Section 3.5.3.2.
119    pub fn as_full_value(&self) -> &[u8] {
120        self.bytes.as_slice()
121    }
122
123    /// View the value as encoded in the ID_CRED_x position of plaintext_2 and plaintext_3.
124    ///
125    /// Note that this is NOT doing CBOR encoding, it is rather performing (when applicable)
126    /// the compact encoding of ID_CRED fields.
127    /// This style of encoding is used when ID_CRED_x has an impact on message size.
128    pub fn as_encoded_value(&self) -> &[u8] {
129        match self.bytes.as_slice() {
130            [0xa1, KID_LABEL, 0x41, x] if (x >> 5) < 2 && (x & 0x1f) < 24 => {
131                &self.bytes.as_slice()[3..]
132            }
133            [0xa1, KID_LABEL, ..] => &self.bytes.as_slice()[2..],
134            _ => self.bytes.as_slice(),
135        }
136    }
137
138    pub fn reference_only(&self) -> bool {
139        [IdCredType::KID].contains(&self.item_type())
140    }
141
142    pub fn item_type(&self) -> IdCredType {
143        self.bytes.as_slice()[1].into()
144    }
145
146    pub fn get_ccs(&self) -> Option<Credential> {
147        if self.item_type() == IdCredType::KCCS {
148            Credential::parse_ccs(&self.bytes.as_slice()[2..]).ok()
149        } else {
150            None
151        }
152    }
153
154    fn bstr_representable_as_int(value: u8) -> bool {
155        (0x0..=0x17).contains(&value) || (0x20..=0x37).contains(&value)
156    }
157}
158
159/// A credential for use in EDHOC
160///
161/// For now supports CCS credentials only.
162/// Experimental support for CCS_PSK credentials is also available.
163// TODO: add back support for C and Python bindings
164#[cfg_attr(feature = "python-bindings", pyclass)]
165#[derive(Clone, Copy, Debug, PartialEq)]
166#[repr(C)]
167pub struct Credential {
168    /// Original bytes of the credential, CBOR-encoded
169    ///
170    /// If the credential is a CCS, it contains an encoded CBOR map containnig
171    /// a COSE_Key in a cnf claim, see RFC 9528 Section 3.5.2.
172    pub bytes: BufferCred,
173    pub key: CredentialKey,
174    pub kid: Option<BufferKid>, // other types of identifiers can be added, such as `pub x5t: Option<BytesX5T>`
175    pub cred_type: CredentialType,
176}
177
178impl Credential {
179    /// Creates a new CCS credential with the given bytes and public key
180    pub fn new_ccs(bytes: BufferCred, public_key: BytesKeyEC2) -> Self {
181        Self {
182            bytes,
183            key: CredentialKey::EC2Compact(public_key),
184            kid: None,
185            cred_type: CredentialType::CCS,
186        }
187    }
188
189    /// Creates a new CCS credential with the given bytes and a pre-shared key
190    ///
191    /// NOTE: For now this is only useful for the experimental PSK method.
192    pub fn new_ccs_symmetric(bytes: BufferCred, symmetric_key: BytesKeyAES128) -> Self {
193        Self {
194            bytes,
195            key: CredentialKey::Symmetric(symmetric_key),
196            kid: None,
197            cred_type: CredentialType::CCS_PSK,
198        }
199    }
200
201    pub fn with_kid(self, kid: BufferKid) -> Self {
202        Self {
203            kid: Some(kid),
204            ..self
205        }
206    }
207
208    pub fn public_key(&self) -> Option<BytesKeyEC2> {
209        match self.key {
210            CredentialKey::EC2Compact(key) => Some(key),
211            _ => None,
212        }
213    }
214
215    /// Parse a CCS style credential.
216    ///
217    /// If the given value matches the shape lakers expects of a CCS, i.e. credentials from RFC9529,
218    /// its public key and key ID are extracted into a full credential.
219    pub fn parse_ccs(value: &[u8]) -> Result<Self, EDHOCError> {
220        let mut decoder = CBORDecoder::new(value);
221        let mut x_kid = None;
222        for _ in 0..decoder.map()? {
223            match decoder.u8()? {
224                // subject: ignored
225                2 => {
226                    let _subject = decoder.str()?;
227                }
228                // cnf
229                8 => {
230                    if decoder.map()? != 1 {
231                        // cnf is always single-item'd
232                        return Err(EDHOCError::ParsingError);
233                    }
234
235                    if decoder.u8()? != 1 {
236                        // Unexpected cnf
237                        return Err(EDHOCError::ParsingError);
238                    }
239
240                    x_kid = Some(Self::parse_cosekey(&mut decoder)?);
241                }
242                _ => {
243                    return Err(EDHOCError::ParsingError);
244                }
245            }
246        }
247
248        let Some((x, kid)) = x_kid else {
249            // Missing critical component
250            return Err(EDHOCError::ParsingError);
251        };
252
253        if !decoder.finished() {
254            return Err(EDHOCError::ParsingError);
255        }
256
257        Ok(Self {
258            bytes: BufferCred::new_from_slice(value).map_err(|_| EDHOCError::ParsingError)?,
259            key: x,
260            kid,
261            cred_type: CredentialType::CCS,
262        })
263    }
264
265    /// Parse a CCS style credential, but the key is a symmetric key.
266    ///
267    /// NOTE: For now this is only useful for the experimental PSK method.
268    pub fn parse_ccs_symmetric(value: &[u8]) -> Result<Self, EDHOCError> {
269        const CCS_PREFIX_LEN: usize = 3;
270        const CNF_AND_COSE_KEY_PREFIX_LEN: usize = 8;
271        const COSE_KEY_FIRST_ITEMS_LEN: usize = 3; //COSE for symmetric key
272        const SYMMETRIC_KEY_LEN: usize = 16; // Assuming a 128-bit symmetric key
273
274        if value.len()
275            < CCS_PREFIX_LEN
276                + 1
277                + CNF_AND_COSE_KEY_PREFIX_LEN
278                + COSE_KEY_FIRST_ITEMS_LEN
279                + SYMMETRIC_KEY_LEN
280        {
281            Err(EDHOCError::ParsingError)
282        } else {
283            let subject_len = CBORDecoder::info_of(value[2]) as usize;
284
285            let id_cred_offset: usize = CCS_PREFIX_LEN
286                .checked_add(subject_len)
287                .and_then(|x| x.checked_add(CNF_AND_COSE_KEY_PREFIX_LEN))
288                .ok_or(EDHOCError::ParsingError)?;
289
290            let symmetric_key_offset: usize = id_cred_offset
291                .checked_add(COSE_KEY_FIRST_ITEMS_LEN)
292                .ok_or(EDHOCError::ParsingError)?;
293
294            if symmetric_key_offset
295                .checked_add(SYMMETRIC_KEY_LEN)
296                .map_or(false, |end| end <= value.len())
297            {
298                let symmetric_key: [u8; SYMMETRIC_KEY_LEN] = value
299                    [symmetric_key_offset..symmetric_key_offset + SYMMETRIC_KEY_LEN]
300                    .try_into()
301                    .map_err(|_| EDHOCError::ParsingError)?;
302
303                let kid = value[id_cred_offset];
304
305                Ok(Self {
306                    bytes: BufferCred::new_from_slice(value)
307                        .map_err(|_| EDHOCError::ParsingError)?,
308                    key: CredentialKey::Symmetric(symmetric_key),
309                    kid: Some(BufferKid::new_from_slice(&[kid]).unwrap()),
310                    cred_type: CredentialType::CCS_PSK,
311                })
312            } else {
313                Err(EDHOCError::ParsingError)
314            }
315        }
316    }
317
318    /// Parse a COSE Key, accepting only understood fields.
319    ///
320    /// This takes a decoder rather than a slice because this enables a naked decoder to assert that
321    /// the decoder is done, and others to continue.
322    ///
323    /// This function does not try to require deterministic encoding, as that is not exposed by the
324    /// decoder. (Adding it would be possible, but would not just mean asserting monotony, but also
325    /// requiring it integer encodings etc).
326    fn parse_cosekey<'data>(
327        decoder: &mut CBORDecoder<'data>,
328    ) -> Result<(CredentialKey, Option<BufferKid>), EDHOCError> {
329        let items = decoder.map()?;
330        let mut x = None;
331        let mut kid = None;
332        for _ in 0..items {
333            match decoder.i8()? {
334                // kty: EC2
335                1 => {
336                    if decoder.u8()? != 2 {
337                        return Err(EDHOCError::ParsingError);
338                    }
339                }
340                // kid: bytes. Note that this is always a byte string, even if in other places it's used
341                // with integer compression.
342                2 => {
343                    kid = Some(
344                        BufferKid::new_from_slice(decoder.bytes()?)
345                            // Could be too long
346                            .map_err(|_| EDHOCError::ParsingError)?,
347                    );
348                }
349                // crv: p-256
350                -1 => {
351                    if decoder.u8()? != 1 {
352                        return Err(EDHOCError::ParsingError);
353                    }
354                }
355                // x
356                -2 => {
357                    x = Some(CredentialKey::EC2Compact(
358                        decoder
359                            .bytes()?
360                            // Wrong length
361                            .try_into()
362                            .map_err(|_| EDHOCError::ParsingError)?,
363                    ));
364                }
365                // y
366                -3 => {
367                    let _ = decoder.bytes()?;
368                }
369                _ => {
370                    return Err(EDHOCError::ParsingError);
371                }
372            }
373        }
374        Ok((x.ok_or(EDHOCError::ParsingError)?, kid))
375    }
376
377    /// Dress a naked COSE_Key as a CCS by prepending 0xA108A101 as specified in Section 3.5.2 of
378    /// RFC9528
379    ///
380    ///
381    /// # Usage example
382    ///
383    /// ```
384    /// # use hexlit::hex;
385    /// let key = hex!("a301022001215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff");
386    /// let ccs = lakers_shared::Credential::parse_and_dress_naked_cosekey(&key).unwrap();
387    /// // The key bytes that are part of the input
388    /// assert!(ccs.public_key().unwrap().as_slice().starts_with(&hex!("bac5b1")));
389    /// // This particular key does not contain a KID
390    /// assert!(ccs.kid.is_none());
391    /// // This is true for all dressed naked COSE keys
392    /// assert!(ccs.bytes.as_slice().starts_with(&hex!("a108a101")));
393    /// ```
394    pub fn parse_and_dress_naked_cosekey(cosekey: &[u8]) -> Result<Self, EDHOCError> {
395        let mut decoder = CBORDecoder::new(cosekey);
396        let (key, kid) = Self::parse_cosekey(&mut decoder)?;
397        if !decoder.finished() {
398            return Err(EDHOCError::ParsingError);
399        }
400        let mut bytes = BufferCred::new();
401        bytes
402            .extend_from_slice(&[0xa1, 0x08, 0xa1, 0x01])
403            .expect("Minimal size fits in the buffer");
404        bytes
405            .extend_from_slice(cosekey)
406            .map_err(|_| EDHOCError::CredentialTooLongError)?;
407        Ok(Self {
408            bytes,
409            key,
410            kid,
411            cred_type: CredentialType::CCS,
412        })
413    }
414
415    /// Returns a COSE_Header map with a single entry representing a credential by value.
416    ///
417    /// For example, if the credential is a CCS:
418    ///   { /kccs/ 14: bytes }
419    pub fn by_value(&self) -> Result<IdCred, EDHOCError> {
420        match self.cred_type {
421            CredentialType::CCS => {
422                let mut id_cred = IdCred::new();
423                id_cred
424                    .bytes
425                    .extend_from_slice(&[CBOR_MAJOR_MAP + 1, KCCS_LABEL])
426                    .map_err(|_| EDHOCError::CredentialTooLongError)?;
427                id_cred
428                    .bytes
429                    .extend_from_slice(self.bytes.as_slice())
430                    .unwrap();
431                Ok(id_cred)
432            }
433            // if we could encode a message along the error below,
434            // it would be this: "Symmetric keys cannot be sent by value"
435            CredentialType::CCS_PSK => Err(EDHOCError::UnexpectedCredential),
436        }
437    }
438
439    /// Returns a COSE_Header map with a single entry representing a credential by reference.
440    ///
441    /// For example, if the reference is a kid:
442    ///   { /kid/ 4: kid }
443    ///
444    /// TODO: accept a parameter to specify the type of reference, e.g. kid, x5t, etc.
445    pub fn by_kid(&self) -> Result<IdCred, EDHOCError> {
446        let Some(kid) = self.kid.as_ref() else {
447            return Err(EDHOCError::MissingIdentity);
448        };
449        let mut id_cred = IdCred::new();
450        id_cred
451            .bytes
452            .extend_from_slice(&[
453                CBOR_MAJOR_MAP + 1,
454                KID_LABEL,
455                CBOR_MAJOR_BYTE_STRING | kid.len() as u8,
456            ])
457            .map_err(|_| EDHOCError::CredentialTooLongError)?;
458        id_cred.bytes.extend_from_slice(kid.as_slice()).unwrap();
459        Ok(id_cred)
460    }
461}
462
463#[cfg(test)]
464mod test {
465    use super::*;
466    use hexlit::hex;
467    use rstest::rstest;
468
469    const CRED_TV: &[u8] = &hex!("a2026b6578616d706c652e65647508a101a501020241322001215820bbc34960526ea4d32e940cad2a234148ddc21791a12afbcbac93622046dd44f02258204519e257236b2a0ce2023f0931f1f386ca7afda64fcde0108c224c51eabf6072");
470    const G_A_TV: &[u8] = &hex!("BBC34960526EA4D32E940CAD2A234148DDC21791A12AFBCBAC93622046DD44F0");
471    const ID_CRED_BY_REF_TV: &[u8] = &hex!("a1044132");
472    const ID_CRED_BY_VALUE_TV: &[u8] = &hex!("A10EA2026B6578616D706C652E65647508A101A501020241322001215820BBC34960526EA4D32E940CAD2A234148DDC21791A12AFBCBAC93622046DD44F02258204519E257236B2A0CE2023F0931F1F386CA7AFDA64FCDE0108C224C51EABF6072");
473    const KID_VALUE_TV: &[u8] = &hex!("32");
474
475    const CRED_PSK: &[u8] =
476        &hex!("A202686D79646F74626F7408A101A30104024132205050930FF462A77A3540CF546325DEA214");
477    const K: &[u8] = &hex!("50930FF462A77A3540CF546325DEA214");
478    const KID_VALUE_PSK: &[u8] = &hex!("32");
479
480    #[test]
481    fn test_new_cred_ccs() {
482        let cred = Credential::new_ccs(CRED_TV.try_into().unwrap(), G_A_TV.try_into().unwrap());
483        assert_eq!(cred.bytes.as_slice(), CRED_TV);
484    }
485
486    #[test]
487    fn test_cred_ccs_by_value_or_reference() {
488        let cred = Credential::new_ccs(CRED_TV.try_into().unwrap(), G_A_TV.try_into().unwrap())
489            .with_kid(KID_VALUE_TV.try_into().unwrap());
490        let id_cred = cred.by_value().unwrap();
491        assert_eq!(id_cred.bytes.as_slice(), ID_CRED_BY_VALUE_TV);
492        assert_eq!(id_cred.item_type(), IdCredType::KCCS);
493        let id_cred = cred.by_kid().unwrap();
494        assert_eq!(id_cred.bytes.as_slice(), ID_CRED_BY_REF_TV);
495        assert_eq!(id_cred.item_type(), IdCredType::KID);
496    }
497
498    #[test]
499    fn test_parse_ccs() {
500        let cred = Credential::parse_ccs(CRED_TV).unwrap();
501        assert_eq!(cred.bytes.as_slice(), CRED_TV);
502        assert_eq!(
503            cred.key,
504            CredentialKey::EC2Compact(G_A_TV.try_into().unwrap())
505        );
506        assert_eq!(cred.kid.unwrap().as_slice(), KID_VALUE_TV);
507        assert_eq!(cred.cred_type, CredentialType::CCS);
508
509        // A CCS without a subject.
510        let cred_no_sub = hex!("a108a101a401022001215820f5aeba08b599754ba16f5db80feafdf91e90a5a7ccb2e83178adb51b8c68ea9522582097e7a3fdd70a3a7c0a5f9578c6e4e96d8bc55f6edd0ff64f1caeaac19d37b67d");
511        // A CCS without a KID.
512        let cred_no_kid = hex!("a20263666f6f08a101a401022001215820f5aeba08b599754ba16f5db80feafdf91e90a5a7ccb2e83178adb51b8c68ea9522582097e7a3fdd70a3a7c0a5f9578c6e4e96d8bc55f6edd0ff64f1caeaac19d37b67d");
513        for cred in [cred_no_sub.as_slice(), cred_no_kid.as_slice()] {
514            let CredentialKey::EC2Compact(key) = Credential::parse_ccs(&cred).unwrap().key else {
515                panic!("CCS contains unexpected key type.");
516            };
517            assert!(key.as_slice().starts_with(&hex!("f5aeba08b59975")));
518        }
519
520        // A CCS with an issuer.
521        // It's OK if this starts working in future, but then its public key needs to start with
522        // F5AEBA08B599754 (it'd be clearly wrong if this produced an Ok value with a different
523        // public key).
524        let cred_exotic = hex!("a2016008a101a401022001215820f5aeba08b599754ba16f5db80feafdf91e90a5a7ccb2e83178adb51b8c68ea9522582097e7a3fdd70a3a7c0a5f9578c6e4e96d8bc55f6edd0ff64f1caeaac19d37b67d");
525        Credential::parse_ccs(&cred_exotic).unwrap_err();
526    }
527
528    #[rstest]
529    #[case(&[0x0D], &[0xa1, 0x04, 0x41, 0x0D])] // two optimizations: omit kid label and encode as CBOR integer
530    #[case(&[0x41, 0x18], &[0xa1, 0x04, 0x41, 0x18])] // one optimization: omit kid label
531    #[case(ID_CRED_BY_VALUE_TV, ID_CRED_BY_VALUE_TV)] // regular credential by value
532    fn test_id_cred_from_encoded_plaintext(#[case] input: &[u8], #[case] expected: &[u8]) {
533        assert_eq!(
534            IdCred::from_encoded_value(input).unwrap().as_full_value(),
535            expected
536        );
537    }
538}
539
540#[cfg(test)]
541mod test_experimental {
542    use super::*;
543    use hexlit::hex;
544
545    const CRED_PSK: &[u8] =
546        &hex!("A202686D79646F74626F7408A101A30104024132205050930FF462A77A3540CF546325DEA214");
547    const K: &[u8] = &hex!("50930FF462A77A3540CF546325DEA214");
548    const KID_VALUE_PSK: &[u8] = &hex!("32");
549
550    #[test]
551    fn test_cred_ccs_symmetric_by_value_or_reference() {
552        // TODO
553    }
554
555    #[test]
556    fn test_new_cred_ccs_symmetric() {
557        let cred =
558            Credential::new_ccs_symmetric(CRED_PSK.try_into().unwrap(), K.try_into().unwrap());
559        assert_eq!(cred.bytes.as_slice(), CRED_PSK);
560    }
561
562    #[test]
563    fn test_parse_ccs_symmetric() {
564        let cred = Credential::parse_ccs_symmetric(CRED_PSK).unwrap();
565        assert_eq!(cred.bytes.as_slice(), CRED_PSK);
566        assert_eq!(cred.key, CredentialKey::Symmetric(K.try_into().unwrap()));
567        assert_eq!(cred.kid.unwrap().as_slice(), KID_VALUE_PSK);
568        assert_eq!(cred.cred_type, CredentialType::CCS_PSK);
569    }
570}