passkey_authenticator/authenticator/extensions/
hmac_secret.rs1use 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#[derive(Debug)]
18pub struct HmacSecretConfig {
19 credentials: HmacSecretCredentialSupport,
20 on_make_credential_support: bool,
24}
25
26impl HmacSecretConfig {
27 pub fn new_with_uv_only() -> Self {
30 Self {
31 credentials: HmacSecretCredentialSupport::WithUvOnly,
32 on_make_credential_support: false,
33 }
34 }
35
36 pub fn new_without_uv() -> Self {
40 Self {
41 credentials: HmacSecretCredentialSupport::WithoutUv,
42 on_make_credential_support: false,
43 }
44 }
45
46 pub fn enable_on_make_credential(mut self) -> Self {
48 self.on_make_credential_support = true;
49 self
50 }
51
52 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#[derive(Debug)]
65pub enum HmacSecretCredentialSupport {
66 WithUvOnly,
68 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 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 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;