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}