blob: 336af4fa174ac42dea4826edd0d1e613fa22e5f6 [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};
46use nix::unistd::{Gid, Uid};
47use rustutils::users::AID_USER_OFFSET;
David Drysdale89e87d52024-10-04 13:07:43 +010048use std::{time::Duration, thread::sleep};
David Drysdalef7ed95a2024-05-08 13:51:45 +010049
David Drysdale89e87d52024-10-04 13:07:43 +010050/// SELinux context.
51const CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20";
David Drysdalef7ed95a2024-05-08 13:51:45 +010052/// Test user ID.
53const TEST_USER_ID: i32 = 100;
David Drysdale89e87d52024-10-04 13:07:43 +010054/// Corresponding uid value.
55const UID: u32 = TEST_USER_ID as u32 * AID_USER_OFFSET + 1001;
56/// Fake synthetic password blob.
57static SYNTHETIC_PASSWORD: &[u8] = &[
David Drysdalef7ed95a2024-05-08 13:51:45 +010058 0x42, 0x39, 0x30, 0x37, 0x44, 0x37, 0x32, 0x37, 0x39, 0x39, 0x43, 0x42, 0x39, 0x41, 0x42, 0x30,
59 0x34, 0x31, 0x30, 0x38, 0x46, 0x44, 0x33, 0x45, 0x39, 0x42, 0x32, 0x38, 0x36, 0x35, 0x41, 0x36,
60 0x33, 0x44, 0x42, 0x42, 0x43, 0x36, 0x33, 0x42, 0x34, 0x39, 0x37, 0x33, 0x35, 0x45, 0x41, 0x41,
61 0x32, 0x45, 0x31, 0x35, 0x43, 0x43, 0x46, 0x32, 0x39, 0x36, 0x33, 0x34, 0x31, 0x32, 0x41, 0x39,
62];
David Drysdaleacd3d982024-10-04 16:18:16 +010063/// Gatekeeper password.
64static GK_PASSWORD: &[u8] = b"correcthorsebatterystaple";
David Drysdalef7ed95a2024-05-08 13:51:45 +010065/// Fake SID value corresponding to Gatekeeper.
David Drysdale89e87d52024-10-04 13:07:43 +010066static GK_FAKE_SID: i64 = 123456;
David Drysdalef7ed95a2024-05-08 13:51:45 +010067/// Fake SID value corresponding to a biometric authenticator.
David Drysdale89e87d52024-10-04 13:07:43 +010068static BIO_FAKE_SID1: i64 = 345678;
David Drysdalef7ed95a2024-05-08 13:51:45 +010069/// Fake SID value corresponding to a biometric authenticator.
David Drysdale89e87d52024-10-04 13:07:43 +010070static BIO_FAKE_SID2: i64 = 456789;
David Drysdalef7ed95a2024-05-08 13:51:45 +010071
72const WEAK_UNLOCK_ENABLED: bool = true;
73const WEAK_UNLOCK_DISABLED: bool = false;
74const UNFORCED: bool = false;
75
76fn get_authorization() -> binder::Strong<dyn IKeystoreAuthorization> {
77 binder::get_interface("android.security.authorization").unwrap()
78}
79
80fn get_maintenance() -> binder::Strong<dyn IKeystoreMaintenance> {
81 binder::get_interface("android.security.maintenance").unwrap()
82}
83
David Drysdaleacd3d982024-10-04 16:18:16 +010084/// Get the default Gatekeeper instance. This may fail on older devices where Gatekeeper is still a
85/// HIDL interface rather than AIDL.
86fn get_gatekeeper() -> Option<binder::Strong<dyn IGatekeeper>> {
87 binder::get_interface("android.hardware.gatekeeper.IGatekeeper/default").ok()
88}
89
90/// Indicate whether a Gatekeeper result indicates a delayed-retry is needed.
91fn is_gk_retry<T: std::fmt::Debug>(result: &BinderResult<T>) -> bool {
92 matches!(result, Err(s) if s.exception_code() == ExceptionCode::SERVICE_SPECIFIC
93 && s.service_specific_error() == ERROR_RETRY_TIMEOUT)
94}
95
David Drysdalef7ed95a2024-05-08 13:51:45 +010096fn abort_op(result: binder::Result<CreateOperationResponse>) {
97 if let Ok(rsp) = result {
98 if let Some(op) = rsp.iOperation {
99 if let Err(e) = op.abort() {
100 warn!("abort op failed: {e:?}");
101 }
102 } else {
103 warn!("can't abort op with missing iOperation");
104 }
105 } else {
106 warn!("can't abort failed op: {result:?}");
107 }
108}
109
110/// RAII structure to ensure that test users are removed at the end of a test.
111struct TestUser {
112 id: i32,
113 maint: binder::Strong<dyn IKeystoreMaintenance>,
David Drysdaleacd3d982024-10-04 16:18:16 +0100114 gk: Option<binder::Strong<dyn IGatekeeper>>,
115 gk_sid: Option<i64>,
116 gk_handle: Vec<u8>,
David Drysdalef7ed95a2024-05-08 13:51:45 +0100117}
118
119impl TestUser {
120 fn new() -> Self {
David Drysdale89e87d52024-10-04 13:07:43 +0100121 Self::new_user(TEST_USER_ID, SYNTHETIC_PASSWORD)
David Drysdalef7ed95a2024-05-08 13:51:45 +0100122 }
David Drysdaleacd3d982024-10-04 16:18:16 +0100123 fn new_user(user_id: i32, sp: &[u8]) -> Self {
David Drysdalef7ed95a2024-05-08 13:51:45 +0100124 let maint = get_maintenance();
125 maint.onUserAdded(user_id).expect("failed to add test user");
126 maint
David Drysdaleacd3d982024-10-04 16:18:16 +0100127 .initUserSuperKeys(user_id, sp, /* allowExisting= */ false)
David Drysdalef7ed95a2024-05-08 13:51:45 +0100128 .expect("failed to init test user");
David Drysdaleacd3d982024-10-04 16:18:16 +0100129 let gk = get_gatekeeper();
130 let (gk_sid, gk_handle) = if let Some(gk) = &gk {
131 // AIDL Gatekeeper is available, so enroll a password.
132 loop {
133 let result = gk.enroll(user_id, &[], &[], GK_PASSWORD);
134 if is_gk_retry(&result) {
135 sleep(Duration::from_secs(1));
136 continue;
137 }
138 let rsp = result.expect("gk.enroll() failed");
139 info!("registered test user {user_id} as sid {} with GK", rsp.secureUserId);
140 break (Some(rsp.secureUserId), rsp.data);
141 }
142 } else {
143 (None, vec![])
144 };
145 Self { id: user_id, maint, gk, gk_sid, gk_handle }
146 }
147
148 /// Perform Gatekeeper verification, which will return a HAT on success.
149 fn gk_verify(&self, challenge: i64) -> Option<HardwareAuthToken> {
150 let Some(gk) = &self.gk else { return None };
151 loop {
152 let result = gk.verify(self.id, challenge, &self.gk_handle, GK_PASSWORD);
153 if is_gk_retry(&result) {
154 sleep(Duration::from_secs(1));
155 continue;
156 }
157 let rsp = result.expect("gk.verify failed");
158 break Some(rsp.hardwareAuthToken);
159 }
David Drysdalef7ed95a2024-05-08 13:51:45 +0100160 }
161}
162
163impl Drop for TestUser {
164 fn drop(&mut self) {
165 let _ = self.maint.onUserRemoved(self.id);
David Drysdaleacd3d982024-10-04 16:18:16 +0100166 if let Some(gk) = &self.gk {
167 info!("deregister test user {} with GK", self.id);
168 if let Err(e) = gk.deleteUser(self.id) {
169 warn!("failed to deregister test user {}: {e:?}", self.id);
170 }
171 }
David Drysdalef7ed95a2024-05-08 13:51:45 +0100172 }
173}
174
175#[test]
David Drysdaleacd3d982024-10-04 16:18:16 +0100176fn test_auth_bound_timeout_with_gk() {
177 type Barrier = BarrierReachedWithData<Option<i64>>;
178 android_logger::init_once(
179 android_logger::Config::default()
180 .with_tag("keystore2_client_tests")
181 .with_max_level(log::LevelFilter::Debug),
182 );
183
184 let child_fn = move |reader: &mut ChannelReader<Barrier>,
185 writer: &mut ChannelWriter<Barrier>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100186 -> Result<(), run_as::Error> {
David Drysdaleacd3d982024-10-04 16:18:16 +0100187 // Now we're in a new process, wait to be notified before starting.
188 let gk_sid: i64 = match reader.recv().0 {
189 Some(sid) => sid,
190 None => {
191 // There is no AIDL Gatekeeper available, so abandon the test. It would be nice to
192 // know this before starting the child process, but finding it out requires Binder,
193 // which can't be used until after the child has forked.
194 return Ok(());
195 }
196 };
197
198 // Action A: create a new auth-bound key which requires auth in the last 3 seconds,
199 // and fail to start an operation using it.
200 let ks2 = get_keystore_service();
David Drysdale9f0007e2024-10-04 18:58:11 +0100201 let sec_level =
202 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100203 let params = AuthSetBuilder::new()
204 .user_secure_id(gk_sid)
205 .user_secure_id(BIO_FAKE_SID1)
206 .user_secure_id(BIO_FAKE_SID2)
207 .user_auth_type(HardwareAuthenticatorType::ANY)
208 .auth_timeout(3)
209 .algorithm(Algorithm::EC)
210 .purpose(KeyPurpose::SIGN)
211 .purpose(KeyPurpose::VERIFY)
212 .digest(Digest::SHA_2_256)
213 .ec_curve(EcCurve::P_256);
214
215 let KeyMetadata { key, .. } = sec_level
216 .generateKey(
217 &KeyDescriptor {
218 domain: Domain::APP,
219 nspace: -1,
220 alias: Some("auth-bound-timeout".to_string()),
221 blob: None,
222 },
223 None,
224 &params,
225 0,
226 b"entropy",
227 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100228 .context("key generation failed")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100229 info!("A: created auth-timeout key {key:?}");
230
231 // No HATs so cannot create an operation using the key.
232 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
233 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100234 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdaleacd3d982024-10-04 16:18:16 +0100235 info!("A: failed auth-bound operation (no HAT) as expected {result:?}");
236
237 writer.send(&Barrier::new(None)); // A done.
238
239 // Action B: succeed when a valid HAT is available.
240 reader.recv();
241
242 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100243 expect!(result.is_ok());
244 let op = result.unwrap().iOperation.context("no operation in result")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100245 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100246 expect!(result.is_ok());
David Drysdaleacd3d982024-10-04 16:18:16 +0100247 info!("B: performed auth-bound operation (with valid GK HAT) as expected");
248
249 writer.send(&Barrier::new(None)); // B done.
250
251 // Action C: fail again when the HAT is old enough to not even be checked.
252 reader.recv();
253 info!("C: wait so that any HAT times out");
254 sleep(Duration::from_secs(4));
255 let result = sec_level.createOperation(&key, &params, UNFORCED);
256 info!("C: failed auth-bound operation (HAT is too old) as expected {result:?}");
257 writer.send(&Barrier::new(None)); // C done.
258
259 Ok(())
260 };
261
262 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
263 // `--test-threads=1`), and nothing yet done with binder.
264 let mut child_handle = unsafe {
265 // Perform keystore actions while running as the test user.
266 run_as::run_as_child(CTX, Uid::from_raw(UID), Gid::from_raw(UID), child_fn)
267 }
268 .unwrap();
269
270 // Now that the separate process has been forked off, it's safe to use binder to setup a test
271 // user.
272 let _ks2 = get_keystore_service();
273 let user = TestUser::new();
274 if user.gk.is_none() {
275 // Can't run this test if there's no AIDL Gatekeeper.
276 child_handle.send(&Barrier::new(None));
277 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
278 return;
279 }
280 let user_id = user.id;
281 let auth_service = get_authorization();
282
283 // Lock and unlock to ensure super keys are already created.
284 auth_service
285 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
286 .unwrap();
287 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
288
289 info!("trigger child process action A and wait for completion");
290 child_handle.send(&Barrier::new(Some(user.gk_sid.unwrap())));
David Drysdale9f0007e2024-10-04 18:58:11 +0100291 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100292
293 // Unlock with GK password to get a genuine auth token.
294 let real_hat = user.gk_verify(0).expect("failed to perform GK verify");
295 auth_service.addAuthToken(&real_hat).unwrap();
296
297 info!("trigger child process action B and wait for completion");
298 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100299 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100300
301 info!("trigger child process action C and wait for completion");
302 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100303 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100304
305 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
306}
307
308#[test]
David Drysdale89e87d52024-10-04 13:07:43 +0100309fn test_auth_bound_timeout_failure() {
David Drysdalef7ed95a2024-05-08 13:51:45 +0100310 android_logger::init_once(
311 android_logger::Config::default()
312 .with_tag("keystore2_client_tests")
313 .with_max_level(log::LevelFilter::Debug),
314 );
David Drysdalef7ed95a2024-05-08 13:51:45 +0100315
David Drysdale89e87d52024-10-04 13:07:43 +0100316 let child_fn = move |reader: &mut ChannelReader<BarrierReached>,
317 writer: &mut ChannelWriter<BarrierReached>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100318 -> Result<(), run_as::Error> {
David Drysdale89e87d52024-10-04 13:07:43 +0100319 // Now we're in a new process, wait to be notified before starting.
320 reader.recv();
321
322 // Action A: create a new auth-bound key which requires auth in the last 3 seconds,
323 // and fail to start an operation using it.
324 let ks2 = get_keystore_service();
325
David Drysdale9f0007e2024-10-04 18:58:11 +0100326 let sec_level =
327 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100328 let params = AuthSetBuilder::new()
329 .user_secure_id(BIO_FAKE_SID1)
330 .user_secure_id(BIO_FAKE_SID2)
331 .user_auth_type(HardwareAuthenticatorType::ANY)
332 .auth_timeout(3)
333 .algorithm(Algorithm::EC)
334 .purpose(KeyPurpose::SIGN)
335 .purpose(KeyPurpose::VERIFY)
336 .digest(Digest::SHA_2_256)
337 .ec_curve(EcCurve::P_256);
338
339 let KeyMetadata { key, .. } = sec_level
340 .generateKey(
341 &KeyDescriptor {
342 domain: Domain::APP,
343 nspace: -1,
344 alias: Some("auth-bound-timeout".to_string()),
345 blob: None,
346 },
347 None,
348 &params,
349 0,
350 b"entropy",
351 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100352 .context("key generation failed")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100353 info!("A: created auth-timeout key {key:?}");
354
355 // No HATs so cannot create an operation using the key.
356 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
357 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100358 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100359 info!("A: failed auth-bound operation (no HAT) as expected {result:?}");
360
361 writer.send(&BarrierReached {}); // A done.
362
363 // Action B: fail again when an invalid HAT is available.
364 reader.recv();
365
366 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100367 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100368 info!("B: failed auth-bound operation (HAT is invalid) as expected {result:?}");
369
370 writer.send(&BarrierReached {}); // B done.
371
372 // Action C: fail again when the HAT is old enough to not even be checked.
373 reader.recv();
374 info!("C: wait so that any HAT times out");
375 sleep(Duration::from_secs(4));
376 let result = sec_level.createOperation(&key, &params, UNFORCED);
David Drysdale9f0007e2024-10-04 18:58:11 +0100377 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100378 info!("C: failed auth-bound operation (HAT is too old) as expected {result:?}");
379 writer.send(&BarrierReached {}); // C done.
380
381 Ok(())
382 };
383
384 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
385 // `--test-threads=1`), and nothing yet done with binder.
David Drysdalef7ed95a2024-05-08 13:51:45 +0100386 let mut child_handle = unsafe {
387 // Perform keystore actions while running as the test user.
David Drysdale89e87d52024-10-04 13:07:43 +0100388 run_as::run_as_child(CTX, Uid::from_raw(UID), Gid::from_raw(UID), child_fn)
389 }
390 .unwrap();
Rajesh Nyamagoudca4f7af2024-07-29 18:39:01 +0000391
David Drysdale89e87d52024-10-04 13:07:43 +0100392 // Now that the separate process has been forked off, it's safe to use binder to setup a test
393 // user.
394 let _ks2 = get_keystore_service();
395 let user = TestUser::new();
396 let user_id = user.id;
397 let auth_service = get_authorization();
Rajesh Nyamagoudca4f7af2024-07-29 18:39:01 +0000398
David Drysdale89e87d52024-10-04 13:07:43 +0100399 // Lock and unlock to ensure super keys are already created.
400 auth_service
401 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
402 .unwrap();
403 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
404 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100405
David Drysdale89e87d52024-10-04 13:07:43 +0100406 info!("trigger child process action A and wait for completion");
407 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100408 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100409
David Drysdale89e87d52024-10-04 13:07:43 +0100410 // Unlock with password and a fake auth token that matches the key
411 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
412 auth_service.addAuthToken(&fake_bio_lskf_token(GK_FAKE_SID, BIO_FAKE_SID1)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100413
David Drysdale89e87d52024-10-04 13:07:43 +0100414 info!("trigger child process action B and wait for completion");
415 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100416 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100417
David Drysdale89e87d52024-10-04 13:07:43 +0100418 info!("trigger child process action C and wait for completion");
419 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100420 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100421
David Drysdale89e87d52024-10-04 13:07:43 +0100422 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
423}
424
425#[test]
David Drysdaleacd3d982024-10-04 16:18:16 +0100426fn test_auth_bound_per_op_with_gk() {
427 type Barrier = BarrierReachedWithData<Option<i64>>;
428 android_logger::init_once(
429 android_logger::Config::default()
430 .with_tag("keystore2_client_tests")
431 .with_max_level(log::LevelFilter::Debug),
432 );
433
434 let child_fn = move |reader: &mut ChannelReader<Barrier>,
435 writer: &mut ChannelWriter<Barrier>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100436 -> Result<(), run_as::Error> {
David Drysdaleacd3d982024-10-04 16:18:16 +0100437 // Now we're in a new process, wait to be notified before starting.
438 let gk_sid: i64 = match reader.recv().0 {
439 Some(sid) => sid,
440 None => {
441 // There is no AIDL Gatekeeper available, so abandon the test. It would be nice to
442 // know this before starting the child process, but finding it out requires Binder,
443 // which can't be used until after the child has forked.
444 return Ok(());
445 }
446 };
447
448 // Action A: create a new auth-bound key which requires auth-per-operation (because
449 // AUTH_TIMEOUT is not specified), and fail to finish an operation using it.
450 let ks2 = get_keystore_service();
David Drysdale9f0007e2024-10-04 18:58:11 +0100451 let sec_level =
452 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100453 let params = AuthSetBuilder::new()
454 .user_secure_id(gk_sid)
455 .user_secure_id(BIO_FAKE_SID1)
456 .user_auth_type(HardwareAuthenticatorType::ANY)
457 .algorithm(Algorithm::EC)
458 .purpose(KeyPurpose::SIGN)
459 .purpose(KeyPurpose::VERIFY)
460 .digest(Digest::SHA_2_256)
461 .ec_curve(EcCurve::P_256);
462
463 let KeyMetadata { key, .. } = sec_level
464 .generateKey(
465 &KeyDescriptor {
466 domain: Domain::APP,
467 nspace: -1,
468 alias: Some("auth-per-op".to_string()),
469 blob: None,
470 },
471 None,
472 &params,
473 0,
474 b"entropy",
475 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100476 .context("key generation failed")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100477 info!("A: created auth-per-op key {key:?}");
478
479 // We can create an operation using the key...
480 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
481 let result = sec_level
482 .createOperation(&key, &params, UNFORCED)
483 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100484 let op = result.iOperation.context("no operation in result")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100485 info!("A: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
486
487 // .. but attempting to finish the operation fails because Keystore can't find a HAT.
488 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100489 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdaleacd3d982024-10-04 16:18:16 +0100490 info!("A: failed auth-per-op op (no HAT) as expected {result:?}");
491
492 writer.send(&Barrier::new(None)); // A done.
493
494 // Action B: start an operation and pass out the challenge
495 reader.recv();
496 let result = sec_level
497 .createOperation(&key, &params, UNFORCED)
498 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100499 let op = result.iOperation.context("no operation in result")?;
David Drysdaleacd3d982024-10-04 16:18:16 +0100500 info!("B: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
501 writer.send(&Barrier::new(Some(result.operationChallenge.unwrap().challenge))); // B done.
502
503 // Action C: finishing the operation succeeds now there's a per-op HAT.
504 reader.recv();
505 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100506 expect!(result.is_ok());
David Drysdaleacd3d982024-10-04 16:18:16 +0100507 info!("C: performed auth-per-op op expected");
508 writer.send(&Barrier::new(None)); // D done.
509
510 Ok(())
511 };
512
513 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
514 // `--test-threads=1`), and nothing yet done with binder.
515 let mut child_handle = unsafe {
516 // Perform keystore actions while running as the test user.
517 run_as::run_as_child(CTX, Uid::from_raw(UID), Gid::from_raw(UID), child_fn)
518 }
519 .unwrap();
520
521 // Now that the separate process has been forked off, it's safe to use binder to setup a test
522 // user.
523 let _ks2 = get_keystore_service();
524 let user = TestUser::new();
525 if user.gk.is_none() {
526 // Can't run this test if there's no AIDL Gatekeeper.
527 child_handle.send(&Barrier::new(None));
528 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
529 return;
530 }
531 let user_id = user.id;
532 let auth_service = get_authorization();
533
534 // Lock and unlock to ensure super keys are already created.
535 auth_service
536 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
537 .unwrap();
538 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
539
540 info!("trigger child process action A and wait for completion");
541 child_handle.send(&Barrier::new(Some(user.gk_sid.unwrap())));
David Drysdale9f0007e2024-10-04 18:58:11 +0100542 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100543
544 info!("trigger child process action B and wait for completion");
545 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100546 let challenge = child_handle.recv_or_die().0.expect("no challenge");
David Drysdaleacd3d982024-10-04 16:18:16 +0100547
548 // Unlock with GK and the challenge to get a genuine per-op auth token
549 let real_hat = user.gk_verify(challenge).expect("failed to perform GK verify");
550 auth_service.addAuthToken(&real_hat).unwrap();
551
552 info!("trigger child process action C and wait for completion");
553 child_handle.send(&Barrier::new(None));
David Drysdale9f0007e2024-10-04 18:58:11 +0100554 child_handle.recv_or_die();
David Drysdaleacd3d982024-10-04 16:18:16 +0100555
556 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
557}
558
559#[test]
David Drysdale89e87d52024-10-04 13:07:43 +0100560fn test_auth_bound_per_op_failure() {
561 type Barrier = BarrierReachedWithData<i64>;
562 android_logger::init_once(
563 android_logger::Config::default()
564 .with_tag("keystore2_client_tests")
565 .with_max_level(log::LevelFilter::Debug),
566 );
567
568 let child_fn = move |reader: &mut ChannelReader<Barrier>,
569 writer: &mut ChannelWriter<Barrier>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100570 -> Result<(), run_as::Error> {
David Drysdale89e87d52024-10-04 13:07:43 +0100571 // Now we're in a new process, wait to be notified before starting.
572 reader.recv();
573
574 // Action A: create a new auth-bound key which requires auth-per-operation (because
575 // AUTH_TIMEOUT is not specified), and fail to finish an operation using it.
576 let ks2 = get_keystore_service();
577
David Drysdale9f0007e2024-10-04 18:58:11 +0100578 let sec_level =
579 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100580 let params = AuthSetBuilder::new()
581 .user_secure_id(GK_FAKE_SID)
582 .user_secure_id(BIO_FAKE_SID1)
583 .user_auth_type(HardwareAuthenticatorType::ANY)
584 .algorithm(Algorithm::EC)
585 .purpose(KeyPurpose::SIGN)
586 .purpose(KeyPurpose::VERIFY)
587 .digest(Digest::SHA_2_256)
588 .ec_curve(EcCurve::P_256);
589
590 let KeyMetadata { key, .. } = sec_level
591 .generateKey(
592 &KeyDescriptor {
593 domain: Domain::APP,
594 nspace: -1,
595 alias: Some("auth-per-op".to_string()),
596 blob: None,
597 },
598 None,
599 &params,
600 0,
601 b"entropy",
602 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100603 .context("key generation failed")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100604 info!("A: created auth-per-op key {key:?}");
605
606 // We can create an operation using the key...
607 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
608 let result = sec_level
609 .createOperation(&key, &params, UNFORCED)
610 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100611 let op = result.iOperation.context("no operation in result")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100612 info!("A: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
613
614 // .. but attempting to finish the operation fails because Keystore can't find a HAT.
615 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100616 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100617 info!("A: failed auth-per-op op (no HAT) as expected {result:?}");
618
619 writer.send(&Barrier::new(0)); // A done.
620
621 // Action B: fail again when an irrelevant HAT is available.
622 reader.recv();
623
624 let result = sec_level
625 .createOperation(&key, &params, UNFORCED)
626 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100627 let op = result.iOperation.context("no operation in result")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100628 info!("B: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
629 // The operation fails because the HAT that Keystore received is not related to the
630 // challenge.
631 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100632 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100633 info!("B: failed auth-per-op op (HAT is not per-op) as expected {result:?}");
634
635 writer.send(&Barrier::new(0)); // B done.
636
637 // Action C: start an operation and pass out the challenge
638 reader.recv();
639 let result = sec_level
640 .createOperation(&key, &params, UNFORCED)
641 .expect("failed to create auth-per-op operation");
David Drysdale9f0007e2024-10-04 18:58:11 +0100642 let op = result.iOperation.context("no operation in result")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100643 info!("C: created auth-per-op operation, got challenge {:?}", result.operationChallenge);
644 writer.send(&Barrier::new(result.operationChallenge.unwrap().challenge)); // C done.
645
646 // Action D: finishing the operation still fails because the per-op HAT
647 // is invalid (the HMAC signature is faked and so the secure world
648 // rejects the HAT).
649 reader.recv();
650 let result = op.finish(Some(b"data"), None);
David Drysdale9f0007e2024-10-04 18:58:11 +0100651 expect_km_error!(&result, ErrorCode::KEY_USER_NOT_AUTHENTICATED);
David Drysdale89e87d52024-10-04 13:07:43 +0100652 info!("D: failed auth-per-op op (HAT is per-op but invalid) as expected {result:?}");
653 writer.send(&Barrier::new(0)); // D done.
654
655 Ok(())
656 };
657
658 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
659 // `--test-threads=1`), and nothing yet done with binder.
660 let mut child_handle = unsafe {
661 // Perform keystore actions while running as the test user.
662 run_as::run_as_child(CTX, Uid::from_raw(UID), Gid::from_raw(UID), child_fn)
663 }
664 .unwrap();
665
666 // Now that the separate process has been forked off, it's safe to use binder to setup a test
667 // user.
668 let _ks2 = get_keystore_service();
669 let user = TestUser::new();
670 let user_id = user.id;
671 let auth_service = get_authorization();
672
673 // Lock and unlock to ensure super keys are already created.
674 auth_service
675 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
676 .unwrap();
677 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
678 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
679
680 info!("trigger child process action A and wait for completion");
681 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100682 child_handle.recv_or_die();
David Drysdale89e87d52024-10-04 13:07:43 +0100683
684 // Unlock with password and a fake auth token.
685 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
686 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
687
688 info!("trigger child process action B and wait for completion");
689 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100690 child_handle.recv_or_die();
David Drysdale89e87d52024-10-04 13:07:43 +0100691
692 info!("trigger child process action C and wait for completion");
693 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100694 let challenge = child_handle.recv_or_die().0;
David Drysdale89e87d52024-10-04 13:07:43 +0100695
696 // Add a fake auth token with the challenge value.
697 auth_service.addAuthToken(&fake_lskf_token_with_challenge(GK_FAKE_SID, challenge)).unwrap();
698
699 info!("trigger child process action D and wait for completion");
700 child_handle.send(&Barrier::new(0));
David Drysdale9f0007e2024-10-04 18:58:11 +0100701 child_handle.recv_or_die();
David Drysdale89e87d52024-10-04 13:07:43 +0100702
703 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
704}
705
706#[test]
707fn test_unlocked_device_required() {
708 android_logger::init_once(
709 android_logger::Config::default()
710 .with_tag("keystore2_client_tests")
711 .with_max_level(log::LevelFilter::Debug),
712 );
713
714 let child_fn = move |reader: &mut ChannelReader<BarrierReached>,
715 writer: &mut ChannelWriter<BarrierReached>|
David Drysdale9f0007e2024-10-04 18:58:11 +0100716 -> Result<(), run_as::Error> {
David Drysdale89e87d52024-10-04 13:07:43 +0100717 let ks2 = get_keystore_service();
718 if ks2.getInterfaceVersion().unwrap() < 4 {
719 // Assuming `IKeystoreAuthorization::onDeviceLocked` and
720 // `IKeystoreAuthorization::onDeviceUnlocked` APIs will be supported on devices
721 // with `IKeystoreService` >= 4.
722 return Ok(());
723 }
724
725 // Now we're in a new process, wait to be notified before starting.
726 reader.recv();
727
728 // Action A: create a new unlocked-device-required key (which thus requires
729 // super-encryption), while the device is unlocked.
David Drysdale9f0007e2024-10-04 18:58:11 +0100730 let sec_level =
731 ks2.getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT).context("no TEE")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100732 let params = AuthSetBuilder::new()
733 .no_auth_required()
734 .unlocked_device_required()
735 .algorithm(Algorithm::EC)
736 .purpose(KeyPurpose::SIGN)
737 .purpose(KeyPurpose::VERIFY)
738 .digest(Digest::SHA_2_256)
739 .ec_curve(EcCurve::P_256);
740
741 let KeyMetadata { key, .. } = sec_level
742 .generateKey(
743 &KeyDescriptor {
744 domain: Domain::APP,
745 nspace: -1,
746 alias: Some("unlocked-device-required".to_string()),
747 blob: None,
748 },
749 None,
750 &params,
751 0,
752 b"entropy",
753 )
David Drysdale9f0007e2024-10-04 18:58:11 +0100754 .context("key generation failed")?;
David Drysdale89e87d52024-10-04 13:07:43 +0100755 info!("A: created unlocked-device-required key while unlocked {key:?}");
756 writer.send(&BarrierReached {}); // A done.
757
758 // Action B: fail to use the unlocked-device-required key while locked.
759 reader.recv();
760 let params = AuthSetBuilder::new().purpose(KeyPurpose::SIGN).digest(Digest::SHA_2_256);
761 let result = sec_level.createOperation(&key, &params, UNFORCED);
762 info!("B: use unlocked-device-required key while locked => {result:?}");
David Drysdale9f0007e2024-10-04 18:58:11 +0100763 expect_km_error!(&result, ErrorCode::DEVICE_LOCKED);
David Drysdale89e87d52024-10-04 13:07:43 +0100764 writer.send(&BarrierReached {}); // B done.
765
766 // Action C: try to use the unlocked-device-required key while unlocked with a
767 // password.
768 reader.recv();
769 let result = sec_level.createOperation(&key, &params, UNFORCED);
770 info!("C: use unlocked-device-required key while lskf-unlocked => {result:?}");
David Drysdale9f0007e2024-10-04 18:58:11 +0100771 expect!(result.is_ok(), "failed with {result:?}");
David Drysdale89e87d52024-10-04 13:07:43 +0100772 abort_op(result);
773 writer.send(&BarrierReached {}); // C done.
774
775 // Action D: try to use the unlocked-device-required key while unlocked with a weak
776 // biometric.
777 reader.recv();
778 let result = sec_level.createOperation(&key, &params, UNFORCED);
779 info!("D: use unlocked-device-required key while weak-locked => {result:?}");
David Drysdale9f0007e2024-10-04 18:58:11 +0100780 expect!(result.is_ok(), "createOperation failed: {result:?}");
David Drysdale89e87d52024-10-04 13:07:43 +0100781 abort_op(result);
782 writer.send(&BarrierReached {}); // D done.
783
784 Ok(())
785 };
786
787 // Safety: only one thread at this point (enforced by `AndroidTest.xml` setting
788 // `--test-threads=1`), and nothing yet done with binder.
789 let mut child_handle = unsafe {
790 // Perform keystore actions while running as the test user.
791 run_as::run_as_child(CTX, Uid::from_raw(UID), Gid::from_raw(UID), child_fn)
David Drysdalef7ed95a2024-05-08 13:51:45 +0100792 }
793 .unwrap();
794
Rajesh Nyamagoudca4f7af2024-07-29 18:39:01 +0000795 let ks2 = get_keystore_service();
796 if ks2.getInterfaceVersion().unwrap() < 4 {
797 // Assuming `IKeystoreAuthorization::onDeviceLocked` and
798 // `IKeystoreAuthorization::onDeviceUnlocked` APIs will be supported on devices
799 // with `IKeystoreService` >= 4.
800 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
801 return;
802 }
David Drysdalef7ed95a2024-05-08 13:51:45 +0100803 // Now that the separate process has been forked off, it's safe to use binder.
804 let user = TestUser::new();
805 let user_id = user.id;
806 let auth_service = get_authorization();
807
808 // Lock and unlock to ensure super keys are already created.
David Drysdale89e87d52024-10-04 13:07:43 +0100809 auth_service
810 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
811 .unwrap();
812 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
813 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100814
815 info!("trigger child process action A while unlocked and wait for completion");
816 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100817 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100818
819 // Move to locked and don't allow weak unlock, so super keys are wiped.
David Drysdale89e87d52024-10-04 13:07:43 +0100820 auth_service
821 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_DISABLED)
822 .unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100823
824 info!("trigger child process action B while locked and wait for completion");
825 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100826 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100827
828 // Unlock with password => loads super key from database.
David Drysdale89e87d52024-10-04 13:07:43 +0100829 auth_service.onDeviceUnlocked(user_id, Some(SYNTHETIC_PASSWORD)).unwrap();
830 auth_service.addAuthToken(&fake_lskf_token(GK_FAKE_SID)).unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100831
832 info!("trigger child process action C while lskf-unlocked and wait for completion");
833 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100834 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100835
836 // Move to locked and allow weak unlock, then do a weak unlock.
David Drysdale89e87d52024-10-04 13:07:43 +0100837 auth_service
838 .onDeviceLocked(user_id, &[BIO_FAKE_SID1, BIO_FAKE_SID2], WEAK_UNLOCK_ENABLED)
839 .unwrap();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100840 auth_service.onDeviceUnlocked(user_id, None).unwrap();
841
842 info!("trigger child process action D while weak-unlocked and wait for completion");
843 child_handle.send(&BarrierReached {});
David Drysdale9f0007e2024-10-04 18:58:11 +0100844 child_handle.recv_or_die();
David Drysdalef7ed95a2024-05-08 13:51:45 +0100845
846 assert_eq!(child_handle.get_result(), Ok(()), "child process failed");
847}
848
849/// Generate a fake [`HardwareAuthToken`] for the given sid.
850fn fake_lskf_token(gk_sid: i64) -> HardwareAuthToken {
David Drysdale89e87d52024-10-04 13:07:43 +0100851 fake_lskf_token_with_challenge(gk_sid, 0)
852}
853
854/// Generate a fake [`HardwareAuthToken`] for the given sid and challenge.
855fn fake_lskf_token_with_challenge(gk_sid: i64, challenge: i64) -> HardwareAuthToken {
856 HardwareAuthToken {
857 challenge,
858 userId: gk_sid,
859 authenticatorId: 0,
860 authenticatorType: HardwareAuthenticatorType::PASSWORD,
861 timestamp: Timestamp { milliSeconds: 123 },
862 mac: vec![1, 2, 3],
863 }
864}
865
866/// Generate a fake [`HardwareAuthToken`] for the given sids
867fn fake_bio_lskf_token(gk_sid: i64, bio_sid: i64) -> HardwareAuthToken {
David Drysdalef7ed95a2024-05-08 13:51:45 +0100868 HardwareAuthToken {
869 challenge: 0,
870 userId: gk_sid,
David Drysdale89e87d52024-10-04 13:07:43 +0100871 authenticatorId: bio_sid,
David Drysdalef7ed95a2024-05-08 13:51:45 +0100872 authenticatorType: HardwareAuthenticatorType::PASSWORD,
873 timestamp: Timestamp { milliSeconds: 123 },
874 mac: vec![1, 2, 3],
875 }
876}