Skip to main content

passkey_authenticator/authenticator/extensions/
hmac_secret.rs

1use std::ops::Not;
2
3use passkey_crypto::{CryptoBackend, hash::Sha256Backend, rng::RngBackend};
4use passkey_types::ctap2::{
5    Ctap2Error, StatusCode,
6    extensions::{
7        AuthenticatorPrfGetOutputs, AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs,
8        AuthenticatorPrfValues, HmacSecretSaltOrOutput,
9    },
10};
11
12use crate::Authenticator;
13
14/// Logical module for configuring the [hmac-secret] authenticator extension.
15///
16/// [hmac-secret]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-hmac-secret-extension
17#[derive(Debug)]
18pub struct HmacSecretConfig {
19    credentials: HmacSecretCredentialSupport,
20    /// Extension to retrieve a symmetric secret from the authenticator during registration.
21    ///
22    /// In the spec this is support for `hmac-secret-mc`
23    on_make_credential_support: bool,
24}
25
26impl HmacSecretConfig {
27    /// Create a new configuration which only supports creating credentials
28    /// gated by UV
29    pub fn new_with_uv_only() -> Self {
30        Self {
31            credentials: HmacSecretCredentialSupport::WithUvOnly,
32            on_make_credential_support: false,
33        }
34    }
35
36    /// Create a new configuration which supports creating 2 credentials:
37    /// 1. Gated by UV
38    /// 2. Not protected by UV, and only needs UP
39    pub fn new_without_uv() -> Self {
40        Self {
41            credentials: HmacSecretCredentialSupport::WithoutUv,
42            on_make_credential_support: false,
43        }
44    }
45
46    /// Enable support for returning the hmac-secret values on credential creation
47    pub fn enable_on_make_credential(mut self) -> Self {
48        self.on_make_credential_support = true;
49        self
50    }
51
52    /// Check whether this configuration supports `hmac-secret-mc`,
53    /// meaning it supports retrieving the symmetric secret on credential creation.
54    pub fn hmac_secret_mc(&self) -> bool {
55        self.on_make_credential_support
56    }
57
58    fn supports_no_uv(&self) -> bool {
59        self.credentials.without_uv()
60    }
61}
62
63/// Set whether the Hmac Secret generation supports one or two credentials
64#[derive(Debug)]
65pub enum HmacSecretCredentialSupport {
66    /// Only support one credential, which is necessarily backed by User Verification
67    WithUvOnly,
68    /// Support 2 credentials, where the second one is not protected by User Verification
69    WithoutUv,
70}
71
72impl HmacSecretCredentialSupport {
73    fn without_uv(&self) -> bool {
74        match self {
75            HmacSecretCredentialSupport::WithUvOnly => false,
76            HmacSecretCredentialSupport::WithoutUv => true,
77        }
78    }
79}
80
81impl<S, U, C> Authenticator<S, U, C>
82where
83    C: CryptoBackend,
84{
85    pub(super) fn make_hmac_secret(
86        &self,
87        hmac_secret_request: Option<bool>,
88    ) -> Option<passkey_types::StoredHmacSecret> {
89        let config = self.extensions.hmac_secret.as_ref()?;
90
91        // The spec recommends to still generate and associate credentials regardless of the request
92        // but in that case we might be storing things that won't be used. I'd rather wait an see
93        // if theres really cases for this.
94        if hmac_secret_request.is_some_and(|b| b).not() {
95            return None;
96        }
97
98        Some(passkey_types::StoredHmacSecret {
99            cred_with_uv: <C as CryptoBackend>::Rng::random_vec(32),
100            cred_without_uv: config
101                .credentials
102                .without_uv()
103                .then(|| <C as CryptoBackend>::Rng::random_vec(32)),
104        })
105    }
106
107    pub(super) fn make_prf(
108        &self,
109        passkey_ext: Option<&passkey_types::StoredHmacSecret>,
110        request: AuthenticatorPrfInputs,
111        uv: bool,
112    ) -> Result<Option<AuthenticatorPrfMakeOutputs>, StatusCode> {
113        let Some(ref config) = self.extensions.hmac_secret else {
114            return Ok(None);
115        };
116
117        let Some(creds) = passkey_ext else {
118            return Ok(Some(AuthenticatorPrfMakeOutputs {
119                enabled: false,
120                results: None,
121            }));
122        };
123
124        let results = config
125            .on_make_credential_support
126            .then(|| {
127                request.eval.map(|eval| {
128                    let request = HmacSecretSaltOrOutput::new(eval.first, eval.second);
129
130                    Self::calculate_hmac_secret(creds, request, config, uv)
131                })
132            })
133            .flatten()
134            .transpose()?;
135
136        Ok(Some(AuthenticatorPrfMakeOutputs {
137            enabled: true,
138            results: results.map(|shared_secrets| AuthenticatorPrfValues {
139                first: shared_secrets.first().try_into().unwrap(),
140                second: shared_secrets.second().map(|b| b.try_into().unwrap()),
141            }),
142        }))
143    }
144
145    pub(super) fn get_prf(
146        &self,
147        credential_id: &[u8],
148        passkey_ext: Option<&passkey_types::StoredHmacSecret>,
149        salts: AuthenticatorPrfInputs,
150        uv: bool,
151    ) -> Result<Option<AuthenticatorPrfGetOutputs>, StatusCode> {
152        let Some(ref config) = self.extensions.hmac_secret else {
153            return Ok(None);
154        };
155
156        let Some(hmac_creds) = passkey_ext else {
157            return Ok(None);
158        };
159
160        let Some(request) = select_salts(credential_id, salts) else {
161            return Ok(None);
162        };
163
164        let results = Self::calculate_hmac_secret(hmac_creds, request, config, uv)?;
165
166        Ok(Some(AuthenticatorPrfGetOutputs {
167            results: AuthenticatorPrfValues {
168                first: results.first().try_into().unwrap(),
169                second: results.second().map(|b| b.try_into().unwrap()),
170            },
171        }))
172    }
173
174    /// Calculates the Hmac secret output given the stored credentials and given salts.
175    ///
176    /// ## Process
177    /// * The authenticator chooses which CredRandom to use for next step based on whether user verification was done or not in above steps.
178    ///   * If uv bit is set to `true` in the response, let CredRandom be [`passkey_types::StoredHmacSecret::cred_with_uv`].
179    ///   * If uv bit is set to `false` in the response, let CredRandom be [`passkey_types::StoredHmacSecret::cred_without_uv`].
180    /// * If the authenticator cannot find corresponding CredRandom associated with the credential, authenticator ignores this extension and does not add any response from this extension to "extensions" field of the authenticatorGetAssertion response.
181    /// * The authenticator generates one or two HMAC-SHA-256 values, depending upon whether it received one salt (32 bytes) or two salts (64 bytes):
182    ///   * output1: `HMAC-SHA-256(CredRandom, salt1)`
183    ///   * output2: `HMAC-SHA-256(CredRandom, salt2)`
184    ///
185    /// <https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-hmac-secret-extension>
186    fn calculate_hmac_secret(
187        hmac_creds: &passkey_types::StoredHmacSecret,
188        salts: HmacSecretSaltOrOutput,
189        config: &HmacSecretConfig,
190        uv: bool,
191    ) -> Result<HmacSecretSaltOrOutput, StatusCode> {
192        let cred_random = if uv {
193            &hmac_creds.cred_with_uv
194        } else {
195            config
196                .supports_no_uv()
197                .then_some(hmac_creds.cred_without_uv.as_ref())
198                .flatten()
199                .ok_or(Ctap2Error::UserVerificationBlocked)?
200        };
201
202        let output1 = C::Sha256::hmac_sha256(cred_random, salts.first());
203        let output2 = salts
204            .second()
205            .map(|salt2| C::Sha256::hmac_sha256(cred_random, salt2));
206
207        let result = HmacSecretSaltOrOutput::new(output1, output2);
208
209        Ok(result)
210    }
211}
212
213fn select_salts(
214    credential_id: &[u8],
215    request: AuthenticatorPrfInputs,
216) -> Option<HmacSecretSaltOrOutput> {
217    if let Some(eval_by_cred) = request.eval_by_credential {
218        let eval = eval_by_cred
219            .into_iter()
220            .find(|(key, _)| key.as_slice() == credential_id);
221        if let Some((_, eval)) = eval {
222            return Some(HmacSecretSaltOrOutput::new(eval.first, eval.second));
223        }
224    }
225
226    let eval = request.eval?;
227
228    Some(HmacSecretSaltOrOutput::new(eval.first, eval.second))
229}
230
231#[cfg(test)]
232pub mod tests;