Skip to content

Commit c749243

Browse files
authored
Fingerprints are stored even on beginEnroll (#752)
* Fingerprints are stored even on beginEnroll Before we had a bug that when enrollment only needed one single sample, it wouldn't be saved to persistent storage. Also makes the fake TockEnv implementation usable with Chrome. Since it will accept all MakeCredential and GetAssertion requests with up and uv flag set without any user interaction, proceed with caution when deploying with the `fingerprint` feature. Adds some tests for the new fingerprint functionality, after checking with coveralls. * Checks that cancelling enrollment clears status
1 parent 1306426 commit c749243

File tree

3 files changed

+231
-23
lines changed

3 files changed

+231
-23
lines changed

libraries/opensk/src/ctap/fingerprint.rs

Lines changed: 184 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,6 @@ impl EnrollmentStatus {
6060
self.hardware_id.clone()
6161
}
6262

63-
/// Returns the preferred choice for the template ID returned to CTAP.
64-
pub fn external_id(&self) -> Option<Vec<u8>> {
65-
self.temporary_id.clone().or(self.hardware_id.clone())
66-
}
67-
6863
/// Checks whether the incoming template ID form CTAP is plausible.
6964
pub fn check_template_id(&self, template_id: &[u8]) -> bool {
7065
if let Some(existing_id) = &self.temporary_id {
@@ -204,6 +199,20 @@ fn check_fingerprint_loop<E: Env>(
204199
Err(Ctap2StatusCode::CTAP2_ERR_USER_ACTION_TIMEOUT)
205200
}
206201

202+
/// Helper function to move the template ID into persistent storage.
203+
fn finish_enrollment<E: Env>(
204+
env: &mut E,
205+
enrollment_status: &mut EnrollmentStatus,
206+
) -> CtapResult<()> {
207+
if let Some(template_id) = enrollment_status.internal_id() {
208+
env.persist().store_template_id(template_id)?;
209+
} else {
210+
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
211+
}
212+
enrollment_status.clear();
213+
Ok(())
214+
}
215+
207216
/// Logic for the enrollBegin subcommand.
208217
fn enroll_begin<E: Env>(
209218
env: &mut E,
@@ -216,13 +225,19 @@ fn enroll_begin<E: Env>(
216225
let (sample_status, remaining_samples, hardware_id) =
217226
env.fingerprint().capture_sample(timeout_ms)?;
218227
// We need to remember if this is a fake ID or hardware ID to overwrite it later.
219-
if let Some(template_id) = hardware_id {
228+
let template_id = if let Some(template_id) = hardware_id {
220229
enrollment_status.insert_hardware_id(&template_id)?;
230+
template_id
221231
} else {
222-
enrollment_status.insert_temporary_id(&env.rng().gen_uniform_u8x32())?;
232+
let random_id = env.rng().gen_uniform_u8x32().to_vec();
233+
enrollment_status.insert_temporary_id(&random_id)?;
234+
random_id
235+
};
236+
if remaining_samples == 0 {
237+
finish_enrollment(env, enrollment_status)?;
223238
}
224239
let response = AuthenticatorBioEnrollmentResponse {
225-
template_id: enrollment_status.external_id(),
240+
template_id: Some(template_id),
226241
last_enroll_sample_status: Some(sample_status),
227242
remaining_samples: Some(remaining_samples as u64),
228243
..Default::default()
@@ -248,12 +263,7 @@ fn enroll_capture_next_sample<E: Env>(
248263
enrollment_status.insert_hardware_id(&template_id)?;
249264
}
250265
if remaining_samples == 0 {
251-
if let Some(template_id) = enrollment_status.internal_id() {
252-
env.persist().store_template_id(template_id)?;
253-
} else {
254-
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
255-
}
256-
enrollment_status.clear();
266+
finish_enrollment(env, enrollment_status)?;
257267
}
258268
let response = AuthenticatorBioEnrollmentResponse {
259269
last_enroll_sample_status: Some(sample_status),
@@ -501,11 +511,12 @@ mod test {
501511
};
502512
}
503513

504-
fn call_subcommand(
514+
fn call_subcommand_with_status(
505515
env: &mut TestEnv,
506516
sub_command: BioEnrollmentSubCommand,
507517
sub_command_params: Option<BioEnrollmentSubCommandParams>,
508518
use_pin_uv: bool,
519+
enrollment_status: &mut EnrollmentStatus,
509520
) -> CtapResult<ResponseData> {
510521
let key_agreement_key = EcdhSk::<TestEnv>::random(env.rng());
511522
let pin_uv_auth_token = [0x55; 32];
@@ -539,14 +550,169 @@ mod test {
539550
pin_uv_auth_param,
540551
get_modality: None,
541552
};
542-
process_bio_enrollment(
553+
process_bio_enrollment(env, &mut client_pin, params, enrollment_status)
554+
}
555+
556+
fn call_subcommand(
557+
env: &mut TestEnv,
558+
sub_command: BioEnrollmentSubCommand,
559+
sub_command_params: Option<BioEnrollmentSubCommandParams>,
560+
use_pin_uv: bool,
561+
) -> CtapResult<ResponseData> {
562+
call_subcommand_with_status(
543563
env,
544-
&mut client_pin,
545-
params,
564+
sub_command,
565+
sub_command_params,
566+
use_pin_uv,
546567
&mut EnrollmentStatus::default(),
547568
)
548569
}
549570

571+
#[test]
572+
fn test_enrollment_cycle() {
573+
let mut env = TestEnv::default();
574+
let sub_command_params = BioEnrollmentSubCommandParams {
575+
template_id: None,
576+
template_friendly_name: None,
577+
timeout_milliseconds: Some(10_000),
578+
};
579+
let mut enrollment_status = EnrollmentStatus::default();
580+
let response = call_subcommand_with_status(
581+
&mut env,
582+
BioEnrollmentSubCommand::EnrollBegin,
583+
Some(sub_command_params),
584+
true,
585+
&mut enrollment_status,
586+
);
587+
let (template_id, mut remaining_samples) = match response.unwrap() {
588+
ResponseData::AuthenticatorBioEnrollment(Some(response)) => (
589+
response.template_id.unwrap(),
590+
response.remaining_samples.unwrap(),
591+
),
592+
_ => panic!("Invalid response type"),
593+
};
594+
595+
while remaining_samples > 0 {
596+
let sub_command_params = BioEnrollmentSubCommandParams {
597+
template_id: Some(template_id.clone()),
598+
template_friendly_name: None,
599+
timeout_milliseconds: Some(10_000),
600+
};
601+
let response = call_subcommand_with_status(
602+
&mut env,
603+
BioEnrollmentSubCommand::EnrollCaptureNextSample,
604+
Some(sub_command_params),
605+
true,
606+
&mut enrollment_status,
607+
);
608+
match response.unwrap() {
609+
ResponseData::AuthenticatorBioEnrollment(Some(response)) => {
610+
remaining_samples = response.remaining_samples.unwrap()
611+
}
612+
_ => panic!("Invalid response type"),
613+
};
614+
}
615+
assert_eq!(enrollment_status, EnrollmentStatus::default());
616+
617+
let response = call_subcommand(
618+
&mut env,
619+
BioEnrollmentSubCommand::EnumerateEnrollments,
620+
None,
621+
true,
622+
);
623+
// This is the actual hardware template ID that we stored.
624+
let template_id = match response.unwrap() {
625+
ResponseData::AuthenticatorBioEnrollment(Some(response)) => {
626+
response.template_infos.unwrap().pop().unwrap().template_id
627+
}
628+
_ => panic!("Invalid response type"),
629+
};
630+
631+
let sub_command_params = BioEnrollmentSubCommandParams {
632+
template_id: Some(template_id.clone()),
633+
template_friendly_name: Some(String::from("Name")),
634+
timeout_milliseconds: None,
635+
};
636+
let response = call_subcommand(
637+
&mut env,
638+
BioEnrollmentSubCommand::SetFriendlyName,
639+
Some(sub_command_params),
640+
true,
641+
);
642+
assert_eq!(response, Ok(ResponseData::AuthenticatorBioEnrollment(None)));
643+
644+
let response = call_subcommand(
645+
&mut env,
646+
BioEnrollmentSubCommand::EnumerateEnrollments,
647+
None,
648+
true,
649+
);
650+
match response.unwrap() {
651+
ResponseData::AuthenticatorBioEnrollment(Some(response)) => {
652+
let expected = AuthenticatorBioEnrollmentResponse {
653+
template_infos: Some(vec![TemplateInfo {
654+
template_id: template_id.clone(),
655+
template_friendly_name: Some(String::from("Name")),
656+
}]),
657+
..Default::default()
658+
};
659+
assert_eq!(response, expected);
660+
}
661+
_ => panic!("Invalid response type"),
662+
};
663+
664+
let sub_command_params = BioEnrollmentSubCommandParams {
665+
template_id: Some(template_id),
666+
template_friendly_name: None,
667+
timeout_milliseconds: None,
668+
};
669+
let response = call_subcommand(
670+
&mut env,
671+
BioEnrollmentSubCommand::RemoveEnrollment,
672+
Some(sub_command_params),
673+
true,
674+
);
675+
assert_eq!(response, Ok(ResponseData::AuthenticatorBioEnrollment(None)));
676+
677+
let response = call_subcommand(
678+
&mut env,
679+
BioEnrollmentSubCommand::EnumerateEnrollments,
680+
None,
681+
true,
682+
);
683+
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION));
684+
}
685+
686+
#[test]
687+
fn test_cancel_enrollment() {
688+
let mut env = TestEnv::default();
689+
let mut enrollment_status = EnrollmentStatus::default();
690+
let sub_command_params = BioEnrollmentSubCommandParams {
691+
template_id: None,
692+
template_friendly_name: None,
693+
timeout_milliseconds: Some(10_000),
694+
};
695+
let response = call_subcommand_with_status(
696+
&mut env,
697+
BioEnrollmentSubCommand::EnrollBegin,
698+
Some(sub_command_params),
699+
true,
700+
&mut enrollment_status,
701+
);
702+
assert!(response.is_ok());
703+
704+
// Cancel is a no-op if the call before succeeds immediately.
705+
let response = call_subcommand_with_status(
706+
&mut env,
707+
BioEnrollmentSubCommand::CancelCurrentEnrollment,
708+
None,
709+
false,
710+
&mut enrollment_status,
711+
);
712+
assert!(response.is_ok());
713+
assert_eq!(enrollment_status, EnrollmentStatus::default());
714+
}
715+
550716
#[test]
551717
fn test_enumerate_enrollments_no_fingerprint() {
552718
let mut env = TestEnv::default();
@@ -643,7 +809,6 @@ mod test {
643809
let mut status = EnrollmentStatus::default();
644810
status.insert_hardware_id(&[0x01]).unwrap();
645811
assert_eq!(status.internal_id(), Some(vec![0x01]));
646-
assert_eq!(status.external_id(), Some(vec![0x01]));
647812
assert!(status.check_template_id(&[0x01]));
648813
assert!(!status.check_template_id(&[0x02]));
649814
}
@@ -654,7 +819,6 @@ mod test {
654819
status.insert_temporary_id(&[0x02]).unwrap();
655820
status.insert_hardware_id(&[0x01]).unwrap();
656821
assert_eq!(status.internal_id(), Some(vec![0x01]));
657-
assert_eq!(status.external_id(), Some(vec![0x02]));
658822
assert!(!status.check_template_id(&[0x01]));
659823
assert!(status.check_template_id(&[0x02]));
660824
}
@@ -679,7 +843,6 @@ mod test {
679843
Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
680844
);
681845
assert_eq!(status.internal_id(), Some(vec![0x01]));
682-
assert_eq!(status.external_id(), Some(vec![0x01]));
683846
assert!(status.check_template_id(&[0x01]));
684847
assert!(!status.check_template_id(&[0x02]));
685848
}

libraries/opensk/src/ctap/status_code.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,46 @@ impl From<persistent_store::StoreError> for Ctap2StatusCode {
129129
}
130130
}
131131
}
132+
133+
#[cfg(test)]
134+
mod test {
135+
use super::*;
136+
137+
#[test]
138+
fn test_key_store_no_leak() {
139+
let error1 = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER;
140+
let error2 = Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR;
141+
let error1 = Ctap2StatusCode::from(key_store::Error::from(error1));
142+
let error2 = Ctap2StatusCode::from(key_store::Error::from(error2));
143+
assert_eq!(error1, error2);
144+
}
145+
146+
#[test]
147+
#[cfg(feature = "persistent_store")]
148+
fn test_persistent_store_errors() {
149+
for (store_error, ctap_error) in [
150+
(
151+
persistent_store::StoreError::NoCapacity,
152+
Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL,
153+
),
154+
(
155+
persistent_store::StoreError::NoLifetime,
156+
Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL,
157+
),
158+
(
159+
persistent_store::StoreError::InvalidArgument,
160+
Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
161+
),
162+
(
163+
persistent_store::StoreError::InvalidStorage,
164+
Ctap2StatusCode::CTAP2_ERR_VENDOR_HARDWARE_FAILURE,
165+
),
166+
(
167+
persistent_store::StoreError::StorageError,
168+
Ctap2StatusCode::CTAP1_ERR_OTHER,
169+
),
170+
] {
171+
assert_eq!(Ctap2StatusCode::from(store_error), ctap_error);
172+
}
173+
}
174+
}

src/env/tock/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
use alloc::boxed::Box;
16+
#[cfg(feature = "fingerprint")]
17+
use alloc::vec;
1618
use alloc::vec::Vec;
1719
use clock::TockClock;
1820
use core::convert::TryFrom;
@@ -356,7 +358,7 @@ where
356358
&mut self,
357359
_timeout_ms: Option<usize>,
358360
) -> CtapResult<(Ctap2EnrollFeedback, usize, Option<Vec<u8>>)> {
359-
Ok((Ctap2EnrollFeedback::FpGood, 0, None))
361+
Ok((Ctap2EnrollFeedback::FpGood, 0, Some(vec![0xF9])))
360362
}
361363

362364
fn cancel_enrollment(&mut self) -> CtapResult<()> {
@@ -384,7 +386,7 @@ where
384386
}
385387

386388
fn max_capture_samples_required_for_enroll(&self) -> usize {
387-
6
389+
1
388390
}
389391
}
390392

0 commit comments

Comments
 (0)