blob: 187256b7d55be2bc237268884a68ad90677fba16 [file] [log] [blame]
David Drysdalef7ed95a2024-05-08 13:51:45 +01001// Copyright 2024, The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Tests for user authentication interactions (via `IKeystoreAuthorization`).
16
David Drysdale89e87d52024-10-04 13:07:43 +010017use crate::keystore2_client_test_utils::{BarrierReached, BarrierReachedWithData};
David Drysdalef7ed95a2024-05-08 13:51:45 +010018use android_security_authorization::aidl::android::security::authorization::{
19 IKeystoreAuthorization::IKeystoreAuthorization
20};
21use android_security_maintenance::aidl::android::security::maintenance::IKeystoreMaintenance::{
22 IKeystoreMaintenance,
23};
24use android_hardware_security_keymint::aidl::android::hardware::security::keymint::{
David Drysdale89e87d52024-10-04 13:07:43 +010025 Algorithm::Algorithm, Digest::Digest, EcCurve::EcCurve, ErrorCode::ErrorCode,
26 HardwareAuthToken::HardwareAuthToken, HardwareAuthenticatorType::HardwareAuthenticatorType,
27 KeyPurpose::KeyPurpose, SecurityLevel::SecurityLevel,
David Drysdalef7ed95a2024-05-08 13:51:45 +010028};
David Drysdaleacd3d982024-10-04 16:18:16 +010029use android_hardware_gatekeeper::aidl::android::hardware::gatekeeper::{
30 IGatekeeper::IGatekeeper, IGatekeeper::ERROR_RETRY_TIMEOUT,
31};
David Drysdalef7ed95a2024-05-08 13:51:45 +010032use android_system_keystore2::aidl::android::system::keystore2::{
33 CreateOperationResponse::CreateOperationResponse, Domain::Domain, KeyDescriptor::KeyDescriptor,
34 KeyMetadata::KeyMetadata,
35};
David Drysdaleacd3d982024-10-04 16:18:16 +010036use android_system_keystore2::binder::{ExceptionCode, Result as BinderResult};
David Drysdalef7ed95a2024-05-08 13:51:45 +010037use android_hardware_security_secureclock::aidl::android::hardware::security::secureclock::{
38 Timestamp::Timestamp,
39};
David Drysdale9f0007e2024-10-04 18:58:11 +010040use anyhow::Context;
David Drysdalef7ed95a2024-05-08 13:51:45 +010041use keystore2_test_utils::{
David Drysdale9f0007e2024-10-04 18:58:11 +010042 authorizations::AuthSetBuilder, expect, get_keystore_service, run_as,
43 run_as::{ChannelReader, ChannelWriter}, expect_km_error,
David Drysdalef7ed95a2024-05-08 13:51:45 +010044};
45use log::{warn, info};
David Drysdalef7ed95a2024-05-08 13:51:45 +010046use rustutils::users::AID_USER_OFFSET;
David Drysdale89e87d52024-10-04 13:07:43 +010047use std::{time::Duration, thread::sleep};
David Drysdalef7ed95a2024-05-08 13:51:45 +010048
49/// Test user ID.
50const TEST_USER_ID: i32 = 100;
David Drysdale89e87d52024-10-04 13:07:43 +010051/// Corresponding uid value.
52const UID: u32 = TEST_USER_ID as u32 * AID_USER_OFFSET + 1001;
53/// Fake synthetic password blob.
54static SYNTHETIC_PASSWORD: &[u8] = &[
David Drysdalef7ed95a2024-05-08 13:51:45 +010055 0x42, 0x39, 0x30, 0x37, 0x44, 0x37, 0x32, 0x37, 0x39, 0x39, 0x43, 0x42, 0x39, 0x41, 0x42, 0x30,
56 0x34, 0x31, 0x30, 0x38, 0x46, 0x44, 0x33, 0x45, 0x39, 0x42, 0x32, 0x38, 0x36, 0x35, 0x41, 0x36,
57 0x33, 0x44, 0x42, 0x42, 0x43, 0x36, 0x33, 0x42, 0x34, 0x39, 0x37, 0x33, 0x35, 0x45, 0x41, 0x41,
58 0x32, 0x45, 0x31, 0x35, 0x43, 0x43, 0x46, 0x32, 0x39, 0x36, 0x33, 0x34, 0x31, 0x32, 0x41, 0x39,
59];
David Drysdaleacd3d982024-10-04 16:18:16 +010060/// Gatekeeper password.
61static GK_PASSWORD: &[u8] = b"correcthorsebatterystaple";
David Drysdalef7ed95a2024-05-08 13:51:45 +010062/// Fake SID value corresponding to Gatekeeper.
David Drysdale89e87d52024-10-04 13:07:43 +010063static GK_FAKE_SID: i64 = 123456;
David Drysdalef7ed95a2024-05-08 13:51:45 +010064/// Fake SID value corresponding to a biometric authenticator.
David Drysdale89e87d52024-10-04 13:07:43 +010065static BIO_FAKE_SID1: i64 = 345678;
David Drysdalef7ed95a2024-05-08 13:51:45 +010066/// Fake SID value corresponding to a biometric authenticator.
David Drysdale89e87d52024-10-04 13:07:43 +010067static BIO_FAKE_SID2: i64 = 456789;
David Drysdalef7ed95a2024-05-08 13:51:45 +010068
69const WEAK_UNLOCK_ENABLED: bool = true;
70const WEAK_UNLOCK_DISABLED: bool = false;
71const UNFORCED: bool = false;
72
73fn get_authorization() -> binder::Strong<dyn IKeystoreAuthorization> {
74 binder::get_interface("android.security.authorization").unwrap()
75}
76
77fn get_maintenance() -> binder::Strong<dyn IKeystoreMaintenance> {
78 binder::get_interface("android.security.maintenance").unwrap()
79}
80
David Drysdaleacd3d982024-10-04 16:18:16 +010081/// Get the default Gatekeeper instance. This may fail on older devices where Gatekeeper is still a
82/// HIDL interface rather than AIDL.
83fn get_gatekeeper() -> Option<binder::Strong<dyn IGatekeeper>> {
84 binder::get_interface("android.hardware.gatekeeper.IGatekeeper/default").ok()
85}
86
87/// Indicate whether a Gatekeeper result indicates a delayed-retry is needed.
88fn is_gk_retry<T: std::fmt::Debug>(result: &BinderResult<T>) -> bool {
89 matches!(result, Err(s) if s.exception_code() == ExceptionCode::SERVICE_SPECIFIC
90 && s.service_specific_error() == ERROR_RETRY_TIMEOUT)
91}
92
David Drysdalef7ed95a2024-05-08 13:51:45 +010093fn abort_op(result: binder::Result<CreateOperationResponse>) {
94 if let Ok(rsp) = result {
95 if let Some(op) = rsp.iOperation {
96 if let Err(e) = op.abort() {
97 warn!("abort op failed: {e:?}");
98 }
99 } else {
100 warn!("can't abort op with missing iOperation");
101 }
102 } else {
103 warn!("can't abort failed op: {result:?}");
104 }
105}
106
107/// RAII structure to ensure that test users are removed at the end of a test.
108struct TestUser {
109 id: i32,
110 maint: binder::Strong<dyn IKeystoreMaintenance>,
David Drysdaleacd3d982024-10-04 16:18:16 +0100111 gk: Option<binder::Strong<dyn IGatekeeper>>,
112 gk_sid: Option<i64>,
113 gk_handle: Vec<u8>,
David Drysdalef7ed95a2024-05-08 13:51:45 +0100114}
115
116impl TestUser {
117 fn new() -> Self {
David Drysdale89e87d52024-10-04 13:07:43 +0100118 Self::new_user(TEST_USER_ID, SYNTHETIC_PASSWORD)
David Drysdalef7ed95a2024-05-08 13:51:45 +0100119 }
David Drysdaleacd3d982024-10-04 16:18:16 +0100120 fn new_user(user_id: i32, sp: &[u8]) -> Self {
David Drysdalef7ed95a2024-05-08 13:51:45 +0100121 let maint = get_maintenance();
122 maint.onUserAdded(user_id).expect("failed to add test user");
123 maint
David Drysdaleacd3d982024-10-04 16:18:16 +0100124 .initUserSuperKeys(user_id, sp, /* allowExisting= */ false)
David Drysdalef7ed95a2024-05-08 13:51:45 +0100125 .expect("failed to init test user");
David Drysdaleacd3d982024-10-04 16:18:16 +0100126 let gk = get_gatekeeper();
127 let (gk_sid, gk_handle) = if let Some(gk) = &gk {
128 // AIDL Gatekeeper is available, so enroll a password.
129 loop {
130 let result = gk.enroll(user_id, &[], &[], GK_PASSWORD);
131 if is_gk_retry(&result) {
132 sleep(Duration::from_secs(1));
133 continue;
134 }
135 let rsp = result.expect("gk.enroll() failed");
136 info!("registered test user {user_id} as sid {} with GK", rsp.secureUserId);
137 break (Some(rsp.secureUserId), rsp.data);
138 }
139 } else {
140 (None, vec![])
141 };
142 Self { id: user_id, maint, gk, gk_sid, gk_handle }
143 }
144
145 /// Perform Gatekeeper verification, which will return a HAT on success.
146 fn gk_verify(&self, challenge: i64) -> Option<HardwareAuthToken> {
147 let Some(gk) = &self.gk else { return None };
148 loop {
149 let result = gk.verify(self.id, challenge, &self.gk_handle, GK_PASSWORD);
150 if is_gk_retry(&result) {
151 sleep(Duration::from_secs(1));
152 continue;
153 }
154 let rsp = result.expect("gk.verify failed");
155 break Some(rsp.hardwareAuthToken);
156 }
David Drysdalef7ed95a2024-05-08 13:51:45 +0100157 }
158}
159
160impl Drop for TestUser {
161 fn drop(&mut self) {
162 let _ = self.maint.onUserRemoved(self.id);
David Drysdaleacd3d982024-10-04 16:18:16 +0100163 if let Some(gk) = &self.gk {
164 info!("deregister test user {} with GK", self.id);
165 if let Err(e) = gk.deleteUser(self.id) {
166 warn!("failed to deregister test user {}: {e:?}", self.id);
167 }
168 }
David Drysdalef7ed95a2024-05-08 13:51:45 +0100169 }
170}
171
172#[test]
David Drysdaleacd3d982024-10-04 16:18:16 +0100173fn test_auth_bound_timeout_with_gk() {
174 type Barrier = BarrierReachedWithData<Option<i64>>;
175 android_logger::init_once(
176 android_logger::Config::default()
177 .with_tag("keystore2_client_tests")
178 .with_max_level(log::LevelFilter::Debug),
179 );
180
181 let child_fn = move |reader: &mut ChannelReader<Barrier>,
182 writer: &mut ChannelWriter<Barrier>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100183 -> Result<(), run_as::Error> {
David Drysdaleacd3d982024-10-04 16:18:16 +0100184 // Now we're in a new process, wait to be notified before starting.
185 let gk_sid: i64 = match reader.recv().0 {
186 Some(sid) => sid,
187 None => {
188 // There is no AIDL Gatekeeper available, so abandon the test. It would be nice to
189 // know this before starting the child process, but finding it out requires Binder,
190 // which can't be used until after the child has forked.
191 return Ok(());
192 }
193 };
194
195 // Action A: create a new auth-bound key which requires auth in the last 3 seconds,
196 // and fail to start an operation using it.
197 let ks2 = get_keystore_service();
David Drysdale9f0007e2024-10-04 18:58:11 +0100198 let sec_level =
199 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100200 let params = AuthSetBuilder::new()
201 .user_secure_id(gk_sid)
202 .user_secure_id(BIO_FAKE_SID1)
203 .user_secure_id(BIO_FAKE_SID2)
204 .user_auth_type(HardwareAuthenticatorType::ANY)
205 .auth_timeout(3)
206 .algorithm(Algorithm::EC)
207 .purpose(KeyPurpose::SIGN)
208 .purpose(KeyPurpose::VERIFY)
209 .digest(Digest::SHA_2_256)
210 .ec_curve(EcCurve::P_256);
211
212 let KeyMetadata { key, .. } = sec_level
213 .generateKey(
214 &KeyDescriptor {
215 domain: Domain::APP,
216 nspace: -1,
217 alias: Some("auth-bound-timeout".to_string()),
218 blob: None,
219 },
220 None,
221 &params,
222 0,
223 b"entropy",
224 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100225 .context("key generation failed")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100226 info!("A: created auth-timeout key {key:?}");
227
228 // No HATs so cannot create an operation using the key.
229 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
230 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100231 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdaleacd3d982024-10-04 16:18:16 +0100232 info!("A: failed auth-bound operation (no HAT) as expected {result:?}");
233
234 writer.send(&Barrier::new(None)); // A done.
235
236 // Action B: succeed when a valid HAT is available.
237 reader.recv();
238
239 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100240 expect!(result.is_ok());
241 let op = result.unwrap().iOperation.context("no operation in result")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100242 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100243 expect!(result.is_ok());
David Drysdaleacd3d982024-10-04 16:18:16 +0100244 info!("B: performed auth-bound operation (with valid GK HAT) as expected");
245
246 writer.send(&Barrier::new(None)); // B done.
247
248 // Action C: fail again when the HAT is old enough to not even be checked.
249 reader.recv();
250 info!("C: wait so that any HAT times out");
251 sleep(Duration::from_secs(4));
252 let result = sec_level.createOperation(&key, &params, UNFORCED);
253 info!("C: failed auth-bound operation (HAT is too old) as expected {result:?}");
254 writer.send(&Barrier::new(None)); // C done.
255
256 Ok(())
257 };
258
259 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
260 // `--test-threads=1`), and nothing yet done with binder.
261 let mut child_handle = unsafe {
262 // Perform keystore actions while running as the test user.
David Drysdale33b7a622024-10-23 17:17:11 +0100263 run_as::run_as_child_app(UID, UID, child_fn)
David Drysdaleacd3d982024-10-04 16:18:16 +0100264 }
265 .unwrap();
266
267 // Now that the separate process has been forked off, it's safe to use binder to setup a test
268 // user.
269 let _ks2 = get_keystore_service();
270 let user = TestUser::new();
271 if user.gk.is_none() {
272 // Can't run this test if there's no AIDL Gatekeeper.
273 child_handle.send(&Barrier::new(None));
274 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
275 return;
276 }
277 let user_id = user.id;
278 let auth_service = get_authorization();
279
280 // Lock and unlock to ensure super keys are already created.
281 auth_service
282 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
283 .unwrap();
284 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
285
286 info!("trigger child process action A and wait for completion");
287 child_handle.send(&Barrier::new(Some(user.gk_sid.unwrap())));
David Drysdale9f0007e2024-10-04 18:58:11 +0100288 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100289
290 // Unlock with GK password to get a genuine auth token.
291 let real_hat = user.gk_verify(0).expect("failed to perform GK verify");
292 auth_service.addAuthToken(&real_hat).unwrap();
293
294 info!("trigger child process action B and wait for completion");
295 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100296 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100297
298 info!("trigger child process action C and wait for completion");
299 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100300 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100301
302 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
303}
304
305#[test]
David Drysdale89e87d52024-10-04 13:07:43 +0100306fn test_auth_bound_timeout_failure() {
David Drysdalef7ed95a2024-05-08 13:51:45 +0100307 android_logger::init_once(
308 android_logger::Config::default()
309 .with_tag("keystore2_client_tests")
310 .with_max_level(log::LevelFilter::Debug),
311 );
David Drysdalef7ed95a2024-05-08 13:51:45 +0100312
David Drysdale89e87d52024-10-04 13:07:43 +0100313 let child_fn = move |reader: &mut ChannelReader<BarrierReached>,
314 writer: &mut ChannelWriter<BarrierReached>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100315 -> Result<(), run_as::Error> {
David Drysdale89e87d52024-10-04 13:07:43 +0100316 // Now we're in a new process, wait to be notified before starting.
317 reader.recv();
318
319 // Action A: create a new auth-bound key which requires auth in the last 3 seconds,
320 // and fail to start an operation using it.
321 let ks2 = get_keystore_service();
322
David Drysdale9f0007e2024-10-04 18:58:11 +0100323 let sec_level =
324 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100325 let params = AuthSetBuilder::new()
326 .user_secure_id(BIO_FAKE_SID1)
327 .user_secure_id(BIO_FAKE_SID2)
328 .user_auth_type(HardwareAuthenticatorType::ANY)
329 .auth_timeout(3)
330 .algorithm(Algorithm::EC)
331 .purpose(KeyPurpose::SIGN)
332 .purpose(KeyPurpose::VERIFY)
333 .digest(Digest::SHA_2_256)
334 .ec_curve(EcCurve::P_256);
335
336 let KeyMetadata { key, .. } = sec_level
337 .generateKey(
338 &KeyDescriptor {
339 domain: Domain::APP,
340 nspace: -1,
341 alias: Some("auth-bound-timeout".to_string()),
342 blob: None,
343 },
344 None,
345 &params,
346 0,
347 b"entropy",
348 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100349 .context("key generation failed")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100350 info!("A: created auth-timeout key {key:?}");
351
352 // No HATs so cannot create an operation using the key.
353 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
354 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100355 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100356 info!("A: failed auth-bound operation (no HAT) as expected {result:?}");
357
358 writer.send(&BarrierReached {}); // A done.
359
360 // Action B: fail again when an invalid HAT is available.
361 reader.recv();
362
363 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100364 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100365 info!("B: failed auth-bound operation (HAT is invalid) as expected {result:?}");
366
367 writer.send(&BarrierReached {}); // B done.
368
369 // Action C: fail again when the HAT is old enough to not even be checked.
370 reader.recv();
371 info!("C: wait so that any HAT times out");
372 sleep(Duration::from_secs(4));
373 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100374 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100375 info!("C: failed auth-bound operation (HAT is too old) as expected {result:?}");
376 writer.send(&BarrierReached {}); // C done.
377
378 Ok(())
379 };
380
381 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
382 // `--test-threads=1`), and nothing yet done with binder.
David Drysdalef7ed95a2024-05-08 13:51:45 +0100383 let mut child_handle = unsafe {
384 // Perform keystore actions while running as the test user.
David Drysdale33b7a622024-10-23 17:17:11 +0100385 run_as::run_as_child_app(UID, UID, child_fn)
David Drysdale89e87d52024-10-04 13:07:43 +0100386 }
387 .unwrap();
Rajesh Nyamagoudca4f7af2024-07-29 18:39:01 +0000388
David Drysdale89e87d52024-10-04 13:07:43 +0100389 // Now that the separate process has been forked off, it's safe to use binder to setup a test
390 // user.
391 let _ks2 = get_keystore_service();
392 let user = TestUser::new();
393 let user_id = user.id;
394 let auth_service = get_authorization();
Rajesh Nyamagoudca4f7af2024-07-29 18:39:01 +0000395
David Drysdale89e87d52024-10-04 13:07:43 +0100396 // Lock and unlock to ensure super keys are already created.
397 auth_service
398 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
399 .unwrap();
400 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
401 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100402
David Drysdale89e87d52024-10-04 13:07:43 +0100403 info!("trigger child process action A and wait for completion");
404 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100405 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100406
David Drysdale89e87d52024-10-04 13:07:43 +0100407 // Unlock with password and a fake auth token that matches the key
408 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
409 auth_service.addAuthToken(&fake_bio_lskf_token(GK_FAKE_SID, BIO_FAKE_SID1)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100410
David Drysdale89e87d52024-10-04 13:07:43 +0100411 info!("trigger child process action B and wait for completion");
412 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100413 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100414
David Drysdale89e87d52024-10-04 13:07:43 +0100415 info!("trigger child process action C and wait for completion");
416 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100417 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100418
David Drysdale89e87d52024-10-04 13:07:43 +0100419 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
420}
421
422#[test]
David Drysdaleacd3d982024-10-04 16:18:16 +0100423fn test_auth_bound_per_op_with_gk() {
424 type Barrier = BarrierReachedWithData<Option<i64>>;
425 android_logger::init_once(
426 android_logger::Config::default()
427 .with_tag("keystore2_client_tests")
428 .with_max_level(log::LevelFilter::Debug),
429 );
430
431 let child_fn = move |reader: &mut ChannelReader<Barrier>,
432 writer: &mut ChannelWriter<Barrier>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100433 -> Result<(), run_as::Error> {
David Drysdaleacd3d982024-10-04 16:18:16 +0100434 // Now we're in a new process, wait to be notified before starting.
435 let gk_sid: i64 = match reader.recv().0 {
436 Some(sid) => sid,
437 None => {
438 // There is no AIDL Gatekeeper available, so abandon the test. It would be nice to
439 // know this before starting the child process, but finding it out requires Binder,
440 // which can't be used until after the child has forked.
441 return Ok(());
442 }
443 };
444
445 // Action A: create a new auth-bound key which requires auth-per-operation (because
446 // AUTH_TIMEOUT is not specified), and fail to finish an operation using it.
447 let ks2 = get_keystore_service();
David Drysdale9f0007e2024-10-04 18:58:11 +0100448 let sec_level =
449 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100450 let params = AuthSetBuilder::new()
451 .user_secure_id(gk_sid)
452 .user_secure_id(BIO_FAKE_SID1)
453 .user_auth_type(HardwareAuthenticatorType::ANY)
454 .algorithm(Algorithm::EC)
455 .purpose(KeyPurpose::SIGN)
456 .purpose(KeyPurpose::VERIFY)
457 .digest(Digest::SHA_2_256)
458 .ec_curve(EcCurve::P_256);
459
460 let KeyMetadata { key, .. } = sec_level
461 .generateKey(
462 &KeyDescriptor {
463 domain: Domain::APP,
464 nspace: -1,
465 alias: Some("auth-per-op".to_string()),
466 blob: None,
467 },
468 None,
469 &params,
470 0,
471 b"entropy",
472 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100473 .context("key generation failed")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100474 info!("A: created auth-per-op key {key:?}");
475
476 // We can create an operation using the key...
477 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
478 let result = sec_level
479 .createOperation(&key, &params, UNFORCED)
480 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100481 let op = result.iOperation.context("no operation in result")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100482 info!("A: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
483
484 // .. but attempting to finish the operation fails because Keystore can't find a HAT.
485 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100486 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdaleacd3d982024-10-04 16:18:16 +0100487 info!("A: failed auth-per-op op (no HAT) as expected {result:?}");
488
489 writer.send(&Barrier::new(None)); // A done.
490
491 // Action B: start an operation and pass out the challenge
492 reader.recv();
493 let result = sec_level
494 .createOperation(&key, &params, UNFORCED)
495 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100496 let op = result.iOperation.context("no operation in result")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100497 info!("B: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
498 writer.send(&Barrier::new(Some(result.operationChallenge.unwrap().challenge))); // B done.
499
500 // Action C: finishing the operation succeeds now there's a per-op HAT.
501 reader.recv();
502 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100503 expect!(result.is_ok());
David Drysdaleacd3d982024-10-04 16:18:16 +0100504 info!("C: performed auth-per-op op expected");
505 writer.send(&Barrier::new(None)); // D done.
506
507 Ok(())
508 };
509
510 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
511 // `--test-threads=1`), and nothing yet done with binder.
512 let mut child_handle = unsafe {
513 // Perform keystore actions while running as the test user.
David Drysdale33b7a622024-10-23 17:17:11 +0100514 run_as::run_as_child_app(UID, UID, child_fn)
David Drysdaleacd3d982024-10-04 16:18:16 +0100515 }
516 .unwrap();
517
518 // Now that the separate process has been forked off, it's safe to use binder to setup a test
519 // user.
520 let _ks2 = get_keystore_service();
521 let user = TestUser::new();
522 if user.gk.is_none() {
523 // Can't run this test if there's no AIDL Gatekeeper.
524 child_handle.send(&Barrier::new(None));
525 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
526 return;
527 }
528 let user_id = user.id;
529 let auth_service = get_authorization();
530
531 // Lock and unlock to ensure super keys are already created.
532 auth_service
533 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
534 .unwrap();
535 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
536
537 info!("trigger child process action A and wait for completion");
538 child_handle.send(&Barrier::new(Some(user.gk_sid.unwrap())));
David Drysdale9f0007e2024-10-04 18:58:11 +0100539 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100540
541 info!("trigger child process action B and wait for completion");
542 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100543 let challenge = child_handle.recv_or_die().0.expect("no challenge");
David Drysdaleacd3d982024-10-04 16:18:16 +0100544
545 // Unlock with GK and the challenge to get a genuine per-op auth token
546 let real_hat = user.gk_verify(challenge).expect("failed to perform GK verify");
547 auth_service.addAuthToken(&real_hat).unwrap();
548
549 info!("trigger child process action C and wait for completion");
550 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100551 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100552
553 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
554}
555
556#[test]
David Drysdale89e87d52024-10-04 13:07:43 +0100557fn test_auth_bound_per_op_failure() {
558 type Barrier = BarrierReachedWithData<i64>;
559 android_logger::init_once(
560 android_logger::Config::default()
561 .with_tag("keystore2_client_tests")
562 .with_max_level(log::LevelFilter::Debug),
563 );
564
565 let child_fn = move |reader: &mut ChannelReader<Barrier>,
566 writer: &mut ChannelWriter<Barrier>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100567 -> Result<(), run_as::Error> {
David Drysdale89e87d52024-10-04 13:07:43 +0100568 // Now we're in a new process, wait to be notified before starting.
569 reader.recv();
570
571 // Action A: create a new auth-bound key which requires auth-per-operation (because
572 // AUTH_TIMEOUT is not specified), and fail to finish an operation using it.
573 let ks2 = get_keystore_service();
574
David Drysdale9f0007e2024-10-04 18:58:11 +0100575 let sec_level =
576 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100577 let params = AuthSetBuilder::new()
578 .user_secure_id(GK_FAKE_SID)
579 .user_secure_id(BIO_FAKE_SID1)
580 .user_auth_type(HardwareAuthenticatorType::ANY)
581 .algorithm(Algorithm::EC)
582 .purpose(KeyPurpose::SIGN)
583 .purpose(KeyPurpose::VERIFY)
584 .digest(Digest::SHA_2_256)
585 .ec_curve(EcCurve::P_256);
586
587 let KeyMetadata { key, .. } = sec_level
588 .generateKey(
589 &KeyDescriptor {
590 domain: Domain::APP,
591 nspace: -1,
592 alias: Some("auth-per-op".to_string()),
593 blob: None,
594 },
595 None,
596 &params,
597 0,
598 b"entropy",
599 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100600 .context("key generation failed")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100601 info!("A: created auth-per-op key {key:?}");
602
603 // We can create an operation using the key...
604 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
605 let result = sec_level
606 .createOperation(&key, &params, UNFORCED)
607 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100608 let op = result.iOperation.context("no operation in result")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100609 info!("A: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
610
611 // .. but attempting to finish the operation fails because Keystore can't find a HAT.
612 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100613 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100614 info!("A: failed auth-per-op op (no HAT) as expected {result:?}");
615
616 writer.send(&Barrier::new(0)); // A done.
617
618 // Action B: fail again when an irrelevant HAT is available.
619 reader.recv();
620
621 let result = sec_level
622 .createOperation(&key, &params, UNFORCED)
623 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100624 let op = result.iOperation.context("no operation in result")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100625 info!("B: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
626 // The operation fails because the HAT that Keystore received is not related to the
627 // challenge.
628 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100629 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100630 info!("B: failed auth-per-op op (HAT is not per-op) as expected {result:?}");
631
632 writer.send(&Barrier::new(0)); // B done.
633
634 // Action C: start an operation and pass out the challenge
635 reader.recv();
636 let result = sec_level
637 .createOperation(&key, &params, UNFORCED)
638 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100639 let op = result.iOperation.context("no operation in result")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100640 info!("C: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
641 writer.send(&Barrier::new(result.operationChallenge.unwrap().challenge)); // C done.
642
643 // Action D: finishing the operation still fails because the per-op HAT
644 // is invalid (the HMAC signature is faked and so the secure world
645 // rejects the HAT).
646 reader.recv();
647 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100648 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100649 info!("D: failed auth-per-op op (HAT is per-op but invalid) as expected {result:?}");
650 writer.send(&Barrier::new(0)); // D done.
651
652 Ok(())
653 };
654
655 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
656 // `--test-threads=1`), and nothing yet done with binder.
657 let mut child_handle = unsafe {
658 // Perform keystore actions while running as the test user.
David Drysdale33b7a622024-10-23 17:17:11 +0100659 run_as::run_as_child_app(UID, UID, child_fn)
David Drysdale89e87d52024-10-04 13:07:43 +0100660 }
661 .unwrap();
662
663 // Now that the separate process has been forked off, it's safe to use binder to setup a test
664 // user.
665 let _ks2 = get_keystore_service();
666 let user = TestUser::new();
667 let user_id = user.id;
668 let auth_service = get_authorization();
669
670 // Lock and unlock to ensure super keys are already created.
671 auth_service
672 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
673 .unwrap();
674 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
675 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
676
677 info!("trigger child process action A and wait for completion");
678 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100679 child_handle.recv_or_die();
David Drysdale89e87d52024-10-04 13:07:43 +0100680
681 // Unlock with password and a fake auth token.
682 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
683 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
684
685 info!("trigger child process action B and wait for completion");
686 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100687 child_handle.recv_or_die();
David Drysdale89e87d52024-10-04 13:07:43 +0100688
689 info!("trigger child process action C and wait for completion");
690 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100691 let challenge = child_handle.recv_or_die().0;
David Drysdale89e87d52024-10-04 13:07:43 +0100692
693 // Add a fake auth token with the challenge value.
694 auth_service.addAuthToken(&fake_lskf_token_with_challenge(GK_FAKE_SID, challenge)).unwrap();
695
696 info!("trigger child process action D and wait for completion");
697 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100698 child_handle.recv_or_die();
David Drysdale89e87d52024-10-04 13:07:43 +0100699
700 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
701}
702
703#[test]
704fn test_unlocked_device_required() {
705 android_logger::init_once(
706 android_logger::Config::default()
707 .with_tag("keystore2_client_tests")
708 .with_max_level(log::LevelFilter::Debug),
709 );
710
711 let child_fn = move |reader: &mut ChannelReader<BarrierReached>,
712 writer: &mut ChannelWriter<BarrierReached>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100713 -> Result<(), run_as::Error> {
David Drysdale89e87d52024-10-04 13:07:43 +0100714 let ks2 = get_keystore_service();
715 if ks2.getInterfaceVersion().unwrap() < 4 {
716 // Assuming `IKeystoreAuthorization::onDeviceLocked` and
717 // `IKeystoreAuthorization::onDeviceUnlocked` APIs will be supported on devices
718 // with `IKeystoreService` >= 4.
719 return Ok(());
720 }
721
722 // Now we're in a new process, wait to be notified before starting.
723 reader.recv();
724
725 // Action A: create a new unlocked-device-required key (which thus requires
726 // super-encryption), while the device is unlocked.
David Drysdale9f0007e2024-10-04 18:58:11 +0100727 let sec_level =
728 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100729 let params = AuthSetBuilder::new()
730 .no_auth_required()
731 .unlocked_device_required()
732 .algorithm(Algorithm::EC)
733 .purpose(KeyPurpose::SIGN)
734 .purpose(KeyPurpose::VERIFY)
735 .digest(Digest::SHA_2_256)
736 .ec_curve(EcCurve::P_256);
737
738 let KeyMetadata { key, .. } = sec_level
739 .generateKey(
740 &KeyDescriptor {
741 domain: Domain::APP,
742 nspace: -1,
743 alias: Some("unlocked-device-required".to_string()),
744 blob: None,
745 },
746 None,
747 &params,
748 0,
749 b"entropy",
750 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100751 .context("key generation failed")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100752 info!("A: created unlocked-device-required key while unlocked {key:?}");
753 writer.send(&BarrierReached {}); // A done.
754
755 // Action B: fail to use the unlocked-device-required key while locked.
756 reader.recv();
757 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
758 let result = sec_level.createOperation(&key, &params, UNFORCED);
759 info!("B: use unlocked-device-required key while locked => {result:?}");
David Drysdale9f0007e2024-10-04 18:58:11 +0100760 expect_km_error!(&result, ErrorCode::DEVICE_LOCKED);
David Drysdale89e87d52024-10-04 13:07:43 +0100761 writer.send(&BarrierReached {}); // B done.
762
763 // Action C: try to use the unlocked-device-required key while unlocked with a
764 // password.
765 reader.recv();
766 let result = sec_level.createOperation(&key, &params, UNFORCED);
767 info!("C: use unlocked-device-required key while lskf-unlocked => {result:?}");
David Drysdale9f0007e2024-10-04 18:58:11 +0100768 expect!(result.is_ok(), "failed with {result:?}");
David Drysdale89e87d52024-10-04 13:07:43 +0100769 abort_op(result);
770 writer.send(&BarrierReached {}); // C done.
771
772 // Action D: try to use the unlocked-device-required key while unlocked with a weak
773 // biometric.
774 reader.recv();
775 let result = sec_level.createOperation(&key, &params, UNFORCED);
776 info!("D: use unlocked-device-required key while weak-locked => {result:?}");
David Drysdale9f0007e2024-10-04 18:58:11 +0100777 expect!(result.is_ok(), "createOperation failed: {result:?}");
David Drysdale89e87d52024-10-04 13:07:43 +0100778 abort_op(result);
779 writer.send(&BarrierReached {}); // D done.
780
781 Ok(())
782 };
783
784 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
785 // `--test-threads=1`), and nothing yet done with binder.
786 let mut child_handle = unsafe {
787 // Perform keystore actions while running as the test user.
David Drysdale33b7a622024-10-23 17:17:11 +0100788 run_as::run_as_child_app(UID, UID, child_fn)
David Drysdalef7ed95a2024-05-08 13:51:45 +0100789 }
790 .unwrap();
791
Rajesh Nyamagoudca4f7af2024-07-29 18:39:01 +0000792 let ks2 = get_keystore_service();
793 if ks2.getInterfaceVersion().unwrap() < 4 {
794 // Assuming `IKeystoreAuthorization::onDeviceLocked` and
795 // `IKeystoreAuthorization::onDeviceUnlocked` APIs will be supported on devices
796 // with `IKeystoreService` >= 4.
797 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
798 return;
799 }
David Drysdalef7ed95a2024-05-08 13:51:45 +0100800 // Now that the separate process has been forked off, it's safe to use binder.
801 let user = TestUser::new();
802 let user_id = user.id;
803 let auth_service = get_authorization();
804
805 // Lock and unlock to ensure super keys are already created.
David Drysdale89e87d52024-10-04 13:07:43 +0100806 auth_service
807 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
808 .unwrap();
809 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
810 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100811
812 info!("trigger child process action A while unlocked and wait for completion");
813 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100814 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100815
816 // Move to locked and don't allow weak unlock, so super keys are wiped.
David Drysdale89e87d52024-10-04 13:07:43 +0100817 auth_service
818 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
819 .unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100820
821 info!("trigger child process action B while locked and wait for completion");
822 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100823 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100824
825 // Unlock with password => loads super key from database.
David Drysdale89e87d52024-10-04 13:07:43 +0100826 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
827 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100828
829 info!("trigger child process action C while lskf-unlocked and wait for completion");
830 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100831 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100832
833 // Move to locked and allow weak unlock, then do a weak unlock.
David Drysdale89e87d52024-10-04 13:07:43 +0100834 auth_service
835 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_ENABLED)
836 .unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100837 auth_service.onDeviceUnlocked(user_id, None).unwrap();
838
839 info!("trigger child process action D while weak-unlocked and wait for completion");
840 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100841 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100842
843 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
844}
845
846/// Generate a fake [`HardwareAuthToken`] for the given sid.
847fn fake_lskf_token(gk_sid: i64) -> HardwareAuthToken {
David Drysdale89e87d52024-10-04 13:07:43 +0100848 fake_lskf_token_with_challenge(gk_sid, 0)
849}
850
851/// Generate a fake [`HardwareAuthToken`] for the given sid and challenge.
852fn fake_lskf_token_with_challenge(gk_sid: i64, challenge: i64) -> HardwareAuthToken {
853 HardwareAuthToken {
854 challenge,
855 userId: gk_sid,
856 authenticatorId: 0,
857 authenticatorType: HardwareAuthenticatorType::PASSWORD,
858 timestamp: Timestamp { milliSeconds: 123 },
859 mac: vec![1, 2, 3],
860 }
861}
862
863/// Generate a fake [`HardwareAuthToken`] for the given sids
864fn fake_bio_lskf_token(gk_sid: i64, bio_sid: i64) -> HardwareAuthToken {
David Drysdalef7ed95a2024-05-08 13:51:45 +0100865 HardwareAuthToken {
866 challenge: 0,
867 userId: gk_sid,
David Drysdale89e87d52024-10-04 13:07:43 +0100868 authenticatorId: bio_sid,
David Drysdalef7ed95a2024-05-08 13:51:45 +0100869 authenticatorType: HardwareAuthenticatorType::PASSWORD,
870 timestamp: Timestamp { milliSeconds: 123 },
871 mac: vec![1, 2, 3],
872 }
873}