Files
codex/codex-rs/user-verification/src/native_operation_tests.rs
T
riley-oai e7637306bc Add macOS user verification with Secure Enclave signing (#43624)
## Why

The user-verification API previously returned an unsupported provider on macOS. Enable native verification using biometric-protected credentials.

## What changed

- Implement credential status, creation, reuse, deletion, and challenge signing with P-256 Secure Enclave keys in the Data Protection Keychain.
- Require biometric authentication through the key's access-control policy and use a fresh `LAContext` for each signature.
- Report Touch ID hardware support independently of enrollment and current readiness, and map native failures to structured verification errors.
- Serialize credential operations across processes with cancellable file-lock waits. Invalidate pending authentication on cancellation and wait for the signer to exit before releasing the lock.

## Testing

Add unit tests for lock contention and cancellation, authentication teardown and late-result rejection, native error classification, and validation of Secure Enclave key attributes.

GitOrigin-RevId: 93268962cd824b2befc1a0c54b99dcb77a0f1044
2026-09-08 00:13:28 +00:00

84 lines
2.8 KiB
Rust

//! Exercises prompt teardown and late-result rejection without requiring biometric hardware.
use super::*;
use crate::UserVerificationCancellationReason;
use pretty_assertions::assert_eq;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
#[test]
fn cancellation_tears_down_the_prompt_on_the_context_owner_thread() {
let guard = UserVerificationRequestGuard::default();
let queued = guard.clone();
let (started_tx, started_rx) = mpsc::channel();
let (dismissed_tx, dismissed_rx) = mpsc::channel();
let (exited_tx, exited_rx) = mpsc::channel();
let owner = std::thread::spawn(move || {
let owner_thread = std::thread::current().id();
run_with_cancellation(
&queued,
move || {
started_tx.send(()).expect("authentication started");
dismissed_rx
.recv_timeout(Duration::from_secs(/*secs*/ 5))
.expect("prompt dismissed before the deadline");
exited_tx.send(()).expect("signer exited");
Ok("late signature")
},
|| {
assert_eq!(std::thread::current().id(), owner_thread);
dismissed_tx.send(()).expect("dismiss authentication");
},
)
});
started_rx
.recv_timeout(Duration::from_secs(/*secs*/ 5))
.expect("signer is waiting for authentication");
guard.cancel();
assert_eq!(
owner.join().expect("owner returned"),
Err(UserVerificationError::Cancelled {
reason: UserVerificationCancellationReason::Interrupted,
message: "the verification operation is no longer active".to_string(),
})
);
exited_rx.try_recv().expect("signer exited before return");
}
#[test]
fn completed_operation_does_not_cancel_its_context() {
let cancelled = AtomicBool::new(/*v*/ false);
assert_eq!(
run_with_cancellation(
&UserVerificationRequestGuard::default(),
|| Ok("signature"),
|| cancelled.store(/*val*/ true, Ordering::Release),
),
Ok("signature")
);
assert!(!cancelled.load(Ordering::Acquire));
}
#[test]
fn already_cancelled_operation_never_starts_a_signer() {
let guard = UserVerificationRequestGuard::default();
guard.cancel();
let started = AtomicBool::new(/*v*/ false);
let result = run_with_cancellation(
&guard,
|| {
started.store(/*val*/ true, Ordering::Release);
Ok(())
},
|| panic!("no context needs cancellation"),
);
assert_eq!(
result,
Err(UserVerificationError::Cancelled {
reason: UserVerificationCancellationReason::Interrupted,
message: "the verification operation is no longer active".to_string(),
})
);
assert!(!started.load(Ordering::Acquire));
}