webauthn_rs_proto/
extensions.rs

1//! Extensions allowing certain types of authenticators to provide supplemental information.
2
3use base64urlsafedata::Base64UrlSafeData;
4use serde::{Deserialize, Serialize};
5
6/// Valid credential protection policies
7#[derive(Debug, Serialize, Clone, Copy, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "camelCase")]
9#[repr(u8)]
10pub enum CredentialProtectionPolicy {
11    /// This reflects "FIDO_2_0" semantics. In this configuration, performing
12    /// some form of user verification is optional with or without credentialID
13    /// list. This is the default state of the credential if the extension is
14    /// not specified.
15    UserVerificationOptional = 0x1,
16    /// In this configuration, credential is discovered only when its
17    /// credentialID is provided by the platform or when some form of user
18    /// verification is performed.
19    UserVerificationOptionalWithCredentialIDList = 0x2,
20    /// This reflects that discovery and usage of the credential MUST be
21    /// preceded by some form of user verification.
22    UserVerificationRequired = 0x3,
23}
24
25impl TryFrom<u8> for CredentialProtectionPolicy {
26    type Error = &'static str;
27
28    fn try_from(v: u8) -> Result<Self, Self::Error> {
29        use CredentialProtectionPolicy::*;
30        match v {
31            0x1 => Ok(UserVerificationOptional),
32            0x2 => Ok(UserVerificationOptionalWithCredentialIDList),
33            0x3 => Ok(UserVerificationRequired),
34            _ => Err("Invalid policy number"),
35        }
36    }
37}
38
39/// The desired options for the client's use of the `credProtect` extension
40///
41/// <https://fidoalliance.org/specs/fido-v2.1-rd-20210309/fido-client-to-authenticator-protocol-v2.1-rd-20210309.html#sctn-credProtect-extension>
42#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "camelCase")]
44pub struct CredProtect {
45    /// The credential policy to enact
46    pub credential_protection_policy: CredentialProtectionPolicy,
47    /// Whether it is better for the authenticator to fail to create a
48    /// credential rather than ignore the protection policy
49    /// If no value is provided, the client treats it as `false`.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub enforce_credential_protection_policy: Option<bool>,
52}
53
54/// Extension option inputs for PublicKeyCredentialCreationOptions.
55///
56/// Implements \[AuthenticatorExtensionsClientInputs\] from the spec.
57#[derive(Debug, Serialize, Clone, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct RequestRegistrationExtensions {
60    /// The `credProtect` extension options
61    #[serde(flatten, skip_serializing_if = "Option::is_none")]
62    pub cred_protect: Option<CredProtect>,
63
64    /// ⚠️  - Browsers do not support this!
65    /// Uvm
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub uvm: Option<bool>,
68
69    /// ⚠️  - This extension result is always unsigned, and only indicates if the
70    /// browser *requests* a residentKey to be created. It has no bearing on the
71    /// true rk state of the credential.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub cred_props: Option<bool>,
74
75    /// CTAP2.1 Minumum pin length
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub min_pin_length: Option<bool>,
78
79    /// ⚠️  - Browsers support the *creation* of the secret, but not the retrieval of it.
80    /// CTAP2.1 create hmac secret
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub hmac_create_secret: Option<bool>,
83}
84
85impl Default for RequestRegistrationExtensions {
86    fn default() -> Self {
87        RequestRegistrationExtensions {
88            cred_protect: None,
89            uvm: Some(true),
90            cred_props: Some(true),
91            min_pin_length: None,
92            hmac_create_secret: None,
93        }
94    }
95}
96
97// Unable to create from, because it's an out of crate struct
98#[allow(clippy::from_over_into)]
99#[cfg(feature = "wasm")]
100impl Into<js_sys::Object> for &RequestRegistrationExtensions {
101    fn into(self) -> js_sys::Object {
102        use js_sys::Object;
103        use wasm_bindgen::JsValue;
104
105        let RequestRegistrationExtensions {
106            cred_protect,
107            uvm,
108            cred_props,
109            min_pin_length,
110            hmac_create_secret,
111        } = self;
112
113        let obj = Object::new();
114
115        if let Some(cred_protect) = cred_protect {
116            let jsv = serde_wasm_bindgen::to_value(&cred_protect).unwrap();
117            js_sys::Reflect::set(&obj, &"credProtect".into(), &jsv).unwrap();
118        }
119
120        if let Some(uvm) = uvm {
121            js_sys::Reflect::set(&obj, &"uvm".into(), &JsValue::from_bool(*uvm)).unwrap();
122        }
123
124        if let Some(cred_props) = cred_props {
125            js_sys::Reflect::set(&obj, &"credProps".into(), &JsValue::from_bool(*cred_props))
126                .unwrap();
127        }
128
129        if let Some(min_pin_length) = min_pin_length {
130            js_sys::Reflect::set(
131                &obj,
132                &"minPinLength".into(),
133                &JsValue::from_bool(*min_pin_length),
134            )
135            .unwrap();
136        }
137
138        if let Some(hmac_create_secret) = hmac_create_secret {
139            js_sys::Reflect::set(
140                &obj,
141                &"hmacCreateSecret".into(),
142                &JsValue::from_bool(*hmac_create_secret),
143            )
144            .unwrap();
145        }
146
147        obj
148    }
149}
150
151// ========== Auth exten ============
152
153/// The inputs to the hmac secret if it was created during registration.
154///
155/// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-hmac-secret-extension>
156#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)]
157#[serde(rename_all = "camelCase")]
158pub struct HmacGetSecretInput {
159    /// Retrieve a symmetric secrets from the authenticator with this input.
160    pub output1: Base64UrlSafeData,
161    /// Rotate the secret in the same operation.
162    pub output2: Option<Base64UrlSafeData>,
163}
164
165/// Extension option inputs for PublicKeyCredentialRequestOptions
166///
167/// Implements \[AuthenticatorExtensionsClientInputs\] from the spec
168#[derive(Debug, Serialize, Clone, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct RequestAuthenticationExtensions {
171    /// The `appid` extension options
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub appid: Option<String>,
174
175    /// ⚠️  - Browsers do not support this!
176    /// Uvm
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub uvm: Option<bool>,
179
180    /// ⚠️  - Browsers do not support this!
181    /// <https://bugs.chromium.org/p/chromium/issues/detail?id=1023225>
182    /// Hmac get secret
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub hmac_get_secret: Option<HmacGetSecretInput>,
185}
186
187// Unable to create from, because it's an out of crate struct
188#[allow(clippy::from_over_into)]
189#[cfg(feature = "wasm")]
190impl Into<js_sys::Object> for &RequestAuthenticationExtensions {
191    fn into(self) -> js_sys::Object {
192        use js_sys::{Object, Uint8Array};
193        use wasm_bindgen::JsValue;
194
195        let RequestAuthenticationExtensions {
196            // I don't think we care?
197            appid: _,
198            uvm,
199            hmac_get_secret,
200        } = self;
201
202        let obj = Object::new();
203
204        if let Some(uvm) = uvm {
205            js_sys::Reflect::set(&obj, &"uvm".into(), &JsValue::from_bool(*uvm)).unwrap();
206        }
207
208        if let Some(HmacGetSecretInput { output1, output2 }) = hmac_get_secret {
209            let hmac = Object::new();
210
211            let o1 = Uint8Array::from(output1.as_slice());
212            js_sys::Reflect::set(&hmac, &"output1".into(), &o1).unwrap();
213
214            if let Some(output2) = output2 {
215                let o2 = Uint8Array::from(output2.as_slice());
216                js_sys::Reflect::set(&hmac, &"output2".into(), &o2).unwrap();
217            }
218
219            js_sys::Reflect::set(&obj, &"hmacGetSecret".into(), &hmac).unwrap();
220        }
221
222        obj
223    }
224}
225
226/// The response to a hmac get secret request.
227#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)]
228#[serde(rename_all = "camelCase")]
229pub struct HmacGetSecretOutput {
230    /// Output of HMAC(Salt 1 || Client Secret)
231    pub output1: Base64UrlSafeData,
232    /// Output of HMAC(Salt 2 || Client Secret)
233    pub output2: Option<Base64UrlSafeData>,
234}
235
236/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs>
237/// The default option here for Options are None, so it can be derived
238#[derive(Debug, Deserialize, Serialize, Clone, Default)]
239pub struct AuthenticationExtensionsClientOutputs {
240    /// Indicates whether the client used the provided appid extension
241    #[serde(default)]
242    pub appid: Option<bool>,
243    /// The response to a hmac get secret request.
244    #[serde(default)]
245    pub hmac_get_secret: Option<HmacGetSecretOutput>,
246}
247
248#[cfg(feature = "wasm")]
249impl From<web_sys::AuthenticationExtensionsClientOutputs>
250    for AuthenticationExtensionsClientOutputs
251{
252    fn from(
253        ext: web_sys::AuthenticationExtensionsClientOutputs,
254    ) -> AuthenticationExtensionsClientOutputs {
255        use js_sys::Uint8Array;
256
257        let appid = js_sys::Reflect::get(&ext, &"appid".into())
258            .ok()
259            .and_then(|jv| jv.as_bool());
260
261        let hmac_get_secret = js_sys::Reflect::get(&ext, &"hmacGetSecret".into())
262            .ok()
263            .and_then(|jv| {
264                let output2 = js_sys::Reflect::get(&jv, &"output2".into())
265                    .map(|v| Uint8Array::new(&v).to_vec())
266                    .map(Base64UrlSafeData::from)
267                    .ok();
268
269                let output1 = js_sys::Reflect::get(&jv, &"output1".into())
270                    .map(|v| Uint8Array::new(&v).to_vec())
271                    .map(Base64UrlSafeData::from)
272                    .ok();
273
274                output1.map(|output1| HmacGetSecretOutput { output1, output2 })
275            });
276
277        AuthenticationExtensionsClientOutputs {
278            appid,
279            hmac_get_secret,
280        }
281    }
282}
283
284/// <https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension>
285#[derive(Debug, Deserialize, Serialize, Clone)]
286pub struct CredProps {
287    /// A user agent supplied hint that this credential *may* have created a resident key. It is
288    /// retured from the user agent, not the authenticator meaning that this is an unreliable
289    /// signal.
290    ///
291    /// Note that this extension is UNSIGNED and may have been altered by page javascript.
292    pub rk: bool,
293}
294
295/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs>
296/// The default option here for Options are None, so it can be derived
297#[derive(Debug, Deserialize, Serialize, Clone, Default)]
298#[serde(rename_all = "camelCase")]
299pub struct RegistrationExtensionsClientOutputs {
300    /// Indicates whether the client used the provided appid extension
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub appid: Option<bool>,
303
304    /// Indicates if the client believes it created a resident key. This
305    /// property is managed by the webbrowser, and is NOT SIGNED and CAN NOT be trusted!
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub cred_props: Option<CredProps>,
308
309    /// Indicates if the client successfully applied a HMAC Secret
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub hmac_secret: Option<bool>,
312
313    /// Indicates if the client successfully applied a credential protection policy.
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub cred_protect: Option<CredentialProtectionPolicy>,
316
317    /// Indicates the current minimum PIN length
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub min_pin_length: Option<u32>,
320}
321
322#[cfg(feature = "wasm")]
323impl From<web_sys::AuthenticationExtensionsClientOutputs> for RegistrationExtensionsClientOutputs {
324    fn from(
325        ext: web_sys::AuthenticationExtensionsClientOutputs,
326    ) -> RegistrationExtensionsClientOutputs {
327        let appid = js_sys::Reflect::get(&ext, &"appid".into())
328            .ok()
329            .and_then(|jv| jv.as_bool());
330
331        // Destructure "credProps":{"rk":false} from within a map.
332        let cred_props = js_sys::Reflect::get(&ext, &"credProps".into())
333            .ok()
334            .and_then(|cred_props_struct| {
335                js_sys::Reflect::get(&cred_props_struct, &"rk".into())
336                    .ok()
337                    .and_then(|jv| jv.as_bool())
338                    .map(|rk| CredProps { rk })
339            });
340
341        let hmac_secret = js_sys::Reflect::get(&ext, &"hmac-secret".into())
342            .ok()
343            .and_then(|jv| jv.as_bool());
344
345        let cred_protect = js_sys::Reflect::get(&ext, &"credProtect".into())
346            .ok()
347            .and_then(|jv| jv.as_f64())
348            .and_then(|f| CredentialProtectionPolicy::try_from(f as u8).ok());
349
350        let min_pin_length = js_sys::Reflect::get(&ext, &"minPinLength".into())
351            .ok()
352            .and_then(|jv| jv.as_f64())
353            .map(|f| f as u32);
354
355        RegistrationExtensionsClientOutputs {
356            appid,
357            cred_props,
358            hmac_secret,
359            cred_protect,
360            min_pin_length,
361        }
362    }
363}
364
365/// The result state of an extension as returned from the authenticator.
366#[derive(Clone, Debug, Default, Serialize, Deserialize)]
367pub enum ExtnState<T>
368where
369    T: Clone + std::fmt::Debug,
370{
371    /// This extension was not requested, and so no result was provided.
372    #[default]
373    NotRequested,
374    /// The extension was requested, and the authenticator did NOT act on it.
375    Ignored,
376    /// The extension was requested, and the authenticator correctly responded.
377    Set(T),
378    /// The extension was not requested, and the authenticator sent an unsolicited extension value.
379    Unsolicited(T),
380    /// ⚠️  WARNING: The data in this extension is not signed cryptographically, and can not be
381    /// trusted for security assertions. It MAY be used for UI/UX hints.
382    Unsigned(T),
383}
384
385/// The set of extensions that were registered by this credential.
386#[derive(Clone, Debug, Default, Serialize, Deserialize)]
387pub struct RegisteredExtensions {
388    // ⚠️  It's critical we place serde default here so that we
389    // can deserialise in the future as we add new types!
390    /// The state of the cred_protect extension
391    #[serde(default)]
392    pub cred_protect: ExtnState<CredentialProtectionPolicy>,
393    /// The state of the hmac-secret extension, if it was created
394    #[serde(default)]
395    pub hmac_create_secret: ExtnState<bool>,
396    /// The state of the client appid extensions
397    #[serde(default)]
398    pub appid: ExtnState<bool>,
399    /// The state of the client credential properties extension
400    #[serde(default)]
401    pub cred_props: ExtnState<CredProps>,
402}
403
404impl RegisteredExtensions {
405    /// Yield an empty set of registered extensions
406    pub fn none() -> Self {
407        RegisteredExtensions {
408            cred_protect: ExtnState::NotRequested,
409            hmac_create_secret: ExtnState::NotRequested,
410            appid: ExtnState::NotRequested,
411            cred_props: ExtnState::NotRequested,
412        }
413    }
414}
415
416/// The set of extensions that were provided by the client during authentication
417#[derive(Clone, Debug, Serialize, Deserialize)]
418pub struct AuthenticationExtensions {}