1use std::sync::{Arc, Condvar, Mutex};
2use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
3
4use anyhow::{Result, anyhow, bail};
5use soft_fido2::{
6 Authenticator, AuthenticatorCallbacks, AuthenticatorConfig, AuthenticatorOptions, Credential,
7 CredentialBackupState, CredentialRef, CtapCommand, Error, UpResult, UvResult,
8};
9
10use crate::application::TouchReceipt;
11use crate::vault::Vault;
12
13const PRESENCE_TIMEOUT: Duration = Duration::from_secs(30);
14const AUC_AAGUID: [u8; 16] = [
16 0x5e, 0x16, 0x05, 0xda, 0x48, 0x1c, 0x5a, 0xb1, 0xa8, 0x8d, 0x5c, 0x33, 0x23, 0x32, 0xe0, 0xb7,
17];
18
19#[derive(Clone)]
20pub struct PresenceGate {
21 shared: Arc<PresenceShared>,
22 timeout: Duration,
23}
24
25struct PresenceShared {
26 state: Mutex<PresenceState>,
27 changed: Condvar,
28}
29
30#[derive(Default)]
31struct PresenceState {
32 command: Option<ActiveCommand>,
33 pending: Option<PendingPresence>,
34}
35
36struct ActiveCommand {
37 channel: u32,
38 cancelled: bool,
39}
40
41struct PendingPresence {
42 channel: u32,
43 operation: String,
44 rp_id: String,
45 deadline: Instant,
46 outcome: Option<PresenceOutcome>,
47}
48
49#[derive(Clone, Copy)]
50enum PresenceOutcome {
51 Accepted,
52 Cancelled,
53}
54
55impl PresenceGate {
56 pub fn new() -> Self {
57 Self::with_timeout(PRESENCE_TIMEOUT)
58 }
59
60 fn with_timeout(timeout: Duration) -> Self {
61 Self {
62 shared: Arc::new(PresenceShared {
63 state: Mutex::new(PresenceState::default()),
64 changed: Condvar::new(),
65 }),
66 timeout,
67 }
68 }
69
70 pub fn begin_command(&self, channel: u32) -> Result<()> {
71 let mut state = self
72 .shared
73 .state
74 .lock()
75 .map_err(|_| anyhow!("auc presence lock was poisoned"))?;
76 if state.command.is_some() {
77 bail!("another CTAP command is already active");
78 }
79 state.command = Some(ActiveCommand {
80 channel,
81 cancelled: false,
82 });
83 Ok(())
84 }
85
86 pub fn finish_command(&self, channel: u32) {
87 if let Ok(mut state) = self.shared.state.lock()
88 && state
89 .command
90 .as_ref()
91 .is_some_and(|command| command.channel == channel)
92 {
93 state.command = None;
94 state.pending = None;
95 self.shared.changed.notify_all();
96 }
97 }
98
99 pub fn cancel(&self, channel: u32) -> bool {
100 let Ok(mut state) = self.shared.state.lock() else {
101 return false;
102 };
103 let Some(command) = state
104 .command
105 .as_mut()
106 .filter(|command| command.channel == channel)
107 else {
108 return false;
109 };
110 command.cancelled = true;
111 if let Some(pending) = state
112 .pending
113 .as_mut()
114 .filter(|pending| pending.channel == channel && pending.outcome.is_none())
115 {
116 pending.outcome = Some(PresenceOutcome::Cancelled);
117 }
118 self.shared.changed.notify_all();
119 true
120 }
121
122 pub fn is_cancelled(&self, channel: u32) -> bool {
123 self.shared
124 .state
125 .lock()
126 .ok()
127 .and_then(|state| {
128 state
129 .command
130 .as_ref()
131 .filter(|command| command.channel == channel)
132 .map(|command| command.cancelled)
133 })
134 .unwrap_or(true)
135 }
136
137 pub fn is_waiting(&self, channel: u32) -> bool {
138 self.shared
139 .state
140 .lock()
141 .ok()
142 .and_then(|state| {
143 state
144 .pending
145 .as_ref()
146 .filter(|pending| pending.channel == channel)
147 .map(|pending| pending.outcome.is_none() && Instant::now() < pending.deadline)
148 })
149 .unwrap_or(false)
150 }
151
152 pub fn has_pending_touch(&self) -> bool {
153 self.shared
154 .state
155 .lock()
156 .ok()
157 .and_then(|state| {
158 state
159 .pending
160 .as_ref()
161 .map(|pending| pending.outcome.is_none() && Instant::now() < pending.deadline)
162 })
163 .unwrap_or(false)
164 }
165
166 pub fn touch(&self) -> Result<TouchReceipt> {
167 let mut state = self
168 .shared
169 .state
170 .lock()
171 .map_err(|_| anyhow!("auc presence lock was poisoned"))?;
172 let pending = state
173 .pending
174 .as_mut()
175 .filter(|pending| pending.outcome.is_none() && Instant::now() < pending.deadline)
176 .ok_or_else(|| anyhow!("no auc operation is waiting for presence"))?;
177 pending.outcome = Some(PresenceOutcome::Accepted);
178 let receipt = TouchReceipt {
179 operation: pending.operation.clone(),
180 rp_id: pending.rp_id.clone(),
181 };
182 self.shared.changed.notify_all();
183 Ok(receipt)
184 }
185
186 fn request(&self, information: &str, rp_id: &str) -> soft_fido2::Result<UpResult> {
187 let mut state = self.shared.state.lock().map_err(|_| Error::Other)?;
188 let channel = match &state.command {
189 Some(command) if !command.cancelled => command.channel,
190 _ => return Ok(UpResult::Denied),
191 };
192 if state.pending.is_some() {
193 return Ok(UpResult::Denied);
194 }
195 let deadline = Instant::now() + self.timeout;
196 state.pending = Some(PendingPresence {
197 channel,
198 operation: operation_label(information).to_string(),
199 rp_id: bounded_rp_id(rp_id),
200 deadline,
201 outcome: None,
202 });
203 loop {
204 let now = Instant::now();
205 let outcome = state
206 .pending
207 .as_ref()
208 .filter(|pending| pending.channel == channel)
209 .and_then(|pending| pending.outcome);
210 match outcome {
211 Some(PresenceOutcome::Accepted) => {
212 state.pending = None;
213 return Ok(UpResult::Accepted);
214 }
215 Some(PresenceOutcome::Cancelled) => {
216 state.pending = None;
217 return Ok(UpResult::Denied);
218 }
219 None if now >= deadline => {
220 state.pending = None;
221 return Ok(UpResult::Timeout);
222 }
223 None => {
224 let wait = deadline.saturating_duration_since(now);
225 let (next, _) = self
226 .shared
227 .changed
228 .wait_timeout(state, wait)
229 .map_err(|_| Error::Other)?;
230 state = next;
231 }
232 }
233 }
234 }
235}
236
237impl Default for PresenceGate {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243pub struct AuthenticatorEngine {
244 authenticator: Authenticator<AucCallbacks>,
245}
246
247impl AuthenticatorEngine {
248 pub fn new(vault: Vault, presence: PresenceGate) -> Result<Self> {
249 let options = AuthenticatorOptions::new()
250 .with_resident_keys(true)
251 .with_user_presence(true)
252 .with_user_verification(Some(false))
253 .with_platform_device(false)
254 .with_client_pin(Some(true))
255 .with_pin_uv_auth_token(Some(true))
256 .with_credential_management(Some(true))
257 .with_biometric_enrollment(Some(false))
258 .with_large_blobs(Some(false))
259 .with_enterprise_attestation(Some(false));
260 let config = AuthenticatorConfig::builder()
261 .aaguid(AUC_AAGUID)
262 .commands(vec![
263 CtapCommand::MakeCredential,
264 CtapCommand::GetAssertion,
265 CtapCommand::GetInfo,
266 CtapCommand::ClientPin,
267 CtapCommand::GetNextAssertion,
268 CtapCommand::CredentialManagement,
269 CtapCommand::Selection,
270 ])
271 .options(options)
272 .max_credentials(4096)
273 .force_resident_keys(true)
274 .constant_sign_count(true)
275 .default_credential_backup_state(CredentialBackupState::Eligible)
276 .algorithms(vec![-7])
277 .device_name("auc software authenticator".to_string())
278 .vendor_id(0x1209)
279 .product_id(0xa0c0)
280 .device_version(0x0001)
281 .max_pin_retries(8)
282 .build();
283 let pin_storage = vault.pin_storage();
284 let callbacks = AucCallbacks { vault, presence };
285 Ok(Self {
286 authenticator: Authenticator::with_config_and_pin_storage(
287 callbacks,
288 config,
289 pin_storage,
290 )
291 .map_err(|_| anyhow!("failed to initialize soft-fido2 authenticator"))?,
292 })
293 }
294
295 pub fn handle(&mut self, request: &[u8]) -> Result<Vec<u8>> {
296 if !request
297 .first()
298 .is_some_and(|command| ALLOWED_COMMANDS.contains(command))
299 {
300 return Ok(vec![soft_fido2::StatusCode::InvalidCommand as u8]);
301 }
302 let mut response = Vec::new();
303 self.authenticator
304 .handle(request, &mut response)
305 .map_err(|_| anyhow!("soft-fido2 command dispatch failed"))?;
306 Ok(response)
307 }
308}
309
310const ALLOWED_COMMANDS: &[u8] = &[0x01, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0b];
311
312struct AucCallbacks {
313 vault: Vault,
314 presence: PresenceGate,
315}
316
317impl AuthenticatorCallbacks for AucCallbacks {
318 fn request_up(
319 &self,
320 information: &str,
321 _user_name: Option<&str>,
322 rp_id: &str,
323 ) -> soft_fido2::Result<UpResult> {
324 self.presence.request(information, rp_id)
325 }
326
327 fn request_uv(
328 &self,
329 _information: &str,
330 _user_name: Option<&str>,
331 _rp_id: &str,
332 ) -> soft_fido2::Result<UvResult> {
333 Ok(UvResult::Denied)
334 }
335
336 fn write_credential(&self, credential: &CredentialRef<'_>) -> soft_fido2::Result<()> {
337 self.vault.write_credential(credential).map_err(vault_error)
338 }
339
340 fn read_credential(&self, credential_id: &[u8]) -> soft_fido2::Result<Option<Credential>> {
341 self.vault
342 .read_credential(credential_id)
343 .map_err(vault_error)
344 }
345
346 fn delete_credential(&self, credential_id: &[u8]) -> soft_fido2::Result<()> {
347 self.vault
348 .delete_credential(credential_id)
349 .map(|_| ())
350 .map_err(vault_error)
351 }
352
353 fn list_credentials(
354 &self,
355 rp_id: &str,
356 user_id: Option<&[u8]>,
357 ) -> soft_fido2::Result<Vec<Credential>> {
358 self.vault
359 .list_credentials(rp_id, user_id)
360 .map_err(vault_error)
361 }
362
363 fn select_credential(
364 &self,
365 _rp_id: &str,
366 credentials: &[Credential],
367 ) -> soft_fido2::Result<usize> {
368 if credentials.is_empty() {
369 Err(Error::Other)
370 } else {
371 Ok(0)
372 }
373 }
374
375 fn enumerate_rps(&self) -> soft_fido2::Result<Vec<(String, Option<String>, usize)>> {
376 self.vault.enumerate_rps().map_err(vault_error)
377 }
378
379 fn credential_count(&self) -> soft_fido2::Result<usize> {
380 self.vault.credential_count().map_err(vault_error)
381 }
382
383 fn get_timestamp_ms(&self) -> u64 {
384 SystemTime::now()
385 .duration_since(UNIX_EPOCH)
386 .map(|duration| duration.as_millis() as u64)
387 .unwrap_or(0)
388 }
389}
390
391fn vault_error(error: anyhow::Error) -> Error {
392 eprintln!("auc vault operation failed: {error:#}");
393 Error::Other
394}
395
396fn operation_label(information: &str) -> &'static str {
397 let information = information.to_ascii_lowercase();
398 if information.contains("registration") || information.contains("make credential") {
399 "register passkey"
400 } else if information.contains("authentication") || information.contains("assertion") {
401 "authenticate"
402 } else {
403 "confirm authenticator operation"
404 }
405}
406
407fn bounded_rp_id(rp_id: &str) -> String {
408 rp_id.chars().take(253).collect()
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn early_touch_is_never_cached_and_touch_is_consumed_once() {
417 let gate = PresenceGate::with_timeout(Duration::from_secs(1));
418 assert!(gate.touch().is_err());
419 gate.begin_command(7).unwrap();
420 let callback = gate.clone();
421 let thread = std::thread::spawn(move || callback.request("authentication", "example.test"));
422 while !gate.has_pending_touch() {
423 std::thread::yield_now();
424 }
425 let receipt = gate.touch().unwrap();
426 assert_eq!(receipt.operation, "authenticate");
427 assert_eq!(receipt.rp_id, "example.test");
428 assert!(gate.touch().is_err());
429 assert_eq!(thread.join().unwrap().unwrap(), UpResult::Accepted);
430 gate.finish_command(7);
431 }
432
433 #[test]
434 fn cancellation_wakes_presence_wait_immediately() {
435 let gate = PresenceGate::with_timeout(Duration::from_secs(10));
436 gate.begin_command(9).unwrap();
437 let callback = gate.clone();
438 let thread = std::thread::spawn(move || callback.request("registration", "example.test"));
439 while !gate.has_pending_touch() {
440 std::thread::yield_now();
441 }
442 assert!(gate.cancel(9));
443 assert_eq!(thread.join().unwrap().unwrap(), UpResult::Denied);
444 assert!(!gate.has_pending_touch());
445 gate.finish_command(9);
446 }
447
448 #[test]
449 fn operation_labels_do_not_echo_arbitrary_core_text() {
450 assert_eq!(
451 operation_label("Make Credential registration"),
452 "register passkey"
453 );
454 assert_eq!(operation_label("get assertion"), "authenticate");
455 assert_eq!(
456 operation_label("attacker controlled detail"),
457 "confirm authenticator operation"
458 );
459 }
460}