Skip to main content

fission_shell_winit/
passkey.rs

1use fission_core::{
2    PasskeyAuthenticationRequest, PasskeyAuthenticationResult, PasskeyAvailability, PasskeyError,
3    PasskeyRegistrationRequest, PasskeyRegistrationResult, AUTHENTICATE_PASSKEY,
4    CANCEL_PASSKEY_OPERATION, GET_PASSKEY_AVAILABILITY, REGISTER_PASSKEY,
5};
6use fission_shell::async_host::AsyncRegistry;
7use std::sync::Arc;
8
9/// Host-side passkey/WebAuthn provider used by shell capability registration.
10pub trait PasskeyHost: Send + Sync + 'static {
11    /// Returns passkey support for the current host, origin, and credential backend.
12    fn availability(&self) -> Result<PasskeyAvailability, PasskeyError>;
13    /// Starts passkey registration and returns WebAuthn registration data.
14    ///
15    /// `request` contains the relying-party identity, user handle, challenge,
16    /// requested algorithms, and authenticator preferences. The returned payload
17    /// must be sent to the relying-party server for verification before the app
18    /// treats the passkey as enrolled.
19    fn register(
20        &self,
21        request: PasskeyRegistrationRequest,
22    ) -> Result<PasskeyRegistrationResult, PasskeyError>;
23    /// Starts passkey authentication and returns WebAuthn assertion data.
24    ///
25    /// `request.challenge` must originate from the server. The returned assertion
26    /// proves nothing until the server verifies the challenge, signature,
27    /// authenticator data, and credential id against the account.
28    fn authenticate(
29        &self,
30        request: PasskeyAuthenticationRequest,
31    ) -> Result<PasskeyAuthenticationResult, PasskeyError>;
32    /// Cancels an active passkey prompt where the host allows cancellation.
33    fn cancel(&self) -> Result<(), PasskeyError>;
34}
35
36/// Default provider used when the active shell has no passkey integration.
37#[derive(Debug, Default)]
38pub struct UnsupportedPasskeyHost;
39
40impl PasskeyHost for UnsupportedPasskeyHost {
41    fn availability(&self) -> Result<PasskeyAvailability, PasskeyError> {
42        Ok(PasskeyAvailability {
43            reason: Some("passkeys are not supported by this host".into()),
44            ..Default::default()
45        })
46    }
47
48    fn register(
49        &self,
50        _request: PasskeyRegistrationRequest,
51    ) -> Result<PasskeyRegistrationResult, PasskeyError> {
52        Err(PasskeyError::unsupported("register"))
53    }
54
55    fn authenticate(
56        &self,
57        _request: PasskeyAuthenticationRequest,
58    ) -> Result<PasskeyAuthenticationResult, PasskeyError> {
59        Err(PasskeyError::unsupported("authenticate"))
60    }
61
62    fn cancel(&self) -> Result<(), PasskeyError> {
63        Err(PasskeyError::unsupported("cancel"))
64    }
65}
66
67/// In-process passkey host for tests and non-OS environments.
68#[derive(Debug, Clone)]
69pub struct MemoryPasskeyHost {
70    availability: PasskeyAvailability,
71    registration_result: PasskeyRegistrationResult,
72    authentication_result: PasskeyAuthenticationResult,
73}
74
75impl MemoryPasskeyHost {
76    pub fn new(
77        availability: PasskeyAvailability,
78        registration_result: PasskeyRegistrationResult,
79        authentication_result: PasskeyAuthenticationResult,
80    ) -> Self {
81        Self {
82            availability,
83            registration_result,
84            authentication_result,
85        }
86    }
87}
88
89impl Default for MemoryPasskeyHost {
90    fn default() -> Self {
91        Self {
92            availability: PasskeyAvailability {
93                supported: true,
94                secure_context: true,
95                platform_authenticator_available: true,
96                conditional_ui_available: true,
97                cross_platform_authenticator_available: true,
98                reason: None,
99            },
100            registration_result: PasskeyRegistrationResult {
101                credential_id: vec![1, 2, 3],
102                raw_id: vec![1, 2, 3],
103                client_data_json: br#"{"type":"webauthn.create"}"#.to_vec(),
104                attestation_object: vec![4, 5, 6],
105                authenticator_attachment: None,
106                transports: Vec::new(),
107            },
108            authentication_result: PasskeyAuthenticationResult {
109                credential_id: vec![1, 2, 3],
110                raw_id: vec![1, 2, 3],
111                user_handle: Some(vec![9]),
112                client_data_json: br#"{"type":"webauthn.get"}"#.to_vec(),
113                authenticator_data: vec![4],
114                signature: vec![5, 6],
115            },
116        }
117    }
118}
119
120impl PasskeyHost for MemoryPasskeyHost {
121    fn availability(&self) -> Result<PasskeyAvailability, PasskeyError> {
122        Ok(self.availability.clone())
123    }
124
125    fn register(
126        &self,
127        _request: PasskeyRegistrationRequest,
128    ) -> Result<PasskeyRegistrationResult, PasskeyError> {
129        Ok(self.registration_result.clone())
130    }
131
132    fn authenticate(
133        &self,
134        _request: PasskeyAuthenticationRequest,
135    ) -> Result<PasskeyAuthenticationResult, PasskeyError> {
136        Ok(self.authentication_result.clone())
137    }
138
139    fn cancel(&self) -> Result<(), PasskeyError> {
140        Ok(())
141    }
142}
143
144pub(crate) fn register_passkey_capabilities(
145    async_registry: &mut AsyncRegistry,
146    host: Arc<dyn PasskeyHost>,
147) {
148    let availability_host = host.clone();
149    async_registry.register_operation_capability(GET_PASSKEY_AVAILABILITY, move |(), _| {
150        let host = availability_host.clone();
151        async move { host.availability() }
152    });
153
154    let register_host = host.clone();
155    async_registry.register_operation_capability(REGISTER_PASSKEY, move |request, _| {
156        let host = register_host.clone();
157        async move { host.register(request) }
158    });
159
160    let authenticate_host = host.clone();
161    async_registry.register_operation_capability(AUTHENTICATE_PASSKEY, move |request, _| {
162        let host = authenticate_host.clone();
163        async move { host.authenticate(request) }
164    });
165
166    async_registry.register_operation_capability(CANCEL_PASSKEY_OPERATION, move |(), _| {
167        let host = host.clone();
168        async move { host.cancel() }
169    });
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use fission_core::{
176        PasskeyAlgorithm, PasskeyAttestationConveyance, PasskeyAuthenticationRequest,
177        PasskeyMediation, PasskeyRegistrationRequest, PasskeyRelyingParty, PasskeyUser,
178        PasskeyUserVerification,
179    };
180
181    #[test]
182    fn unsupported_host_reports_unavailable() {
183        let host = UnsupportedPasskeyHost;
184        let availability = host.availability().unwrap();
185
186        assert!(!availability.supported);
187        assert!(host
188            .authenticate(PasskeyAuthenticationRequest {
189                relying_party_id: "example.com".into(),
190                challenge: vec![1],
191                allow_credentials: Vec::new(),
192                user_verification: PasskeyUserVerification::Preferred,
193                mediation: PasskeyMediation::PlatformDefault,
194                timeout_ms: None,
195            })
196            .is_err());
197    }
198
199    #[test]
200    fn memory_host_registers_and_authenticates() {
201        let host = MemoryPasskeyHost::default();
202        let availability = host.availability().unwrap();
203        assert!(availability.supported);
204
205        let registration = host
206            .register(PasskeyRegistrationRequest {
207                relying_party: PasskeyRelyingParty::new("example.com", "Example"),
208                user: PasskeyUser::new(vec![1], "ada@example.com", "Ada"),
209                challenge: vec![2],
210                pub_key_algorithms: vec![PasskeyAlgorithm::ES256],
211                timeout_ms: None,
212                attestation: PasskeyAttestationConveyance::None,
213                authenticator_selection: None,
214                exclude_credentials: Vec::new(),
215            })
216            .unwrap();
217        assert_eq!(registration.credential_id, vec![1, 2, 3]);
218
219        let authentication = host
220            .authenticate(PasskeyAuthenticationRequest {
221                relying_party_id: "example.com".into(),
222                challenge: vec![3],
223                allow_credentials: Vec::new(),
224                user_verification: PasskeyUserVerification::Preferred,
225                mediation: PasskeyMediation::PlatformDefault,
226                timeout_ms: None,
227            })
228            .unwrap();
229        assert_eq!(authentication.credential_id, vec![1, 2, 3]);
230    }
231}