passkey_types/
passkey.rs

1use std::fmt::Debug;
2
3use super::u2f::{AuthenticationRequest, RegisterRequest, RegisterResponse};
4use crate::{Bytes, ctap2::make_credential as ctap2, webauthn};
5use coset::CoseKey;
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8#[cfg(feature = "testable")]
9mod mock;
10
11#[cfg(feature = "testable")]
12pub use self::mock::PasskeyBuilder;
13
14/// The private WebAuthn credential containing all relevant required and optional information for an
15/// authentication ceremony.
16///
17/// The WebAuthn term for this is a [Public Key Credential Source][cred-src].
18///
19/// # Personally Identifying Information (PII) considerations
20/// While this struct implements [`Debug`], it only prints the following fields:
21/// * [`CoseKey::kty`] enum from the [`Self::key`] field,
22/// * [`Self::counter`] which is the number of times this was used to authenticate.
23///
24/// The rest of this struct should be considered secret, either for cryptographic security, or because
25/// its value could be used as PII.
26///
27/// [cred-src]: https://w3c.github.io/webauthn/#public-key-credential-source
28// TODO: Implement Zeroize on this if/when rolling our own CoseKey type
29// TODO: use `#[non_exhaustive]` here with a builder pattern for building new passkeys
30#[derive(Clone)]
31#[cfg_attr(any(test, feature = "testable"), derive(PartialEq))]
32pub struct Passkey {
33    /// The private key in COSE key format.
34    ///
35    /// # PII considerations
36    /// This value should be considered secret and never printed out as it is a secret cryptographic
37    /// key. The only thing that get printed in the `Debug` implementation is the key type,
38    /// e.g: EC2, RSA, etc.
39    pub key: CoseKey,
40
41    /// A probabilistically-unique byte sequence identifying this [`Passkey`]. It must be at most 1023
42    /// bytes long.
43    ///
44    /// Credential IDs are generated by authenticators in two forms:
45    /// 1. At least 16 bytes that include at least 100 bits of entropy, or
46    /// 2. The [`Passkey`] item, without its `credential_id`, encrypted so only its managing
47    ///    authenticator can decrypt it. This form allows the authenticator to be nearly stateless, by
48    ///    having the Relying Party store any necessary state.
49    ///
50    /// Relying Parties do not need to distinguish these two `credential id` forms.
51    ///
52    ///
53    /// # PII considerations
54    /// This value should be considered secret as it is the user's credential ID for the associated
55    /// Relying Party. See [Privacy leak via credential IDs][privacy] for more information.
56    ///
57    /// [privacy]: https://w3c.github.io/webauthn/#sctn-credential-id-privacy-leak
58    pub credential_id: Bytes,
59
60    /// The [Relying Party ID][RP_ID] for which the [`Passkey`] is associated. This value mirrors the
61    /// [`webauthn::PublicKeyCredentialRpEntity::id`] value passed during the creation of this credential.
62    ///
63    /// # PII considerations
64    /// This should be handled similarly to a URL. Since this is a user credential for a Relying
65    /// Party, this would leak the fact that a user has an account for this particular Relying Party.
66    ///
67    /// [RP_ID]: https://w3c.github.io/webauthn/#relying-party-identifier
68    pub rp_id: String,
69
70    /// This is the [`webauthn::PublicKeyCredentialUserEntity::id`] passed in during the creation of
71    /// this credential. An Authenticator can choose to store this or not. If it stores this value,
72    /// this [`Passkey`] will become a [Discoverable Credential] and will be returned during authentication
73    /// Ceremonies.
74    ///
75    /// # PII Considerations
76    /// This is the identifier a Relying party uses on their side to personally identify a user. This
77    /// value is analogous to a username.
78    ///
79    /// [Discoverable Credential]: https://w3c.github.io/webauthn/#client-side-discoverable-credential
80    pub user_handle: Option<Bytes>,
81
82    /// This is the [`webauthn::PublicKeyCredentialUserEntity::name`] passed in during the creation
83    /// of this credential. An authenticator can choose to store this or not.
84    ///
85    /// # PII Considerations
86    /// This is the username which is a human readable personal identifier. While it does not get
87    /// over the wire to the Relying Party it may be used in displays.
88    pub username: Option<String>,
89
90    /// This is the [`webauthn::PublicKeyCredentialUserEntity::display_name`] passed in during the creation
91    /// of this credential. An authenticator can choose to store this or not.
92    ///
93    /// # PII Considerations
94    /// This is the human-readable name for a user account. While it does not get sent
95    /// over the wire to the Relying Party it may be used in displays.
96    pub user_display_name: Option<String>,
97
98    /// Value tracks the number of times an authentication ceremony has been successfully completed.
99    /// If the value is `None` then it will be sent as the constant `0`.
100    /// See [Signature counter considerations][signCount] for more information.
101    ///
102    /// # PII considerations
103    /// This value, if populated, is used by the Relying Party as an indicator of a cloned
104    /// authenticator. If this [`Passkey`] is to be synced, consider leaving this value empty unless
105    /// you can guarantee the value to always be increased for every use of this passkey across its
106    /// distribution.
107    ///
108    /// [signCount]: https://w3c.github.io/webauthn/#signature-counter
109    pub counter: Option<u32>,
110
111    /// Authenticator extensions that need data associated to passkey secrets
112    pub extensions: CredentialExtensions,
113}
114
115impl Passkey {
116    /// Standardized way to "upgrade" a U2F register request into a passkey
117    pub fn from_u2f_register_response(
118        request: &RegisterRequest,
119        response: &RegisterResponse,
120        private_key: &CoseKey,
121    ) -> Self {
122        let app_id: Bytes = request.application.to_vec().into();
123        Self {
124            key: private_key.clone(),
125            credential_id: response.key_handle.clone().to_vec().into(),
126            rp_id: app_id.into(),
127            user_handle: None,
128            username: None,
129            user_display_name: None,
130            counter: Some(0),
131            extensions: Default::default(),
132        }
133    }
134
135    /// Upgrade a U2F Authentication Request into a Passkey
136    pub fn from_u2f_auth_request(
137        request: &AuthenticationRequest,
138        counter: u32,
139        private_key: &CoseKey,
140    ) -> Self {
141        let app_id: Bytes = request.application.to_vec().into();
142        Self {
143            key: private_key.clone(),
144            credential_id: request.key_handle.clone().to_vec().into(),
145            rp_id: app_id.into(),
146            user_handle: None,
147            username: None,
148            user_display_name: None,
149            counter: Some(counter),
150            extensions: Default::default(),
151        }
152    }
153
154    /// This function wraps up a U2F registration request as a Passkey for storing
155    /// in a CredentialStore.
156    pub fn wrap_u2f_registration_request(
157        request: &RegisterRequest,
158        response: &RegisterResponse,
159        key_handle: &[u8],
160        private_key: &CoseKey,
161    ) -> (
162        Passkey,
163        ctap2::PublicKeyCredentialUserEntity,
164        ctap2::PublicKeyCredentialRpEntity,
165    ) {
166        let passkey = Passkey::from_u2f_register_response(request, response, private_key);
167
168        let user_entity = ctap2::PublicKeyCredentialUserEntity {
169            id: key_handle.to_vec().into(),
170            display_name: None,
171            name: None,
172            icon_url: None,
173        };
174
175        let app_id: Bytes = request.application.to_vec().into();
176        let rp = ctap2::PublicKeyCredentialRpEntity {
177            id: app_id.into(),
178            name: None,
179        };
180
181        (passkey, user_entity, rp)
182    }
183
184    /// Create a passkey mock builder.
185    ///
186    /// The default credential Id length is 16, change it with the [`PasskeyBuilder::credential_id`]
187    /// method.
188    #[cfg(feature = "testable")]
189    pub fn mock(rp_id: String) -> PasskeyBuilder {
190        PasskeyBuilder::new(rp_id)
191    }
192}
193
194impl From<Passkey> for webauthn::PublicKeyCredentialDescriptor {
195    fn from(value: Passkey) -> Self {
196        Self {
197            ty: webauthn::PublicKeyCredentialType::PublicKey,
198            id: value.credential_id,
199            transports: None,
200        }
201    }
202}
203
204impl From<&Passkey> for webauthn::PublicKeyCredentialDescriptor {
205    fn from(value: &Passkey) -> Self {
206        Self {
207            ty: webauthn::PublicKeyCredentialType::PublicKey,
208            id: value.credential_id.clone(),
209            transports: None,
210        }
211    }
212}
213
214impl Debug for Passkey {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        f.debug_struct("Passkey")
217            .field("key_type", &self.key.kty)
218            .field("counter", &self.counter)
219            .finish()
220    }
221}
222
223/// Supported extensions on a [`Passkey`]
224#[derive(Default, Clone, Zeroize, ZeroizeOnDrop)]
225#[cfg_attr(any(test, feature = "testable"), derive(PartialEq))]
226pub struct CredentialExtensions {
227    /// Whether the passkey has hmac-secret credentials associated to it
228    pub hmac_secret: Option<StoredHmacSecret>,
229}
230
231/// The stored hmac-secret credentials associated to a [`Passkey`]
232#[derive(Clone, Zeroize, ZeroizeOnDrop)]
233#[cfg_attr(any(test, feature = "testable"), derive(PartialEq))]
234pub struct StoredHmacSecret {
235    /// The credential that must be gated behind user verification
236    pub cred_with_uv: Vec<u8>,
237    /// The credential that is not gated behind user verification, but is gated behind user presence
238    pub cred_without_uv: Option<Vec<u8>>,
239}
240
241impl Debug for StoredHmacSecret {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        f.debug_struct("StoredHmacSecret")
244            .field("cred_with_uv", &"<Redacted>")
245            .field(
246                "cred_without_uv",
247                if self.cred_without_uv.is_some() {
248                    &"<Redacted>"
249                } else {
250                    &"None"
251                },
252            )
253            .finish()
254    }
255}