Skip to main content

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}