Skip to main content

fido_authenticator/
credential.rs

1//! Internal `Credential` and external `CredentialId` ("keyhandle").
2
3use core::cmp::Ordering;
4
5use serde::Serialize;
6use serde_bytes::ByteArray;
7use trussed_core::{
8    mechanisms::{Chacha8Poly1305, Sha256},
9    syscall, try_syscall,
10    types::{EncryptedData, KeyId},
11    CryptoClient, FilesystemClient,
12};
13
14pub(crate) use ctap_types::{
15    // authenticator::{ctap1, ctap2, Error, Request, Response},
16    ctap2::credential_management::CredentialProtectionPolicy,
17    sizes::*,
18    webauthn::{
19        PublicKeyCredentialDescriptor, PublicKeyCredentialDescriptorRef,
20        PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
21    },
22    Bytes,
23    String,
24};
25
26use crate::{Authenticator, Error, Result, UserPresence};
27
28/// As signaled in `get_info`.
29///
30/// Eventual goal is full support for the CTAP2.1 specification.
31#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
32pub enum CtapVersion {
33    U2fV2,
34    Fido20,
35    Fido21Pre,
36}
37
38/// External ID of a credential, commonly known as "keyhandle".
39#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
40pub struct CredentialId(pub Bytes<MAX_CREDENTIAL_ID_LENGTH>);
41
42impl CredentialId {
43    fn new<T: Chacha8Poly1305, C: Serialize>(
44        trussed: &mut T,
45        credential: &C,
46        key_encryption_key: KeyId,
47        rp_id_hash: &[u8; 32],
48        nonce: &[u8; 12],
49    ) -> Result<Self> {
50        let mut serialized_credential = SerializedCredential::new();
51        cbor_smol::cbor_serialize_to(credential, &mut serialized_credential)
52            .map_err(|_| Error::Other)?;
53        let message = &serialized_credential;
54        // info!("serialized cred = {:?}", message).ok();
55        let associated_data = &rp_id_hash[..];
56        let encrypted_serialized_credential = syscall!(trussed.encrypt_chacha8poly1305(
57            key_encryption_key,
58            message,
59            associated_data,
60            Some(nonce)
61        ));
62        let mut credential_id = Bytes::new();
63        cbor_smol::cbor_serialize_to(
64            &EncryptedData::from(encrypted_serialized_credential),
65            &mut credential_id,
66        )
67        .map_err(|_| Error::RequestTooLarge)?;
68        Ok(Self(credential_id))
69    }
70}
71
72struct CredentialIdRef<'a>(&'a [u8]);
73
74impl CredentialIdRef<'_> {
75    fn deserialize(&self) -> Result<EncryptedData> {
76        cbor_smol::cbor_deserialize(self.0).map_err(|_| Error::InvalidCredential)
77    }
78}
79
80// TODO: how to determine necessary size?
81// pub type SerializedCredential = Bytes<512>;
82// pub type SerializedCredential = Bytes<256>;
83pub(crate) type SerializedCredential = trussed_core::types::Message;
84
85/// Credential keys can either be "discoverable" or not.
86///
87/// The FIDO Alliance likes to refer to "resident keys" as "(client-side) discoverable public key
88/// credential sources" now ;)
89#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
90pub enum Key {
91    ResidentKey(KeyId),
92    // THIS USED TO BE 92 NOW IT'S 96 or 97 or so... waddup?
93    WrappedKey(Bytes<128>),
94}
95
96/// A credential that is managed by the authenticator.
97///
98/// The authenticator uses two credential representations:
99/// - [`FullCredential`][] contains all data available for a credential and is used for resident
100///   credentials that are stored on the filesystem.  Older versions of this app used this
101///   reprensentation for non-resident credentials too.
102/// - [`StrippedCredential`][] contains the minimal data required for non-resident credentials.  As
103///   the data for these credentials is encoded in the credential ID, we try to keep it as small as
104///   possible.
105#[derive(Clone, Debug)]
106#[allow(clippy::large_enum_variant)]
107pub enum Credential {
108    Full(FullCredential),
109    Stripped(StrippedCredential),
110}
111
112impl Credential {
113    pub fn try_from<UP: UserPresence, T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
114        authnr: &mut Authenticator<UP, T>,
115        rp_id_hash: &[u8; 32],
116        descriptor: &PublicKeyCredentialDescriptorRef,
117    ) -> Result<Self> {
118        Self::try_from_bytes(authnr, rp_id_hash, descriptor.id)
119    }
120
121    pub fn try_from_bytes<
122        UP: UserPresence,
123        T: CryptoClient + Chacha8Poly1305 + FilesystemClient,
124    >(
125        authnr: &mut Authenticator<UP, T>,
126        rp_id_hash: &[u8; 32],
127        id: &[u8],
128    ) -> Result<Self> {
129        let encrypted_serialized = CredentialIdRef(id).deserialize()?;
130
131        let kek = authnr
132            .state
133            .persistent
134            .key_encryption_key(&mut authnr.trussed)?;
135
136        let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305(
137            kek,
138            &encrypted_serialized.ciphertext,
139            &rp_id_hash[..],
140            &encrypted_serialized.nonce,
141            &encrypted_serialized.tag,
142        ))
143        .map_err(|_| Error::InvalidCredential)?
144        .plaintext
145        .ok_or(Error::InvalidCredential)?;
146
147        // In older versions of this app, we serialized the full credential.  Now we only serialize
148        // the stripped credential.  For compatibility, we have to try both.
149        FullCredential::deserialize(&serialized)
150            .map(Self::Full)
151            .or_else(|_| StrippedCredential::deserialize(&serialized).map(Self::Stripped))
152            .map_err(|_| Error::InvalidCredential)
153    }
154
155    pub fn id<T: Chacha8Poly1305 + Sha256>(
156        &self,
157        trussed: &mut T,
158        key_encryption_key: KeyId,
159        rp_id_hash: &[u8; 32],
160    ) -> Result<CredentialId> {
161        match self {
162            Self::Full(credential) => credential.id(trussed, key_encryption_key, Some(rp_id_hash)),
163            Self::Stripped(credential) => CredentialId::new(
164                trussed,
165                credential,
166                key_encryption_key,
167                rp_id_hash,
168                &credential.nonce,
169            ),
170        }
171    }
172
173    pub fn algorithm(&self) -> i32 {
174        match self {
175            Self::Full(credential) => credential.algorithm,
176            Self::Stripped(credential) => credential.algorithm,
177        }
178    }
179
180    pub fn cred_protect(&self) -> Option<CredentialProtectionPolicy> {
181        match self {
182            Self::Full(credential) => credential.cred_protect,
183            Self::Stripped(credential) => credential.cred_protect,
184        }
185    }
186
187    pub fn key(&self) -> &Key {
188        match self {
189            Self::Full(credential) => &credential.key,
190            Self::Stripped(credential) => &credential.key,
191        }
192    }
193
194    pub fn third_party_payment(&self) -> Option<bool> {
195        match self {
196            Self::Full(credential) => credential.data.third_party_payment,
197            Self::Stripped(credential) => credential.third_party_payment,
198        }
199    }
200}
201
202fn deserialize_bytes<E: serde::de::Error, const N: usize>(
203    s: &[u8],
204) -> core::result::Result<Bytes<N>, E> {
205    Bytes::try_from(s).map_err(|_| E::invalid_length(s.len(), &"a fixed-size sequence of bytes"))
206}
207
208fn deserialize_str<E: serde::de::Error, const N: usize>(
209    s: &str,
210) -> core::result::Result<String<N>, E> {
211    s.try_into()
212        .map_err(|_| E::custom("Serialized string doesn't fit "))
213}
214
215#[derive(Clone, Copy, Debug, PartialEq)]
216pub enum SerializationFormat {
217    Short,
218    Long,
219}
220
221#[derive(Clone, Debug, PartialEq)]
222pub struct Rp {
223    format: SerializationFormat,
224    inner: PublicKeyCredentialRpEntity,
225}
226
227impl Rp {
228    fn new(inner: PublicKeyCredentialRpEntity) -> Self {
229        Self {
230            format: SerializationFormat::Short,
231            inner,
232        }
233    }
234
235    fn raw(&self) -> RawRp<'_> {
236        let mut raw = RawRp::default();
237        match self.format {
238            SerializationFormat::Short => {
239                raw.i = Some(&self.inner.id);
240                raw.n = self.inner.name.as_deref();
241            }
242            SerializationFormat::Long => {
243                raw.id = Some(&self.inner.id);
244                raw.name = self.inner.name.as_deref();
245            }
246        }
247        raw
248    }
249
250    pub fn id(&self) -> &str {
251        &self.inner.id
252    }
253}
254
255impl AsRef<PublicKeyCredentialRpEntity> for Rp {
256    fn as_ref(&self) -> &PublicKeyCredentialRpEntity {
257        &self.inner
258    }
259}
260
261impl AsMut<PublicKeyCredentialRpEntity> for Rp {
262    fn as_mut(&mut self) -> &mut PublicKeyCredentialRpEntity {
263        &mut self.inner
264    }
265}
266
267impl<'de> serde::Deserialize<'de> for Rp {
268    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
269    where
270        D: serde::Deserializer<'de>,
271    {
272        use serde::de::Error as _;
273
274        let r = RawRp::deserialize(deserializer)?;
275
276        if r.i.is_some() && r.id.is_some() {
277            return Err(D::Error::duplicate_field("i"));
278        }
279
280        let (format, id, name) = if let Some(i) = r.i {
281            if r.name.is_some() {
282                return Err(D::Error::unknown_field("name", &["i", "n"]));
283            }
284            (SerializationFormat::Short, i, r.n)
285        } else if let Some(id) = r.id {
286            if r.n.is_some() {
287                return Err(D::Error::unknown_field("n", &["id", "name"]));
288            }
289            (SerializationFormat::Long, id, r.name)
290        } else {
291            return Err(D::Error::missing_field("i"));
292        };
293
294        let inner = PublicKeyCredentialRpEntity {
295            id: deserialize_str(id)?,
296            name: name.map(deserialize_str).transpose()?,
297            icon: None,
298        };
299        Ok(Self { format, inner })
300    }
301}
302
303impl serde::Serialize for Rp {
304    fn serialize<S: serde::Serializer>(
305        &self,
306        serializer: S,
307    ) -> core::result::Result<S::Ok, S::Error> {
308        self.raw().serialize(serializer)
309    }
310}
311
312impl From<Rp> for PublicKeyCredentialRpEntity {
313    fn from(rp: Rp) -> PublicKeyCredentialRpEntity {
314        rp.inner
315    }
316}
317
318#[derive(Default, serde::Deserialize, serde::Serialize)]
319struct RawRp<'a> {
320    #[serde(skip_serializing_if = "Option::is_none")]
321    i: Option<&'a str>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    id: Option<&'a str>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    n: Option<&'a str>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    name: Option<&'a str>,
328}
329
330#[derive(Clone, Debug, PartialEq)]
331pub struct User {
332    format: SerializationFormat,
333    inner: PublicKeyCredentialUserEntity,
334}
335
336impl User {
337    fn new(inner: PublicKeyCredentialUserEntity) -> Self {
338        Self {
339            format: SerializationFormat::Short,
340            inner,
341        }
342    }
343
344    fn raw(&self) -> RawUser<'_> {
345        let mut raw = RawUser::default();
346        match self.format {
347            SerializationFormat::Short => {
348                raw.i = Some(self.inner.id.as_slice().into());
349                raw.ii = self.inner.icon.as_deref();
350                raw.n = self.inner.name.as_deref();
351                raw.d = self.inner.display_name.as_deref();
352            }
353            SerializationFormat::Long => {
354                raw.id = Some(self.inner.id.as_slice().into());
355                raw.icon = self.inner.icon.as_deref();
356                raw.name = self.inner.name.as_deref();
357                raw.display_name = self.inner.display_name.as_deref();
358            }
359        }
360        raw
361    }
362
363    pub fn id(&self) -> &Bytes<64> {
364        &self.inner.id
365    }
366}
367
368impl AsRef<PublicKeyCredentialUserEntity> for User {
369    fn as_ref(&self) -> &PublicKeyCredentialUserEntity {
370        &self.inner
371    }
372}
373
374impl AsMut<PublicKeyCredentialUserEntity> for User {
375    fn as_mut(&mut self) -> &mut PublicKeyCredentialUserEntity {
376        &mut self.inner
377    }
378}
379
380impl<'de> serde::Deserialize<'de> for User {
381    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
382    where
383        D: serde::Deserializer<'de>,
384    {
385        use serde::de::Error as _;
386
387        let u = RawUser::deserialize(deserializer)?;
388
389        if u.i.is_some() && u.id.is_some() {
390            return Err(D::Error::duplicate_field("i"));
391        }
392
393        let (format, id, icon, name, display_name) = if let Some(i) = u.i {
394            // short format
395            let fields = &["i", "I", "n", "d"];
396            if u.icon.is_some() {
397                return Err(D::Error::unknown_field("icon", fields));
398            }
399            if u.name.is_some() {
400                return Err(D::Error::unknown_field("name", fields));
401            }
402            if u.display_name.is_some() {
403                return Err(D::Error::unknown_field("display_name", fields));
404            }
405
406            (SerializationFormat::Short, i, u.ii, u.n, u.d)
407        } else if let Some(id) = u.id {
408            // long format
409            let fields = &["id", "icon", "name", "display_name"];
410            if u.ii.is_some() {
411                return Err(D::Error::unknown_field("ii", fields));
412            }
413            if u.n.is_some() {
414                return Err(D::Error::unknown_field("n", fields));
415            }
416            if u.d.is_some() {
417                return Err(D::Error::unknown_field("d", fields));
418            }
419
420            (
421                SerializationFormat::Long,
422                id,
423                u.icon,
424                u.name,
425                u.display_name,
426            )
427        } else {
428            // ID is missing
429            return Err(D::Error::missing_field("i"));
430        };
431
432        let inner = PublicKeyCredentialUserEntity {
433            id: deserialize_bytes(id)?,
434            icon: icon.map(deserialize_str).transpose()?,
435            name: name.map(deserialize_str).transpose()?,
436            display_name: display_name.map(deserialize_str).transpose()?,
437        };
438        Ok(Self { format, inner })
439    }
440}
441
442impl serde::Serialize for User {
443    fn serialize<S: serde::Serializer>(
444        &self,
445        serializer: S,
446    ) -> core::result::Result<S::Ok, S::Error> {
447        self.raw().serialize(serializer)
448    }
449}
450
451impl From<User> for PublicKeyCredentialUserEntity {
452    fn from(user: User) -> PublicKeyCredentialUserEntity {
453        user.inner
454    }
455}
456
457#[derive(Default, serde::Deserialize, serde::Serialize)]
458struct RawUser<'a> {
459    #[serde(skip_serializing_if = "Option::is_none")]
460    i: Option<&'a serde_bytes::Bytes>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    id: Option<&'a serde_bytes::Bytes>,
463    #[serde(skip_serializing_if = "Option::is_none", rename = "I")]
464    ii: Option<&'a str>,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    icon: Option<&'a str>,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    n: Option<&'a str>,
469    #[serde(skip_serializing_if = "Option::is_none")]
470    name: Option<&'a str>,
471    #[serde(skip_serializing_if = "Option::is_none")]
472    d: Option<&'a str>,
473    #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")]
474    display_name: Option<&'a str>,
475}
476
477/// The main content of a `FullCredential`.
478#[derive(
479    Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed,
480)]
481pub struct CredentialData {
482    // id, name, url
483    pub rp: Rp,
484    // id, icon, name, display_name
485    pub user: User,
486
487    // can be just a counter, need to be able to determine "latest"
488    pub creation_time: u32,
489    // for stateless deterministic keys, it seems CTAP2 (but not CTAP1) makes signature counters optional
490    use_counter: bool,
491    // P256 or Ed25519
492    pub algorithm: i32,
493    // for RK in non-deterministic mode: refers to actual key
494    // TODO(implement enums in cbor-deser): for all others, is a wrapped key
495    // --> use above Key enum
496    // #[serde(skip_serializing_if = "Option::is_none")]
497    // key_id: Option<KeyId>,
498    pub key: Key,
499
500    // extensions
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub hmac_secret: Option<bool>,
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub cred_protect: Option<CredentialProtectionPolicy>,
505    // TODO: add `sig_counter: Option<CounterId>`,
506    // and grant RKs a per-credential sig-counter.
507
508    // In older app versions, we serialized the full credential to determine the credential ID.  In
509    // newer app versions, we strip unnecessary fields to generate a shorter credential ID.  To
510    // make sure that the credential ID does not change for an existing credential, this field is
511    // used as a marker for new credentials.
512    #[serde(skip_serializing_if = "Option::is_none")]
513    use_short_id: Option<bool>,
514
515    // extensions (cont. -- we can only append new options due to index-based deserialization)
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub large_blob_key: Option<ByteArray<32>>,
518
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub third_party_payment: Option<bool>,
521}
522
523// TODO: figure out sizes
524// We may or may not follow https://github.com/satoshilabs/slips/blob/master/slip-0022.md
525/// The core structure this authenticator creates and uses.
526#[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)]
527pub struct FullCredential {
528    ctap: CtapVersion,
529    pub data: CredentialData,
530    nonce: ByteArray<12>,
531}
532
533// Alas... it would be more symmetrical to have Credential { meta, data },
534// but let's not break binary compatibility for this.
535//
536// struct Metadata {
537//     ctap: CtapVersion,
538//     nonce: Bytes<12>,
539// }
540
541impl core::ops::Deref for FullCredential {
542    type Target = CredentialData;
543
544    fn deref(&self) -> &Self::Target {
545        &self.data
546    }
547}
548
549/// Compare credentials based on key + timestamp.
550///
551/// Likely comparison based on timestamp would be good enough?
552impl PartialEq for FullCredential {
553    fn eq(&self, other: &Self) -> bool {
554        (self.creation_time == other.creation_time) && (self.key == other.key)
555    }
556}
557
558impl PartialEq<&FullCredential> for FullCredential {
559    fn eq(&self, other: &&Self) -> bool {
560        self == *other
561    }
562}
563
564impl Eq for FullCredential {}
565
566impl Ord for FullCredential {
567    fn cmp(&self, other: &Self) -> Ordering {
568        self.data.creation_time.cmp(&other.data.creation_time)
569    }
570}
571
572/// Order by timestamp of creation.
573impl PartialOrd for FullCredential {
574    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
575        Some(self.cmp(other))
576    }
577}
578
579impl PartialOrd<&FullCredential> for FullCredential {
580    fn partial_cmp(&self, other: &&Self) -> Option<Ordering> {
581        Some(self.cmp(*other))
582    }
583}
584
585// Bad idea - huge stack
586// pub(crate) type CredentialList = Vec<Credential, {ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST}>;
587
588impl From<CredentialId> for PublicKeyCredentialDescriptor {
589    fn from(id: CredentialId) -> PublicKeyCredentialDescriptor {
590        PublicKeyCredentialDescriptor {
591            id: id.0,
592            key_type: {
593                let mut key_type = String::new();
594                key_type.push_str("public-key").unwrap();
595                key_type
596            },
597        }
598    }
599}
600
601impl FullCredential {
602    #[allow(clippy::too_many_arguments)]
603    pub fn new(
604        ctap: CtapVersion,
605        // parameters: &ctap2::make_credential::Parameters,
606        rp: &ctap_types::webauthn::PublicKeyCredentialRpEntity,
607        user: &ctap_types::webauthn::PublicKeyCredentialUserEntity,
608        algorithm: i32,
609        key: Key,
610        timestamp: u32,
611        hmac_secret: Option<bool>,
612        cred_protect: Option<CredentialProtectionPolicy>,
613        large_blob_key: Option<ByteArray<32>>,
614        third_party_payment: Option<bool>,
615        nonce: [u8; 12],
616    ) -> Self {
617        info!("credential for algorithm {}", algorithm);
618        let data = CredentialData {
619            rp: Rp::new(rp.clone()),
620            user: User::new(user.clone()),
621
622            creation_time: timestamp,
623            use_counter: true,
624            algorithm,
625            key,
626
627            hmac_secret,
628            cred_protect,
629            large_blob_key,
630            third_party_payment,
631
632            use_short_id: Some(true),
633        };
634
635        FullCredential {
636            ctap,
637            data,
638            nonce: ByteArray::new(nonce),
639        }
640    }
641
642    // ID (or "keyhandle") for the credential.
643    //
644    // Originally, the entire data was serialized, and its encryption
645    // (binding RP as associated data) used as a keyhandle.
646    //
647    // However, this leads to problems with relying parties. According to the old U2F
648    // spec, the length of a keyhandle is encoded as one byte, whereas this procedure would
649    // generate keyhandles of length ~320 bytes.
650    //
651    // Therefore, inessential metadata is stripped before serialization, ensuring
652    // the ID will stay below 255 bytes.
653    //
654    // Existing keyhandles can still be decoded
655    pub fn id<T: Chacha8Poly1305 + Sha256>(
656        &self,
657        trussed: &mut T,
658        key_encryption_key: KeyId,
659        rp_id_hash: Option<&[u8; 32]>,
660    ) -> Result<CredentialId> {
661        let rp_id_hash: [u8; 32] = if let Some(hash) = rp_id_hash {
662            *hash
663        } else {
664            syscall!(trussed.hash_sha256(self.rp.id().as_ref()))
665                .hash
666                .as_slice()
667                .try_into()
668                .map_err(|_| Error::Other)?
669        };
670        if self.use_short_id.unwrap_or_default() {
671            StrippedCredential::from(self).id(trussed, key_encryption_key, &rp_id_hash)
672        } else {
673            let stripped_credential = self.strip();
674            CredentialId::new(
675                trussed,
676                &stripped_credential,
677                key_encryption_key,
678                &rp_id_hash,
679                &self.nonce,
680            )
681        }
682    }
683
684    pub fn serialize(&self) -> Result<SerializedCredential> {
685        let mut serialized_credential = SerializedCredential::new();
686        cbor_smol::cbor_serialize_to(self, &mut serialized_credential).map_err(|_| Error::Other)?;
687        Ok(serialized_credential)
688    }
689
690    pub fn deserialize(bytes: &SerializedCredential) -> Result<Self> {
691        match cbor_smol::cbor_deserialize(bytes) {
692            Ok(s) => Ok(s),
693            Err(_) => {
694                info_now!("could not deserialize {:?}", bytes);
695                Err(Error::Other)
696            }
697        }
698    }
699
700    // This method is only kept for compatibility.  To strip new credentials, use
701    // `StrippedCredential`.
702    #[must_use]
703    fn strip(&self) -> Self {
704        info_now!(":: stripping ID");
705        let mut stripped = self.clone();
706        let rp = stripped.data.rp.as_mut();
707        rp.name = None;
708        let user = stripped.data.user.as_mut();
709        user.icon = None;
710        user.name = None;
711        user.display_name = None;
712        stripped
713    }
714}
715
716/// A reduced version of `FullCredential` that is used for non-resident credentials.
717///
718/// As the credential data is encodeded in the credential ID, we only want to include necessary
719/// data to keep the credential ID as short as possible.
720#[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)]
721pub struct StrippedCredential {
722    pub ctap: CtapVersion,
723    pub creation_time: u32,
724    pub use_counter: bool,
725    pub algorithm: i32,
726    pub key: Key,
727    pub nonce: ByteArray<12>,
728    // extensions
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub hmac_secret: Option<bool>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub cred_protect: Option<CredentialProtectionPolicy>,
733    // TODO: HACK -- remove
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub large_blob_key: Option<ByteArray<32>>,
736    #[serde(skip_serializing_if = "Option::is_none")]
737    pub third_party_payment: Option<bool>,
738}
739
740impl StrippedCredential {
741    fn deserialize(bytes: &SerializedCredential) -> Result<Self> {
742        match cbor_smol::cbor_deserialize(bytes) {
743            Ok(s) => Ok(s),
744            Err(_) => {
745                info_now!("could not deserialize {:?}", bytes);
746                Err(Error::Other)
747            }
748        }
749    }
750
751    pub fn id<T: Chacha8Poly1305>(
752        &self,
753        trussed: &mut T,
754        key_encryption_key: KeyId,
755        rp_id_hash: &[u8; 32],
756    ) -> Result<CredentialId> {
757        CredentialId::new(trussed, self, key_encryption_key, rp_id_hash, &self.nonce)
758    }
759}
760
761impl From<&FullCredential> for StrippedCredential {
762    fn from(credential: &FullCredential) -> Self {
763        Self {
764            ctap: credential.ctap,
765            creation_time: credential.data.creation_time,
766            use_counter: credential.data.use_counter,
767            algorithm: credential.data.algorithm,
768            key: credential.data.key.clone(),
769            nonce: credential.nonce,
770            hmac_secret: credential.data.hmac_secret,
771            cred_protect: credential.data.cred_protect,
772            large_blob_key: credential.data.large_blob_key,
773            third_party_payment: credential.data.third_party_payment,
774        }
775    }
776}
777
778#[cfg(test)]
779mod test {
780    use super::*;
781    use hex_literal::hex;
782    use littlefs2_core::path;
783    use rand::SeedableRng as _;
784    use rand_chacha::ChaCha8Rng;
785    use serde_test::{assert_de_tokens, assert_tokens, Token};
786    use trussed::{
787        client::{Chacha8Poly1305, Sha256},
788        key::{Kind, Secrecy},
789        store::keystore::{ClientKeystore, Keystore as _},
790        types::Location,
791        virt::{self, StoreConfig},
792        Platform as _,
793    };
794
795    fn credential_data() -> CredentialData {
796        CredentialData {
797            rp: Rp::new(PublicKeyCredentialRpEntity {
798                id: String::try_from("John Doe").unwrap(),
799                name: None,
800                icon: None,
801            }),
802            user: User::new(PublicKeyCredentialUserEntity {
803                id: Bytes::from(&[1, 2, 3]),
804                icon: None,
805                name: None,
806                display_name: None,
807            }),
808            creation_time: 123,
809            use_counter: false,
810            algorithm: -7,
811            key: Key::WrappedKey(Bytes::from(&[1, 2, 3])),
812            hmac_secret: Some(false),
813            cred_protect: None,
814            use_short_id: Some(true),
815            large_blob_key: Some(ByteArray::new([0xff; 32])),
816            third_party_payment: Some(true),
817        }
818    }
819
820    fn old_credential_data() -> CredentialData {
821        CredentialData {
822            rp: Rp {
823                format: SerializationFormat::Long,
824                inner: PublicKeyCredentialRpEntity {
825                    id: String::try_from("John Doe").unwrap(),
826                    name: None,
827                    icon: None,
828                },
829            },
830            user: User {
831                format: SerializationFormat::Long,
832                inner: PublicKeyCredentialUserEntity {
833                    id: Bytes::from(&[1, 2, 3]),
834                    icon: None,
835                    name: None,
836                    display_name: None,
837                },
838            },
839            creation_time: 123,
840            use_counter: false,
841            algorithm: -7,
842            key: Key::WrappedKey(Bytes::from(&[1, 2, 3])),
843            hmac_secret: Some(false),
844            cred_protect: None,
845            use_short_id: None,
846            large_blob_key: None,
847            third_party_payment: None,
848        }
849    }
850
851    fn random_byte_array<const N: usize>() -> ByteArray<N> {
852        use rand::{rngs::OsRng, RngCore};
853        let mut bytes = [0; N];
854        OsRng.fill_bytes(&mut bytes);
855        ByteArray::new(bytes)
856    }
857
858    fn random_bytes<const N: usize>() -> Bytes<N> {
859        use rand::{
860            distributions::{Distribution, Uniform},
861            rngs::OsRng,
862            RngCore,
863        };
864        let mut bytes = Bytes::default();
865
866        let between = Uniform::from(0..(N + 1));
867        let n = between.sample(&mut OsRng);
868
869        bytes.resize_zero(n).unwrap();
870
871        OsRng.fill_bytes(&mut bytes);
872        bytes
873    }
874
875    #[allow(dead_code)]
876    fn maybe_random_bytes<const N: usize>() -> Option<Bytes<N>> {
877        use rand::{rngs::OsRng, RngCore};
878        if OsRng.next_u32() & 1 != 0 {
879            Some(random_bytes())
880        } else {
881            None
882        }
883    }
884
885    fn random_string<const N: usize>() -> String<N> {
886        use rand::{
887            distributions::{Alphanumeric, Distribution, Uniform},
888            rngs::OsRng,
889            Rng,
890        };
891        use std::str::FromStr;
892
893        let between = Uniform::from(0..(N + 1));
894        let n = between.sample(&mut OsRng);
895
896        let std_string: std::string::String = OsRng
897            .sample_iter(&Alphanumeric)
898            .take(n)
899            .map(char::from)
900            .collect();
901        String::from_str(&std_string).unwrap()
902    }
903
904    fn maybe_random_string<const N: usize>() -> Option<String<N>> {
905        use rand::{rngs::OsRng, RngCore};
906        if OsRng.next_u32() & 1 != 0 {
907            Some(random_string())
908        } else {
909            None
910        }
911    }
912
913    fn random_credential_data() -> CredentialData {
914        CredentialData {
915            rp: Rp::new(PublicKeyCredentialRpEntity {
916                id: random_string(),
917                name: maybe_random_string(),
918                icon: None,
919            }),
920            user: User::new(PublicKeyCredentialUserEntity {
921                id: random_bytes(), //Bytes::from_slice(&[1,2,3]).unwrap(),
922                icon: maybe_random_string(),
923                name: maybe_random_string(),
924                display_name: maybe_random_string(),
925            }),
926            creation_time: 123,
927            use_counter: false,
928            algorithm: -7,
929            key: Key::WrappedKey(random_bytes()),
930            hmac_secret: Some(false),
931            cred_protect: None,
932            use_short_id: Some(true),
933            large_blob_key: Some(random_byte_array()),
934            third_party_payment: Some(false),
935        }
936    }
937
938    #[test]
939    fn skip_credential_data_options() {
940        use trussed::{cbor_deserialize as deserialize, cbor_serialize_bytes as serialize};
941
942        let credential_data = credential_data();
943        let serialization: Bytes<1024> = serialize(&credential_data).unwrap();
944        let deserialized: CredentialData = deserialize(&serialization).unwrap();
945
946        assert_eq!(credential_data, deserialized);
947
948        let credential_data = random_credential_data();
949        let serialization: Bytes<1024> = serialize(&credential_data).unwrap();
950        let deserialized: CredentialData = deserialize(&serialization).unwrap();
951
952        assert_eq!(credential_data, deserialized);
953    }
954
955    #[test]
956    fn old_credential_id() {
957        // generated with v0.1.1-nitrokey.4 (NK3 firmware version v1.4.0)
958        const OLD_ID: &[u8] = &hex!("A300583A71AEF80C4DA56033D66EB3266E9ACB8D84923D13F89BCBCE9FF30D8CD77ED968A436CA3D39C49999EC0F69A289CB2A65A08ABF251DEB21BB4B56014C00000000000000000000000002504DF499ABDAE80F5615C870985B74A799");
959        const SERIALIZED_DATA: &[u8] = &hex!(
960            "A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4"
961        );
962        const SERIALIZED_CREDENTIAL: &[u8] = &hex!("A3000201A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4024C000000000000000000000000");
963
964        virt::with_platform(StoreConfig::ram(), |mut platform| {
965            let kek = [0; 44];
966            let client_id = path!("fido");
967            let kek = {
968                let rng = ChaCha8Rng::from_rng(platform.rng()).unwrap();
969                let mut keystore = ClientKeystore::new(client_id.into(), rng, platform.store());
970                keystore
971                    .store_key(
972                        Location::Internal,
973                        Secrecy::Secret,
974                        Kind::Symmetric32Nonce(12),
975                        &kek,
976                    )
977                    .unwrap()
978            };
979            platform.run_client(client_id.as_str(), |mut client| {
980                let data = old_credential_data();
981                let rp_id_hash = syscall!(client.hash_sha256(data.rp.id().as_ref())).hash;
982                let encrypted_serialized = CredentialIdRef(OLD_ID).deserialize().unwrap();
983                let serialized = syscall!(client.decrypt_chacha8poly1305(
984                    kek,
985                    &encrypted_serialized.ciphertext,
986                    &rp_id_hash,
987                    &encrypted_serialized.nonce,
988                    &encrypted_serialized.tag,
989                ))
990                .plaintext
991                .unwrap();
992
993                let full = FullCredential::deserialize(&serialized).unwrap();
994                assert_eq!(
995                    full,
996                    FullCredential {
997                        ctap: CtapVersion::Fido21Pre,
998                        data,
999                        nonce: [0; 12].into(),
1000                    }
1001                );
1002
1003                let stripped_credential = full.strip();
1004
1005                let serialized_data: Bytes<1024> =
1006                    trussed::cbor_serialize_bytes(&stripped_credential.data).unwrap();
1007                assert_eq!(
1008                    delog::hexstr!(&serialized_data).to_string(),
1009                    delog::hexstr!(SERIALIZED_DATA).to_string()
1010                );
1011
1012                let serialized_credential: Bytes<1024> =
1013                    trussed::cbor_serialize_bytes(&stripped_credential).unwrap();
1014                assert_eq!(
1015                    delog::hexstr!(&serialized_credential).to_string(),
1016                    delog::hexstr!(SERIALIZED_CREDENTIAL).to_string()
1017                );
1018
1019                let credential = Credential::Full(full);
1020                let id = credential
1021                    .id(&mut client, kek, rp_id_hash.as_ref().try_into().unwrap())
1022                    .unwrap()
1023                    .0;
1024                assert_eq!(
1025                    delog::hexstr!(&id).to_string(),
1026                    delog::hexstr!(OLD_ID).to_string()
1027                );
1028            });
1029        });
1030    }
1031
1032    #[test]
1033    fn credential_ids() {
1034        trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| {
1035            let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key;
1036            let nonce = ByteArray::new([0; 12]);
1037            let data = credential_data();
1038            let mut full_credential = FullCredential {
1039                ctap: CtapVersion::Fido21Pre,
1040                data,
1041                nonce,
1042            };
1043            let rp_id_hash = syscall!(client.hash_sha256(full_credential.rp.id().as_ref()))
1044                .hash
1045                .as_slice()
1046                .try_into()
1047                .unwrap();
1048
1049            // Case 1: credential with use_short_id = Some(true) uses new (short) format
1050            full_credential.data.use_short_id = Some(true);
1051            let stripped_credential = StrippedCredential::from(&full_credential);
1052            let full_id = full_credential
1053                .id(&mut client, kek, Some(&rp_id_hash))
1054                .unwrap();
1055            let short_id = stripped_credential
1056                .id(&mut client, kek, &rp_id_hash)
1057                .unwrap();
1058            assert_eq!(full_id.0, short_id.0);
1059
1060            // Case 2: credential with use_short_id = None uses old (long) format
1061            full_credential.data.use_short_id = None;
1062            let stripped_credential = full_credential.strip();
1063            let full_id = full_credential
1064                .id(&mut client, kek, Some(&rp_id_hash))
1065                .unwrap();
1066            let long_id = CredentialId::new(
1067                &mut client,
1068                &stripped_credential,
1069                kek,
1070                &rp_id_hash,
1071                &full_credential.nonce,
1072            )
1073            .unwrap();
1074            assert_eq!(full_id.0, long_id.0);
1075
1076            assert!(short_id.0.len() < long_id.0.len());
1077        });
1078    }
1079
1080    #[test]
1081    fn max_credential_id() {
1082        let rp_id: String<256> = core::iter::repeat_n('?', 256).collect();
1083        let key = Bytes::from(&[u8::MAX; 128]);
1084        let credential = StrippedCredential {
1085            ctap: CtapVersion::Fido21Pre,
1086            creation_time: u32::MAX,
1087            use_counter: true,
1088            algorithm: i32::MAX,
1089            key: Key::WrappedKey(key),
1090            nonce: ByteArray::new([u8::MAX; 12]),
1091            hmac_secret: Some(true),
1092            cred_protect: Some(CredentialProtectionPolicy::Required),
1093            large_blob_key: Some(ByteArray::new([0xff; 32])),
1094            third_party_payment: Some(true),
1095        };
1096        trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| {
1097            let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key;
1098            let rp_id_hash = syscall!(client.hash_sha256(rp_id.as_ref()))
1099                .hash
1100                .as_slice()
1101                .try_into()
1102                .unwrap();
1103            let id = credential.id(&mut client, kek, &rp_id_hash).unwrap();
1104            assert_eq!(id.0.len(), 241);
1105        });
1106    }
1107
1108    fn test_serde<T>(item: &T, name: &'static str, fields: &[(&'static str, Token)])
1109    where
1110        for<'a> T: core::fmt::Debug + PartialEq + serde::Deserialize<'a> + serde::Serialize,
1111    {
1112        let len = fields.len();
1113
1114        let mut struct_tokens = vec![Token::Struct { name, len }];
1115        let mut map_tokens = vec![Token::Map { len: Some(len) }];
1116        for (key, value) in fields {
1117            struct_tokens.push(Token::Str(key));
1118            struct_tokens.push(Token::Some);
1119            struct_tokens.push(*value);
1120
1121            map_tokens.push(Token::Str(key));
1122            map_tokens.push(Token::Some);
1123            map_tokens.push(*value);
1124        }
1125        struct_tokens.push(Token::StructEnd);
1126        map_tokens.push(Token::MapEnd);
1127
1128        assert_tokens(item, &struct_tokens);
1129        assert_de_tokens(item, &map_tokens);
1130    }
1131
1132    struct RpValues {
1133        id: &'static str,
1134        name: Option<&'static str>,
1135    }
1136
1137    impl RpValues {
1138        fn test(&self) {
1139            for format in [SerializationFormat::Short, SerializationFormat::Long] {
1140                self.test_format(format);
1141            }
1142        }
1143
1144        fn test_format(&self, format: SerializationFormat) {
1145            let (id_field, name_field) = match format {
1146                SerializationFormat::Short => ("i", "n"),
1147                SerializationFormat::Long => ("id", "name"),
1148            };
1149            let rp = Rp {
1150                format,
1151                inner: self.inner(),
1152            };
1153
1154            let mut fields = vec![(id_field, Token::BorrowedStr(self.id))];
1155            if let Some(name) = self.name {
1156                fields.push((name_field, Token::BorrowedStr(name)));
1157            }
1158
1159            test_serde(&rp, "RawRp", &fields);
1160        }
1161
1162        fn inner(&self) -> PublicKeyCredentialRpEntity {
1163            PublicKeyCredentialRpEntity {
1164                id: self.id.try_into().unwrap(),
1165                name: self.name.map(|n| n.try_into().unwrap()),
1166                icon: None,
1167            }
1168        }
1169    }
1170
1171    #[test]
1172    fn serde_rp_name_none() {
1173        RpValues {
1174            id: "Testing rp id",
1175            name: None,
1176        }
1177        .test()
1178    }
1179
1180    #[test]
1181    fn serde_rp_name_some() {
1182        RpValues {
1183            id: "Testing rp id",
1184            name: Some("Testing rp name"),
1185        }
1186        .test()
1187    }
1188
1189    struct UserValues {
1190        id: &'static [u8],
1191        icon: Option<&'static str>,
1192        name: Option<&'static str>,
1193        display_name: Option<&'static str>,
1194    }
1195
1196    impl UserValues {
1197        fn test(&self) {
1198            for format in [SerializationFormat::Short, SerializationFormat::Long] {
1199                self.test_format(format);
1200            }
1201        }
1202
1203        fn test_format(&self, format: SerializationFormat) {
1204            let (id_field, icon_field, name_field, display_name_field) = match format {
1205                SerializationFormat::Short => ("i", "I", "n", "d"),
1206                SerializationFormat::Long => ("id", "icon", "name", "displayName"),
1207            };
1208            let user = User {
1209                format,
1210                inner: self.inner(),
1211            };
1212
1213            let mut fields = vec![(id_field, Token::BorrowedBytes(self.id))];
1214            if let Some(icon) = self.icon {
1215                fields.push((icon_field, Token::BorrowedStr(icon)));
1216            }
1217            if let Some(name) = self.name {
1218                fields.push((name_field, Token::BorrowedStr(name)));
1219            }
1220            if let Some(display_name) = self.display_name {
1221                fields.push((display_name_field, Token::BorrowedStr(display_name)));
1222            }
1223
1224            test_serde(&user, "RawUser", &fields);
1225        }
1226
1227        fn inner(&self) -> PublicKeyCredentialUserEntity {
1228            PublicKeyCredentialUserEntity {
1229                id: Bytes::try_from(self.id).unwrap(),
1230                icon: self.icon.map(|v| v.try_into().unwrap()),
1231                name: self.name.map(|v| v.try_into().unwrap()),
1232                display_name: self.display_name.map(|v| v.try_into().unwrap()),
1233            }
1234        }
1235    }
1236
1237    #[test]
1238    fn serde_user_full() {
1239        UserValues {
1240            id: b"Testing user id",
1241            icon: Some("Testing user icon"),
1242            name: Some("Testing user name"),
1243            display_name: Some("Testing user display_name"),
1244        }
1245        .test();
1246    }
1247
1248    #[test]
1249    fn serde_user_display_name() {
1250        UserValues {
1251            id: b"Testing user id",
1252            icon: None,
1253            name: None,
1254            display_name: Some("Testing user display_name"),
1255        }
1256        .test();
1257    }
1258
1259    #[test]
1260    fn serde_user_icon_display_name() {
1261        UserValues {
1262            id: b"Testing user id",
1263            icon: Some("Testing user icon"),
1264            name: None,
1265            display_name: Some("Testing user display_name"),
1266        }
1267        .test();
1268    }
1269
1270    #[test]
1271    fn serde_user_icon() {
1272        UserValues {
1273            id: b"Testing user id",
1274            icon: Some("Testing user icon"),
1275            name: None,
1276            display_name: None,
1277        }
1278        .test();
1279    }
1280
1281    #[test]
1282    fn serde_user_empty() {
1283        UserValues {
1284            id: b"Testing user id",
1285            icon: None,
1286            name: None,
1287            display_name: None,
1288        }
1289        .test();
1290    }
1291
1292    // Test credentials that were serialized before the migration to shorter field names for serialization
1293    #[test]
1294    fn legacy_full_credential() {
1295        use hex_literal::hex;
1296        let data = hex!(
1297            "
1298            a3000201a700a16269646b776562617574686e2e696f01a2626964476447
1299            567a644445646e616d65657465737431020003f504260582005037635754
1300            c9882b21565a9f8a47b0ece408f5024cf62ca01ed181a3d03d561fc7
1301        "
1302        );
1303
1304        let credential = FullCredential::deserialize(&Bytes::from(&data)).unwrap();
1305        assert!(matches!(credential.ctap, CtapVersion::Fido21Pre));
1306        assert_eq!(credential.nonce, &hex!("F62CA01ED181A3D03D561FC7"));
1307        assert_eq!(
1308            credential.data,
1309            CredentialData {
1310                rp: Rp {
1311                    format: SerializationFormat::Long,
1312                    inner: PublicKeyCredentialRpEntity {
1313                        id: "webauthn.io".try_into().unwrap(),
1314                        name: None,
1315                        icon: None,
1316                    },
1317                },
1318                user: User {
1319                    format: SerializationFormat::Long,
1320                    inner: PublicKeyCredentialUserEntity {
1321                        id: Bytes::from(&hex!("6447567A644445")),
1322                        icon: None,
1323                        name: Some("test1".try_into().unwrap()),
1324                        display_name: None,
1325                    },
1326                },
1327                creation_time: 0,
1328                use_counter: true,
1329                algorithm: -7,
1330                key: Key::ResidentKey(KeyId::from_value(0x37635754C9882B21565A9F8A47B0ECE4)),
1331                hmac_secret: None,
1332                cred_protect: None,
1333                use_short_id: Some(true),
1334                large_blob_key: None,
1335                third_party_payment: None,
1336            },
1337        );
1338    }
1339
1340    // use quickcheck::TestResult;
1341    // quickcheck::quickcheck! {
1342    //   fn prop(
1343    //       rp_id: std::string::String,
1344    //       rp_name: Option<std::string::String>,
1345    //       rp_url: Option<std::string::String>,
1346    //       user_id: std::vec::Vec<u8>,
1347    //       user_name: Option<std::string::String>,
1348    //       creation_time: u32,
1349    //       use_counter: bool,
1350    //       algorithm: i32
1351    //     ) -> TestResult {
1352    //     use std::str::FromStr;
1353    //     use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity};
1354    //     use trussed::{cbor_deserialize as deserialize, cbor_serialize_bytes as serialize};
1355
1356    //     let rp_name = &rp_name.as_ref().map(|string| string.as_str());
1357    //     let rp_url = &rp_url.as_ref().map(|string| string.as_str());
1358    //     let user_name = &user_name.as_ref().map(|string| string.as_str());
1359    //     let discard = [
1360    //         rp_id.len() > 256,
1361    //         rp_name.unwrap_or(&"").len() > 64,
1362    //         rp_url.unwrap_or(&"").len() > 64,
1363    //         user_id.len() > 64,
1364    //         user_name.unwrap_or(&"").len() > 64,
1365
1366    //     ];
1367    //     if discard.iter().any(|&x| x) {
1368    //         return TestResult::discard();
1369    //     }
1370
1371    //     let credential_data = CredentialData {
1372    //         rp: PublicKeyCredentialRpEntity {
1373    //             id: String::from_str(&rp_id).unwrap(),
1374    //             name: rp_name.map(|rp_name| String::from_str(rp_name).unwrap()),
1375    //             url: rp_url.map(|rp_url| String::from_str(rp_url).unwrap()),
1376    //         },
1377    //         user: PublicKeyCredentialUserEntity {
1378    //             id: Bytes::from_slice(&user_id).unwrap(),
1379    //             icon: maybe_random_string(),
1380    //             name: user_name.map(|user_name| String::from_str(user_name).unwrap()),
1381    //             display_name: maybe_random_string(),
1382    //         },
1383    //         creation_time,
1384    //         use_counter,
1385    //         algorithm,
1386    //         key: Key::WrappedKey(random_bytes()),
1387    //         hmac_secret: Some(false),
1388    //         cred_protect: None,
1389    //     };
1390
1391    //     let serialization: Bytes<1024> = serialize(&credential_data).unwrap();
1392    //     let deserialized: CredentialData = deserialize(&serialization).unwrap();
1393
1394    //     TestResult::from_bool(credential_data == deserialized)
1395    // }
1396    // }
1397}