fission_core/platform_passkey.rs
1//! Passkey and WebAuthn host capabilities.
2//!
3//! Passkeys are credential assertions, not raw biometric access. A browser,
4//! operating system, password manager, or hardware authenticator may use a
5//! fingerprint, face check, device PIN, or security key touch to unlock the
6//! credential, but the app receives WebAuthn-style registration or
7//! authentication data for server verification.
8
9use crate::capability::{CapabilityType, OperationCapability};
10use serde::{Deserialize, Serialize};
11
12/// Host support state for passkey operations in the active session.
13#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
14pub struct PasskeyAvailability {
15 /// `true` when the active host can perform at least one passkey operation.
16 pub supported: bool,
17 /// `true` when the origin or app context satisfies secure-context rules.
18 pub secure_context: bool,
19 /// `true` when a built-in platform authenticator is available.
20 pub platform_authenticator_available: bool,
21 /// `true` when the host can present passkeys as conditional sign-in UI.
22 pub conditional_ui_available: bool,
23 /// `true` when roaming authenticators such as hardware security keys may be used.
24 pub cross_platform_authenticator_available: bool,
25 /// Human-readable explanation when support is unavailable or degraded.
26 pub reason: Option<String>,
27}
28
29/// Relying-party identity used for passkey creation.
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub struct PasskeyRelyingParty {
32 /// Relying-party id, normally the effective domain that owns the credential.
33 pub id: String,
34 /// Human-readable product or organization name shown during registration.
35 pub name: String,
36}
37
38impl PasskeyRelyingParty {
39 /// Creates a relying-party identity.
40 ///
41 /// `id` is normally the effective domain that owns the credential, such as
42 /// `example.com`. `name` is the user-facing product or organization name the
43 /// authenticator may show during registration.
44 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
45 Self {
46 id: id.into(),
47 name: name.into(),
48 }
49 }
50}
51
52/// User identity supplied during passkey registration.
53#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
54pub struct PasskeyUser {
55 /// Stable opaque user handle supplied by the server.
56 pub id: Vec<u8>,
57 /// Account identifier, often an email address or username.
58 pub name: String,
59 /// Human-readable account name shown by authenticators.
60 pub display_name: String,
61}
62
63impl PasskeyUser {
64 /// Creates a user identity for passkey registration.
65 ///
66 /// `id` should be a stable opaque server-side user handle, not an email
67 /// address. `name` is often the account login identifier, while
68 /// `display_name` is the human-readable name shown by authenticators.
69 pub fn new(
70 id: impl Into<Vec<u8>>,
71 name: impl Into<String>,
72 display_name: impl Into<String>,
73 ) -> Self {
74 Self {
75 id: id.into(),
76 name: name.into(),
77 display_name: display_name.into(),
78 }
79 }
80}
81
82/// Authenticator transport hints for existing credentials.
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
84pub enum PasskeyTransport {
85 /// USB-connected authenticator.
86 Usb,
87 /// Near-field communication authenticator.
88 Nfc,
89 /// Bluetooth Low Energy authenticator.
90 Ble,
91 /// Platform authenticator built into the device.
92 Internal,
93 /// Hybrid transport such as phone-assisted sign-in.
94 Hybrid,
95 /// Transport was not supplied or is not recognized by this Fission version.
96 Unknown,
97}
98
99/// Public-key algorithm requested during credential creation.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
101pub enum PasskeyAlgorithm {
102 /// ECDSA with SHA-256.
103 ES256,
104 /// RSA PKCS#1 with SHA-256.
105 RS256,
106 /// Edwards-curve signatures.
107 EdDSA,
108 /// COSE algorithm id not represented by the named variants.
109 Other(i32),
110}
111
112/// How strongly the authenticator should verify the user.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
114pub enum PasskeyUserVerification {
115 /// The authenticator must verify the local user.
116 Required,
117 /// The authenticator should verify the local user when possible.
118 Preferred,
119 /// The authenticator should avoid user verification when possible.
120 Discouraged,
121 /// Let the host choose its platform default.
122 #[default]
123 PlatformDefault,
124}
125
126/// Authenticator attachment preference.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
128pub enum PasskeyAuthenticatorAttachment {
129 /// Prefer an authenticator built into the device.
130 Platform,
131 /// Prefer a roaming authenticator such as a security key or phone.
132 CrossPlatform,
133}
134
135/// Resident-key requirement during passkey creation.
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
137pub enum PasskeyResidentKeyRequirement {
138 /// A discoverable credential is required.
139 Required,
140 /// A discoverable credential is preferred.
141 Preferred,
142 /// A discoverable credential should be avoided when possible.
143 Discouraged,
144 /// Let the host choose its platform default.
145 #[default]
146 PlatformDefault,
147}
148
149/// Attestation conveyance preference for passkey registration.
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
151pub enum PasskeyAttestationConveyance {
152 /// Do not request attestation.
153 #[default]
154 None,
155 /// Request indirect attestation where the host supports it.
156 Indirect,
157 /// Request direct attestation where the host supports it.
158 Direct,
159 /// Request enterprise attestation where policy permits it.
160 Enterprise,
161}
162
163/// Browser mediation preference for passkey authentication.
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
165pub enum PasskeyMediation {
166 /// Attempt authentication without visible UI where the host supports it.
167 Silent,
168 /// Allow optional UI.
169 Optional,
170 /// Allow conditional passkey UI, commonly used for sign-in forms.
171 Conditional,
172 /// Require visible mediation.
173 Required,
174 /// Let the host choose its platform default.
175 #[default]
176 PlatformDefault,
177}
178
179/// Existing credential descriptor used for allow/exclude lists.
180#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
181pub struct PasskeyCredentialDescriptor {
182 /// Raw credential id returned by a previous registration.
183 pub id: Vec<u8>,
184 /// Optional transport hints stored with the credential.
185 pub transports: Vec<PasskeyTransport>,
186}
187
188impl PasskeyCredentialDescriptor {
189 /// Creates a credential descriptor for allow/exclude lists.
190 ///
191 /// `id` is the raw credential id returned by a prior registration. Transport
192 /// hints are optional; pass an empty list when the server has not stored
193 /// transport information.
194 pub fn new(id: impl Into<Vec<u8>>, transports: Vec<PasskeyTransport>) -> Self {
195 Self {
196 id: id.into(),
197 transports,
198 }
199 }
200}
201
202/// Authenticator selection preferences for registration.
203#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
204pub struct PasskeyAuthenticatorSelection {
205 /// Optional platform-vs-roaming authenticator preference.
206 pub attachment: Option<PasskeyAuthenticatorAttachment>,
207 /// Resident-key requirement for the new credential.
208 pub resident_key: PasskeyResidentKeyRequirement,
209 /// User-verification requirement for the registration prompt.
210 pub user_verification: PasskeyUserVerification,
211}
212
213/// Request to create and register a new passkey.
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub struct PasskeyRegistrationRequest {
216 /// Relying party that owns the credential.
217 pub relying_party: PasskeyRelyingParty,
218 /// User account identity supplied by the server.
219 pub user: PasskeyUser,
220 /// Fresh challenge generated by the relying-party server.
221 pub challenge: Vec<u8>,
222 /// Public-key algorithms the server is willing to accept.
223 pub pub_key_algorithms: Vec<PasskeyAlgorithm>,
224 /// Optional host prompt timeout in milliseconds.
225 pub timeout_ms: Option<u64>,
226 /// Attestation preference for the credential creation request.
227 pub attestation: PasskeyAttestationConveyance,
228 /// Optional authenticator selection preferences.
229 pub authenticator_selection: Option<PasskeyAuthenticatorSelection>,
230 /// Credentials that should not be registered again for this account.
231 pub exclude_credentials: Vec<PasskeyCredentialDescriptor>,
232}
233
234/// Successful passkey registration payload.
235#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
236pub struct PasskeyRegistrationResult {
237 /// Credential id to store on the server after verification succeeds.
238 pub credential_id: Vec<u8>,
239 /// Raw credential id as returned by the authenticator.
240 pub raw_id: Vec<u8>,
241 /// Client data JSON bytes that the server must verify.
242 pub client_data_json: Vec<u8>,
243 /// Attestation object bytes that the server must verify.
244 pub attestation_object: Vec<u8>,
245 /// Authenticator attachment actually used, when reported by the host.
246 pub authenticator_attachment: Option<PasskeyAuthenticatorAttachment>,
247 /// Transports reported for the created credential.
248 pub transports: Vec<PasskeyTransport>,
249}
250
251/// Request to authenticate with an existing passkey.
252#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
253pub struct PasskeyAuthenticationRequest {
254 /// Relying-party id whose credential should be used.
255 pub relying_party_id: String,
256 /// Fresh authentication challenge generated by the server.
257 pub challenge: Vec<u8>,
258 /// Optional allow-list of credentials that may satisfy the assertion.
259 pub allow_credentials: Vec<PasskeyCredentialDescriptor>,
260 /// User-verification requirement for the authentication prompt.
261 pub user_verification: PasskeyUserVerification,
262 /// Mediation preference for the authentication prompt.
263 pub mediation: PasskeyMediation,
264 /// Optional host prompt timeout in milliseconds.
265 pub timeout_ms: Option<u64>,
266}
267
268/// Successful passkey authentication payload.
269#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
270pub struct PasskeyAuthenticationResult {
271 /// Credential id used for the assertion.
272 pub credential_id: Vec<u8>,
273 /// Raw credential id as returned by the authenticator.
274 pub raw_id: Vec<u8>,
275 /// Optional opaque user handle returned by the authenticator.
276 pub user_handle: Option<Vec<u8>>,
277 /// Client data JSON bytes that the server must verify.
278 pub client_data_json: Vec<u8>,
279 /// Authenticator data bytes that the server must verify.
280 pub authenticator_data: Vec<u8>,
281 /// Assertion signature that the server must verify.
282 pub signature: Vec<u8>,
283}
284
285/// Portable passkey error payload.
286#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
287pub struct PasskeyError {
288 /// Stable machine-readable error code.
289 pub code: String,
290 /// Human-readable error message for logs or developer UI.
291 pub message: String,
292}
293
294impl PasskeyError {
295 /// Creates a portable passkey error payload.
296 ///
297 /// `code` should be stable and machine-readable, such as `unsupported`,
298 /// `not_allowed`, `invalid_state`, or `security_error`. `message` should be
299 /// a concise explanation for logs, diagnostics, or developer-facing UI.
300 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
301 Self {
302 code: code.into(),
303 message: message.into(),
304 }
305 }
306
307 /// Creates the standard unsupported-operation error for this capability.
308 ///
309 /// Use this from hosts that implement the Fission provider trait but cannot
310 /// perform the requested passkey operation on the active platform, origin, or
311 /// credential backend.
312 pub fn unsupported(operation: impl Into<String>) -> Self {
313 Self::new(
314 "unsupported",
315 format!(
316 "passkey operation `{}` is not supported by this host",
317 operation.into()
318 ),
319 )
320 }
321}
322
323pub struct GetPasskeyAvailabilityCapability;
324impl OperationCapability for GetPasskeyAvailabilityCapability {
325 type Request = ();
326 type Ok = PasskeyAvailability;
327 type Err = PasskeyError;
328}
329
330pub struct RegisterPasskeyCapability;
331impl OperationCapability for RegisterPasskeyCapability {
332 type Request = PasskeyRegistrationRequest;
333 type Ok = PasskeyRegistrationResult;
334 type Err = PasskeyError;
335}
336
337pub struct AuthenticatePasskeyCapability;
338impl OperationCapability for AuthenticatePasskeyCapability {
339 type Request = PasskeyAuthenticationRequest;
340 type Ok = PasskeyAuthenticationResult;
341 type Err = PasskeyError;
342}
343
344pub struct CancelPasskeyOperationCapability;
345impl OperationCapability for CancelPasskeyOperationCapability {
346 type Request = ();
347 type Ok = ();
348 type Err = PasskeyError;
349}
350
351pub const GET_PASSKEY_AVAILABILITY: CapabilityType<GetPasskeyAvailabilityCapability> =
352 CapabilityType::new("fission.passkey.get_availability");
353pub const REGISTER_PASSKEY: CapabilityType<RegisterPasskeyCapability> =
354 CapabilityType::new("fission.passkey.register");
355pub const AUTHENTICATE_PASSKEY: CapabilityType<AuthenticatePasskeyCapability> =
356 CapabilityType::new("fission.passkey.authenticate");
357pub const CANCEL_PASSKEY_OPERATION: CapabilityType<CancelPasskeyOperationCapability> =
358 CapabilityType::new("fission.passkey.cancel");
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn passkey_registration_request_round_trips() {
366 let request = PasskeyRegistrationRequest {
367 relying_party: PasskeyRelyingParty::new("example.com", "Example"),
368 user: PasskeyUser::new(vec![1, 2, 3], "ada@example.com", "Ada"),
369 challenge: vec![9, 8, 7],
370 pub_key_algorithms: vec![PasskeyAlgorithm::ES256, PasskeyAlgorithm::EdDSA],
371 timeout_ms: Some(60_000),
372 attestation: PasskeyAttestationConveyance::None,
373 authenticator_selection: Some(PasskeyAuthenticatorSelection {
374 attachment: Some(PasskeyAuthenticatorAttachment::Platform),
375 resident_key: PasskeyResidentKeyRequirement::Required,
376 user_verification: PasskeyUserVerification::Required,
377 }),
378 exclude_credentials: vec![PasskeyCredentialDescriptor::new(
379 vec![4, 5, 6],
380 vec![PasskeyTransport::Internal],
381 )],
382 };
383
384 let bytes = serde_json::to_vec(&request).unwrap();
385 let decoded: PasskeyRegistrationRequest = serde_json::from_slice(&bytes).unwrap();
386
387 assert_eq!(decoded, request);
388 }
389
390 #[test]
391 fn passkey_authentication_result_round_trips() {
392 let result = PasskeyAuthenticationResult {
393 credential_id: vec![1, 2, 3],
394 raw_id: vec![1, 2, 3],
395 user_handle: Some(vec![7, 8]),
396 client_data_json: br#"{"type":"webauthn.get"}"#.to_vec(),
397 authenticator_data: vec![9],
398 signature: vec![10, 11],
399 };
400
401 let bytes = serde_json::to_vec(&result).unwrap();
402 let decoded: PasskeyAuthenticationResult = serde_json::from_slice(&bytes).unwrap();
403
404 assert_eq!(decoded, result);
405 }
406}