webauthn_authenticator_rs/
lib.rs

1//! webauthn-authenticator-rs is a library for interfacing with FIDO/CTAP 2
2//! tokens.
3//!
4//! This performs the actions that would be taken by a client application (such
5//! as a web browser) to facilitate authentication with a remote service.
6//!
7//! This library aims to provide abstrations over many platform-specific APIs,
8//! so that client applications don't need to worry as much about the finer
9//! details of the protocol.
10//!
11//! **This is a "pre-1.0" library:** it is still under active development, and
12//! the API is not yet stable or final. *Some of the modules have edge cases
13//! which may cause you to get permanently locked out of your authenticator.*
14//!
15//! This library is not FIDO certified, and currently lacks a thorough security
16//! review.
17//!
18//! ## FIDO / CTAP version support
19//!
20//! This library currently only supports CTAP 2.0, 2.1 or 2.1-PRE.
21//!
22//! Authenticators which **only** support CTAP 1.x (U2F) are *unsupported*. This
23//! generally only is an issue for older tokens.
24//!
25//! The authors of this library recommend using [FIDO2 certified][] hardware
26//! authenticators with at least [Autenticator Certification Level 2][cert].
27//! Be cautious when buying, as there are many products on the market which
28//! falsely claim certification, have implementation errors, only support U2F,
29//! or use off-the-shelf microcontrollers which do not protect key material
30//! ([Level 1][cert]).
31//!
32//! ## Features
33//!
34//! **Note:** these links may be broken unless you build the documentation with
35//! the appropriate `--features` flag listed inline.
36//!
37//! ### Transports and backends
38//!
39//! * `bluetooth`: [Bluetooth][] [^openssl]
40//! * `cable`: [caBLE / Hybrid Authenticator][cable] [^openssl]
41//!   * `cable-override-tunnel`: [Override caBLE tunnel server URLs][cable-url]
42//! * `mozilla`: [Mozilla Authenticator][], formerly known as `u2fhid`
43//! * `nfc`: [NFC][] via PC/SC API [^openssl]
44//! * `softpasskey`: [SoftPasskey][] (for testing) [^openssl]
45//! * `softtoken`: [SoftToken][] (for testing) [^openssl]
46//! * `usb`: [USB HID][] [^openssl]
47//! * `win10`: [Windows 10][] WebAuthn API
48//!
49//! [^openssl]: Feature requires OpenSSL.
50//!
51//! ### Miscellaneous features
52//!
53//! * `ctap2`: [CTAP 2.0, 2.1 and 2.1-PRE implementation][crate::ctap2]
54//!   [^openssl].
55//!
56//!   Automatically enabled by the `bluetooth`, `cable`, `ctap2-management`,
57//!   `nfc`, `softtoken` and `usb` features.
58//!
59//!   * `ctap2-management`: Adds support for configuring and managing CTAP 2.x
60//!     hardware authenticators to the [CTAP 2.x implementation][crate::ctap2].
61//!
62//! * `crypto`: Enables OpenSSL support [^openssl]. This allows the library to
63//!   avoid a hard dependency on OpenSSL on Windows, if only the `win10` backend
64//!   is enabled.
65//!
66//!   Automatically enabled by the `ctap2`, `softpasskey` and `softtoken`
67//!   features.
68//!
69//! * `qrcode`: QR code display for the [Cli][] UI, recommended for use if the
70//!   `cable` and `ui-cli` features are both enabled
71//!
72//! * `ui-cli`: [Cli][] UI
73//!
74//! [FIDO2 certified]: https://fidoalliance.org/fido-certified-showcase/
75//! [Bluetooth]: crate::bluetooth
76//! [cert]: https://fidoalliance.org/certification/authenticator-certification-levels/
77//! [cable]: crate::cable
78//! [cable-url]: crate::cable::connect_cable_authenticator_with_tunnel_uri
79//! [Cli]: crate::ui::Cli
80//! [Mozilla Authenticator]: crate::mozilla
81//! [NFC]: crate::nfc
82//! [SoftPasskey]: crate::softpasskey
83//! [SoftToken]: crate::softtoken
84//! [USB HID]: crate::usb
85//! [Windows 10]: crate::win10
86
87#![cfg_attr(docsrs, feature(doc_cfg))]
88// #![deny(warnings)]
89#![warn(unused_extern_crates)]
90// #![warn(missing_docs)]
91#![deny(clippy::todo)]
92#![deny(clippy::unimplemented)]
93#![deny(clippy::unwrap_used)]
94// #![deny(clippy::expect_used)]
95#![deny(clippy::panic)]
96#![deny(clippy::unreachable)]
97#![deny(clippy::await_holding_lock)]
98#![deny(clippy::needless_pass_by_value)]
99#![deny(clippy::trivially_copy_pass_by_ref)]
100
101#[macro_use]
102extern crate num_derive;
103#[macro_use]
104extern crate tracing;
105
106use crate::error::WebauthnCError;
107use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64_ENGINE;
108use url::Url;
109
110use webauthn_rs_proto::{
111    CreationChallengeResponse, PublicKeyCredential, PublicKeyCredentialCreationOptions,
112    PublicKeyCredentialRequestOptions, RegisterPublicKeyCredential, RequestChallengeResponse,
113};
114
115pub mod prelude {
116    pub use crate::error::WebauthnCError;
117    pub use crate::WebauthnAuthenticator;
118    pub use url::Url;
119    pub use webauthn_rs_proto::{
120        CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
121        RequestChallengeResponse,
122    };
123}
124
125mod authenticator_hashed;
126#[cfg(any(all(doc, not(doctest)), feature = "crypto"))]
127mod crypto;
128
129#[cfg(any(all(doc, not(doctest)), feature = "ctap2"))]
130pub mod ctap2;
131pub mod error;
132#[cfg(any(all(doc, not(doctest)), feature = "vendor-yubikey"))]
133mod tlv;
134#[cfg(any(all(doc, not(doctest)), feature = "ctap2"))]
135pub mod transport;
136pub mod types;
137pub mod ui;
138mod util;
139
140#[cfg(any(all(doc, not(doctest)), feature = "bluetooth"))]
141pub mod bluetooth;
142
143#[cfg(any(all(doc, not(doctest)), feature = "cable"))]
144pub mod cable;
145
146#[cfg(any(all(doc, not(doctest)), feature = "mozilla"))]
147pub mod mozilla;
148
149#[cfg(any(all(doc, not(doctest)), feature = "nfc"))]
150pub mod nfc;
151
152#[cfg(any(all(doc, not(doctest)), feature = "softpasskey"))]
153pub mod softpasskey;
154
155#[cfg(any(all(doc, not(doctest)), feature = "softtoken"))]
156pub mod softtoken;
157
158#[cfg(any(all(doc, not(doctest)), feature = "usb"))]
159pub mod usb;
160
161#[cfg(any(all(doc, not(doctest)), feature = "u2fhid"))]
162#[deprecated(
163    since = "0.5.0",
164    note = "The 'u2fhid' feature and module have been renamed to 'mozilla'."
165)]
166/// Mozilla `authenticator-rs` backend. Renamed to [MozillaAuthenticator][crate::mozilla::MozillaAuthenticator].
167pub mod u2fhid {
168    pub use crate::mozilla::MozillaAuthenticator as U2FHid;
169}
170
171#[cfg(any(all(doc, not(doctest)), feature = "win10"))]
172pub mod win10;
173
174#[cfg(doc)]
175#[doc(hidden)]
176mod stubs;
177
178#[cfg(any(all(doc, not(doctest)), feature = "ctap2"))]
179pub use crate::authenticator_hashed::{
180    perform_auth_with_request, perform_register_with_request, AuthenticatorBackendHashedClientData,
181};
182
183#[cfg(any(all(doc, not(doctest)), feature = "crypto"))]
184pub use crate::crypto::SHA256Hash;
185
186pub struct WebauthnAuthenticator<T>
187where
188    T: AuthenticatorBackend,
189{
190    backend: T,
191}
192
193pub trait AuthenticatorBackend {
194    fn perform_register(
195        &mut self,
196        origin: Url,
197        options: PublicKeyCredentialCreationOptions,
198        timeout_ms: u32,
199    ) -> Result<RegisterPublicKeyCredential, WebauthnCError>;
200
201    fn perform_auth(
202        &mut self,
203        origin: Url,
204        options: PublicKeyCredentialRequestOptions,
205        timeout_ms: u32,
206    ) -> Result<PublicKeyCredential, WebauthnCError>;
207}
208
209impl<T> WebauthnAuthenticator<T>
210where
211    T: AuthenticatorBackend,
212{
213    pub fn new(backend: T) -> Self {
214        WebauthnAuthenticator { backend }
215    }
216}
217
218impl<T> WebauthnAuthenticator<T>
219where
220    T: AuthenticatorBackend,
221{
222    /// 5.1.3. Create a New Credential - PublicKeyCredential’s Create (origin, options, sameOriginWithAncestors) Method
223    /// <https://www.w3.org/TR/webauthn/#createCredential>
224    ///
225    /// 6.3.2. The authenticatorMakeCredential Operation
226    /// <https://www.w3.org/TR/webauthn/#op-make-cred>
227    pub fn do_registration(
228        &mut self,
229        origin: Url,
230        options: CreationChallengeResponse,
231        // _same_origin_with_ancestors: bool,
232    ) -> Result<RegisterPublicKeyCredential, WebauthnCError> {
233        // Assert: options.publicKey is present.
234        // This is asserted through rust types.
235
236        // If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
237        // We just don't take this value.
238
239        // Let options be the value of options.publicKey.
240        let options = options.public_key;
241
242        // If the timeout member of options is present, check if its value lies within a reasonable range as defined by the client and if not, correct it to the closest value lying within that range. Set a timer lifetimeTimer to this adjusted value. If the timeout member of options is not present, then set lifetimeTimer to a client-specific default.
243        let timeout_ms = options
244            .timeout
245            .map(|t| if t > 60000 { 60000 } else { t })
246            .unwrap_or(60000);
247
248        // Let callerOrigin be origin. If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.
249        // This is a bit unclear - see https://github.com/w3c/wpub/issues/321.
250        // It may be a browser specific quirk.
251        // https://html.spec.whatwg.org/multipage/origin.html
252        // As a result we don't need to check for our needs.
253
254        // Let effectiveDomain be the callerOrigin’s effective domain. If effective domain is not a valid domain, then return a DOMException whose name is "Security" and terminate this algorithm.
255        let effective_domain = origin
256            .domain()
257            // Checking by IP today muddies things. We'd need a check for rp.id about suffixes
258            // to be different for this.
259            // .or_else(|| caller_origin.host_str())
260            .ok_or(WebauthnCError::Security)
261            .inspect_err(|err| {
262                error!(?err, "origin has no domain or host_str (ip address only?)");
263            })?;
264
265        trace!("effective domain -> {:x?}", effective_domain);
266        trace!("relying party id -> {:x?}", options.rp.id);
267
268        // If options.rp.id
269        //      Is present
270        //          If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "Security", and terminate this algorithm.
271        //      Is not present
272        //          Set options.rp.id to effectiveDomain.
273
274        if !effective_domain.ends_with(&options.rp.id) {
275            error!("relying party id domain is not a suffix of the effective domain.");
276            return Err(WebauthnCError::Security);
277        }
278
279        // Check origin is https:// if effectiveDomain != localhost.
280        if !(effective_domain == "localhost" || origin.scheme() == "https") {
281            error!("An insecure domain or scheme in origin. Must be localhost or https://");
282            return Err(WebauthnCError::Security);
283        }
284
285        self.backend.perform_register(origin, options, timeout_ms)
286    }
287
288    /// <https://www.w3.org/TR/webauthn/#getAssertion>
289    pub fn do_authentication(
290        &mut self,
291        origin: Url,
292        options: RequestChallengeResponse,
293    ) -> Result<PublicKeyCredential, WebauthnCError> {
294        // Assert: options.publicKey is present.
295        // This is asserted through rust types.
296
297        // If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
298        // We just don't take this value.
299
300        // Let options be the value of options.publicKey.
301        let options = options.public_key;
302
303        // If the timeout member of options is present, check if its value lies within a reasonable range as defined by the client and if not, correct it to the closest value lying within that range. Set a timer lifetimeTimer to this adjusted value. If the timeout member of options is not present, then set lifetimeTimer to a client-specific default.
304        let timeout_ms = options
305            .timeout
306            .map(|t| if t > 60000 { 60000 } else { t })
307            .unwrap_or(60000);
308
309        // Let callerOrigin be origin. If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.
310        // This is a bit unclear - see https://github.com/w3c/wpub/issues/321.
311        // It may be a browser specific quirk.
312        // https://html.spec.whatwg.org/multipage/origin.html
313        // As a result we don't need to check for our needs.
314
315        // Let effectiveDomain be the callerOrigin’s effective domain. If effective domain is not a valid domain, then return a DOMException whose name is "Security" and terminate this algorithm.
316        let effective_domain = origin
317            .domain()
318            // Checking by IP today muddies things. We'd need a check for rp.id about suffixes
319            // to be different for this.
320            // .or_else(|| caller_origin.host_str())
321            .ok_or(WebauthnCError::Security)
322            .inspect_err(|err| {
323                error!(?err, "origin has no domain or host_str");
324            })?;
325
326        trace!("effective domain -> {:x?}", effective_domain);
327        trace!("relying party id -> {:x?}", options.rp_id);
328
329        // If options.rp.id
330        //      Is present
331        //          If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "Security", and terminate this algorithm.
332        //      Is not present
333        //          Set options.rp.id to effectiveDomain.
334
335        if !effective_domain.ends_with(&options.rp_id) {
336            error!("relying party id domain is not suffix of effective domain.");
337            return Err(WebauthnCError::Security);
338        }
339
340        // Check origin is https:// if effectiveDomain != localhost.
341        if !(effective_domain == "localhost" || origin.scheme() == "https") {
342            error!("An insecure domain or scheme in origin. Must be localhost or https://");
343            return Err(WebauthnCError::Security);
344        }
345
346        self.backend.perform_auth(origin, options, timeout_ms)
347    }
348}