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