passkey_authenticator/authenticator/extensions/
hmac_secret.rs1use 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#[derive(Debug)]
21pub struct HmacSecretConfig {
22 credentials: HmacSecretCredentialSupport,
23 on_make_credential_support: bool,
27}
28
29impl HmacSecretConfig {
30 pub fn new_with_uv_only() -> Self {
33 Self {
34 credentials: HmacSecretCredentialSupport::WithUvOnly,
35 on_make_credential_support: false,
36 }
37 }
38
39 pub fn new_without_uv() -> Self {
43 Self {
44 credentials: HmacSecretCredentialSupport::WithoutUv,
45 on_make_credential_support: false,
46 }
47 }
48
49 pub fn enable_on_make_credential(mut self) -> Self {
51 self.on_make_credential_support = true;
52 self
53 }
54
55 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#[derive(Debug)]
68pub enum HmacSecretCredentialSupport {
69 WithUvOnly,
71 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 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
172fn 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;