passkey_authenticator/authenticator/
extensions.rs

1//! The authenticator extensions as defined in [CTAP2 Defined Extensions][ctap2] or in
2//! [WebAuthn Defined Extensions][webauthn].
3//!
4//! The currently supported extensions are:
5//! * [`HmacSecret`][HmacSecretConfig]
6//! * [AuthenticatorDisplayName]
7//!
8//! [ctap2]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-defined-extensions
9//! [webauthn]: https://w3c.github.io/webauthn/#sctn-defined-extensions
10//! [AuthenticatorDisplayName]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname
11
12use passkey_types::ctap2::{StatusCode, get_assertion, get_info, make_credential};
13
14mod hmac_secret;
15pub use hmac_secret::{HmacSecretConfig, HmacSecretCredentialSupport};
16
17#[cfg(test)]
18pub(crate) use hmac_secret::tests::prf_eval_request;
19
20#[cfg(doc)]
21use passkey_types::webauthn;
22
23use crate::{Authenticator, passkey::PasskeyAccessor};
24
25#[derive(Debug, Default)]
26#[non_exhaustive]
27pub(super) struct Extensions {
28    /// Extension to retrieve a symmetric secret from the authenticator.
29    pub hmac_secret: Option<HmacSecretConfig>,
30}
31
32impl Extensions {
33    /// Get a list of extensions that are currently supported by this instance.
34    pub fn list_extensions(&self) -> Option<Vec<get_info::Extension>> {
35        // We don't support Pin UV auth yet so we will only support the unsigned prf extension
36        let prf = self
37            .hmac_secret
38            .is_some()
39            .then_some(get_info::Extension::Prf);
40
41        prf.map(|ext| vec![ext])
42    }
43}
44
45pub(super) struct MakeExtensionOutputs {
46    pub signed: Option<make_credential::SignedExtensionOutputs>,
47    pub unsigned: Option<make_credential::UnsignedExtensionOutputs>,
48    pub credential: passkey_types::CredentialExtensions,
49}
50
51#[derive(Default)]
52pub(super) struct GetExtensionOutputs {
53    pub signed: Option<get_assertion::SignedExtensionOutputs>,
54    pub unsigned: Option<get_assertion::UnsignedExtensionOutputs>,
55}
56
57impl<S, U> Authenticator<S, U> {
58    pub(super) fn make_extensions(
59        &self,
60        request: Option<make_credential::ExtensionInputs>,
61        uv: bool,
62    ) -> Result<MakeExtensionOutputs, StatusCode> {
63        let request = request.and_then(|r| r.zip_contents());
64        let pk_extensions = self.make_passkey_extensions(request.as_ref());
65
66        let prf = request
67            .and_then(|ext| {
68                ext.prf.and_then(|input| {
69                    self.make_prf(pk_extensions.hmac_secret.as_ref(), input, uv)
70                        .transpose()
71                })
72            })
73            .transpose()?;
74
75        Ok(MakeExtensionOutputs {
76            signed: None,
77            unsigned: make_credential::UnsignedExtensionOutputs { prf }.zip_contents(),
78            credential: pk_extensions,
79        })
80    }
81
82    fn make_passkey_extensions(
83        &self,
84        request: Option<&make_credential::ExtensionInputs>,
85    ) -> passkey_types::CredentialExtensions {
86        let should_build_hmac_secret =
87            request.and_then(|r| r.hmac_secret.or(Some(r.prf.is_some())));
88        let hmac_secret = self.make_hmac_secret(should_build_hmac_secret);
89
90        passkey_types::CredentialExtensions { hmac_secret }
91    }
92
93    pub(super) fn get_extensions<P>(
94        &self,
95        passkey: &P,
96        request: Option<get_assertion::ExtensionInputs>,
97        uv: bool,
98    ) -> Result<GetExtensionOutputs, StatusCode>
99    where
100        P: PasskeyAccessor,
101    {
102        let Some(ext) = request.and_then(get_assertion::ExtensionInputs::zip_contents) else {
103            return Ok(Default::default());
104        };
105
106        let prf = ext
107            .prf
108            .and_then(|salts| {
109                self.get_prf(
110                    passkey.credential_id(),
111                    passkey.extensions().hmac_secret.as_ref(),
112                    salts,
113                    uv,
114                )
115                .transpose()
116            })
117            .transpose()?;
118
119        Ok(GetExtensionOutputs {
120            signed: None,
121            unsigned: get_assertion::UnsignedExtensionOutputs { prf }.zip_contents(),
122        })
123    }
124}