webauthn_authenticator_rs/
mozilla.rs

1//! Authenticator implementation using Mozilla's `authenticator-rs` library.
2//!
3//! This library only supports USB HID devices.
4#[cfg(doc)]
5use crate::stubs::*;
6
7use crate::error::WebauthnCError;
8use crate::AuthenticatorBackend;
9use crate::Url;
10use crate::BASE64_ENGINE;
11
12use base64::Engine;
13use base64urlsafedata::Base64UrlSafeData;
14use webauthn_rs_proto::PublicKeyCredentialCreationOptions;
15use webauthn_rs_proto::{
16    AuthenticatorAttestationResponseRaw, RegisterPublicKeyCredential,
17    RegistrationExtensionsClientOutputs,
18};
19
20use webauthn_rs_proto::PublicKeyCredentialRequestOptions;
21use webauthn_rs_proto::{
22    AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, PublicKeyCredential,
23};
24
25use authenticator::{authenticatorservice::AuthenticatorService, StatusUpdate};
26
27#[cfg(feature = "mozilla")]
28use authenticator::{
29    authenticatorservice::{RegisterArgs, SignArgs},
30    crypto::COSEAlgorithm,
31    ctap2::client_data::{ClientDataHash, CollectedClientData, WebauthnType},
32    ctap2::server::{
33        AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor,
34        PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
35        ResidentKeyRequirement, Transport, UserVerificationRequirement,
36    },
37    statecallback::StateCallback,
38    Pin, StatusPinUv,
39};
40
41use std::sync::mpsc::{channel, RecvError, Sender};
42use std::thread;
43
44pub struct MozillaAuthenticator {
45    status_tx: Sender<StatusUpdate>,
46    _thread_handle: thread::JoinHandle<()>,
47    manager: AuthenticatorService,
48}
49
50impl MozillaAuthenticator {
51    pub fn new() -> Self {
52        let mut manager =
53            AuthenticatorService::new().expect("The auth service should initialize safely");
54
55        manager.add_u2f_usb_hid_platform_transports();
56
57        let (status_tx, status_rx) = channel::<StatusUpdate>();
58
59        let _thread_handle = thread::spawn(move || loop {
60            match status_rx.recv() {
61                Ok(StatusUpdate::SelectDeviceNotice) => {
62                    info!("STATUS: Please select a device by touching one of them.");
63                }
64                Ok(StatusUpdate::PresenceRequired) => {
65                    info!("STATUS: Please touch your device.");
66                }
67
68                Ok(StatusUpdate::SelectResultNotice(_, _)) => {
69                    error!("Unexpected State - SelectResultNotice");
70                    return;
71                }
72
73                Ok(StatusUpdate::InteractiveManagement(..)) => {
74                    error!("Unexpected State - InteractiveManagement");
75                    return;
76                }
77
78                Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
79                    let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
80                        .expect("Failed to read PIN");
81                    sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
82                    continue;
83                }
84
85                Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
86                    error!(
87                        "Wrong PIN! {}",
88                        attempts.map_or("Try again.".to_string(), |a| format!(
89                            "You have {} attempts left.",
90                            a
91                        ))
92                    );
93                    let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
94                        .expect("Failed to read PIN");
95                    sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
96                    continue;
97                }
98
99                Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
100                    error!(
101                        "Invalid User Verification! {}",
102                        attempts.map_or("Try again.".to_string(), |a| format!(
103                            "You have {} attempts left.",
104                            a
105                        ))
106                    );
107                    continue;
108                }
109
110                Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
111                    error!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
112                }
113                Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked))
114                | Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
115                    error!("Too many failed attempts. Your device has been blocked. Reset it.")
116                }
117
118                Ok(StatusUpdate::PinUvError(e)) => {
119                    error!("Unexpected error: {:?}", e)
120                }
121
122                Err(RecvError) => {
123                    debug!("STATUS: end");
124                    return;
125                }
126            }
127        });
128
129        MozillaAuthenticator {
130            status_tx,
131            _thread_handle,
132            manager,
133        }
134    }
135}
136
137impl Default for MozillaAuthenticator {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl AuthenticatorBackend for MozillaAuthenticator {
144    fn perform_register(
145        &mut self,
146        origin: Url,
147        options: PublicKeyCredentialCreationOptions,
148        timeout_ms: u32,
149    ) -> Result<RegisterPublicKeyCredential, WebauthnCError> {
150        let client_data = CollectedClientData {
151            webauthn_type: WebauthnType::Create,
152            challenge: options.challenge.to_vec().into(),
153            origin: origin.to_string(),
154            cross_origin: false,
155            token_binding: None,
156        };
157
158        let ClientDataHash(client_data_hash) = client_data
159            .hash()
160            .map_err(|_| WebauthnCError::InvalidRegistration)?;
161
162        let pub_cred_params = options
163            .pub_key_cred_params
164            .into_iter()
165            .map(|param| {
166                COSEAlgorithm::try_from(param.alg)
167                    .map_err(|e| {
168                        error!(?e, "error converting to COSEAlgorithm");
169                        WebauthnCError::InvalidAlgorithm
170                    })
171                    .map(|alg| PublicKeyCredentialParameters { alg })
172            })
173            .collect::<Result<Vec<_>, _>>()?;
174
175        let ctap_args = RegisterArgs {
176            client_data_hash,
177            relying_party: RelyingParty {
178                id: options.rp.id,
179                name: Some(options.rp.name),
180            },
181            origin: origin.to_string(),
182            user: PublicKeyCredentialUserEntity {
183                id: options.user.id.into(),
184                name: Some(options.user.name),
185                display_name: Some(options.user.display_name),
186            },
187            pub_cred_params,
188            exclude_list: vec![],
189            user_verification_req: UserVerificationRequirement::Required,
190            resident_key_req: ResidentKeyRequirement::Discouraged,
191
192            pin: None,
193            extensions: AuthenticationExtensionsClientInputs {
194                ..Default::default()
195            },
196            use_ctap1_fallback: false,
197        };
198
199        /* Actually call the library. */
200        let (register_tx, register_rx) = channel();
201
202        let callback = StateCallback::new(Box::new(move |rv| {
203            register_tx
204                .send(rv)
205                .expect("Unable to proceed - state callback channel closed!");
206        }));
207
208        if let Err(_e) = self.manager.register(
209            timeout_ms.into(),
210            ctap_args.into(),
211            self.status_tx.clone(),
212            callback,
213        ) {
214            return Err(WebauthnCError::PlatformAuthenticator);
215        };
216
217        let register_result = register_rx
218            .recv()
219            // If the channel closes, the platform goes away.
220            .map_err(|_| WebauthnCError::PlatformAuthenticator)?
221            // If the registration failed
222            .map_err(|_| WebauthnCError::InvalidRegistration)?;
223
224        let attestation_object = register_result.att_obj;
225
226        trace!(?attestation_object);
227        trace!(?client_data);
228
229        // Warning! In the future this may change!
230        // This currently relies on serde_json and serde_cbor_2 being deterministic, and has
231        // been brought up with MS.
232
233        let raw_id = if let Some(cred_data) = &attestation_object.auth_data.credential_data {
234            Base64UrlSafeData::from(cred_data.credential_id.clone())
235        } else {
236            return Err(WebauthnCError::PlatformAuthenticator);
237        };
238
239        // Based on the request attestation format, provide it
240        let attestation_object =
241            serde_cbor_2::to_vec(&attestation_object).map_err(|_| WebauthnCError::Cbor)?;
242
243        let client_data_json =
244            serde_json::to_vec(&client_data).map_err(|_| WebauthnCError::Json)?;
245
246        Ok(RegisterPublicKeyCredential {
247            id: BASE64_ENGINE.encode(&raw_id),
248            raw_id,
249            response: AuthenticatorAttestationResponseRaw {
250                attestation_object: attestation_object.into(),
251                client_data_json: client_data_json.into(),
252                transports: None,
253            },
254            type_: "public-key".to_string(),
255            extensions: RegistrationExtensionsClientOutputs {
256                ..Default::default()
257            },
258        })
259    }
260
261    fn perform_auth(
262        &mut self,
263        origin: Url,
264        options: PublicKeyCredentialRequestOptions,
265        timeout_ms: u32,
266    ) -> Result<PublicKeyCredential, WebauthnCError> {
267        let client_data = CollectedClientData {
268            webauthn_type: WebauthnType::Get,
269            challenge: options.challenge.to_vec().into(),
270            origin: origin.to_string(),
271            cross_origin: false,
272            token_binding: None,
273        };
274
275        let ClientDataHash(client_data_hash) = client_data
276            .hash()
277            .map_err(|_| WebauthnCError::InvalidRegistration)?;
278
279        let allow_list = options
280            .allow_credentials
281            .iter()
282            .map(|cred| {
283                PublicKeyCredentialDescriptor {
284                    id: cred.id.clone().into(),
285                    // It appears we have to always specify the lower transport in this
286                    // library due to discovered bugs
287                    transports: vec![Transport::USB],
288                }
289            })
290            .collect();
291
292        let ctap_args = SignArgs {
293            client_data_hash,
294            origin: origin.to_string(),
295            relying_party_id: options.rp_id,
296            allow_list,
297
298            user_verification_req: UserVerificationRequirement::Required,
299            user_presence_req: true,
300
301            extensions: AuthenticationExtensionsClientInputs {
302                ..Default::default()
303            },
304            pin: None,
305            use_ctap1_fallback: false,
306        };
307
308        let (sign_tx, sign_rx) = channel();
309
310        let callback = StateCallback::new(Box::new(move |rv| {
311            sign_tx
312                .send(rv)
313                .expect("Unable to proceed - state callback channel closed!");
314        }));
315
316        if let Err(_e) = self.manager.sign(
317            timeout_ms.into(),
318            ctap_args.into(),
319            self.status_tx.clone(),
320            callback,
321        ) {
322            return Err(WebauthnCError::PlatformAuthenticator);
323        }
324
325        let sign_result = sign_rx
326            .recv()
327            .map_err(|_| WebauthnCError::PlatformAuthenticator)?
328            .map_err(|_| WebauthnCError::InvalidAssertion)?;
329
330        let assertion = sign_result.assertion;
331
332        trace!(?assertion);
333        trace!(?client_data);
334
335        let raw_id = assertion
336            .credentials
337            .map(|pkdesc| Base64UrlSafeData::from(pkdesc.id))
338            .ok_or(WebauthnCError::Internal)?;
339
340        // let authenticator_data = serde_cbor_2::to_vec(&assertion.auth_data)
341        let authenticator_data = assertion.auth_data.to_vec();
342
343        let client_data_json =
344            serde_json::to_vec(&client_data).map_err(|_| WebauthnCError::Json)?;
345
346        Ok(PublicKeyCredential {
347            id: BASE64_ENGINE.encode(&raw_id),
348            raw_id,
349            response: AuthenticatorAssertionResponseRaw {
350                authenticator_data: authenticator_data.into(),
351                client_data_json: client_data_json.into(),
352                signature: assertion.signature.into(),
353                user_handle: assertion.user.map(|u| u.id.into()),
354            },
355            type_: "public-key".to_string(),
356            extensions: AuthenticationExtensionsClientOutputs {
357                ..Default::default()
358            },
359        })
360    }
361}