passkey_authenticator/authenticator/extensions/
hmac_secret.rs

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