Skip to content

Add Support for User Verification (UV) and BioEnrollment #718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions libraries/opensk/src/api/customization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,31 @@ pub trait Customization {
/// With P=20 and K=150, we have I=2M which is enough for 500 increments per day
/// for 10 years.
fn max_supported_resident_keys(&self) -> usize;

/// This specifies the preferred number of invocations of the
/// `getPinUvAuthTokenUsingUvWithPermissions` subCommand the platform may
/// attempt before falling back to the
/// `getPinUvAuthTokenUsingPinWithPermissions` subCommand or displaying an
/// error.
///
/// MUST be greater than zero. If the value is 1 then all uvRetries are
/// internal and the platform MUST only invoke the
/// getPinUvAuthTokenUsingUvWithPermissions subCommand a single time. If the
/// value is > 1 the authenticator MUST only decrement uvRetries by 1 for
/// each iteration.
fn preferred_platform_uv_attempts(&self) -> usize;

/// Sets the number of consecutive failed User Verification attempts before
/// blocking built-in UV.
///
/// # Invariant
///
/// - CTAP2.1: Maximum PIN retries must be 25 at most.
fn max_uv_retries(&self) -> u8;

/// The maximum number of times the authenticator will retry internally when
/// internalRetry is true as part of the performBuiltInUv() algorithm.
fn max_uv_attempts_for_internal_retries(&self) -> u8;
}

#[derive(Clone)]
Expand All @@ -273,15 +298,18 @@ pub struct CustomizationImpl {
pub max_large_blob_array_size: usize,
pub max_rp_ids_length: usize,
pub max_supported_resident_keys: usize,
pub preferred_platform_uv_attempts: usize,
pub max_uv_retries: u8,
pub max_uv_attempts_for_internal_retries: u8,
}

pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl {
aaguid: &[0; AAGUID_LENGTH],
allows_pin_protocol_v1: true,
default_cred_protect: None,
default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
default_min_pin_length: 4,
default_min_pin_length_rp_ids: &[],
enforce_always_uv: false,
enforce_always_uv: true,
enterprise_attestation_mode: None,
enterprise_rp_id_list: &[],
max_msg_size: 7609,
Expand All @@ -293,6 +321,9 @@ pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl {
max_large_blob_array_size: 2048,
max_rp_ids_length: 8,
max_supported_resident_keys: 150,
preferred_platform_uv_attempts: 5,
max_uv_retries: 8,
max_uv_attempts_for_internal_retries: 8,
};

impl Customization for CustomizationImpl {
Expand Down Expand Up @@ -374,6 +405,18 @@ impl Customization for CustomizationImpl {
fn max_supported_resident_keys(&self) -> usize {
self.max_supported_resident_keys
}

fn preferred_platform_uv_attempts(&self) -> usize {
self.preferred_platform_uv_attempts
}

fn max_uv_retries(&self) -> u8 {
self.max_uv_retries
}

fn max_uv_attempts_for_internal_retries(&self) -> u8 {
self.max_uv_attempts_for_internal_retries
}
}

#[cfg(feature = "std")]
Expand Down
81 changes: 81 additions & 0 deletions libraries/opensk/src/api/fingerprint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#[derive(Debug)]
pub enum FingerprintCaptureError {
NoTouch,
ImageBad,
ImagePartial,
TooFast,
Other,
}

#[derive(Debug)]
pub enum FingerprintCheckError {
NoMatch,
Timeout,
Other,
}

pub trait Fingerprint {
/// Get the maximum number of fingerprint enrollments the sensor can
/// support.
fn get_enrollment_count_maximum(&self) -> u8;

/// Get the number of fingerprints currently enrolled.
fn get_enrollment_count(&self) -> u8;

/// Start an enrollment session for the fingerprint at the given index.
///
/// `index` should be from 0 to `get_enrollment_count_maximum()-1`.
fn prepare_enrollment(&self, index: u8);

/// Capture a fingerprint image.
///
/// `prepare_enrollment()` must be called first.
///
/// Waits for a fingerprint image or returns if `timeout_ms` happens without
/// a touch.
///
/// ## Return
///
/// `Ok(())` on success and `Err(())` on any failure.
fn capture_sample(&self, timeout_ms: usize) -> Result<(), FingerprintCaptureError>;

/// Store a fingerprint enrollment.
///
/// ## Return
///
/// `Ok(())` on success and `Err(())` on any failure.
fn commit_enrollment(&self) -> Result<(), ()>;

/// Cancel a fingerprint enrollment.
fn cancel_enrollment(&self);

/// Check all enrollments to see if they are enrolled.
fn get_enrollments(&self, fingerlist: &mut [u8; 5]);

/// Called before [`check_fingerprint()`].
///
/// Useful for starting any operation that needs to happen before
/// potentially repeated fingerprint checks, such as blinking LEDs.
fn check_fingerprint_init(&mut self);

/// Require the user to touch the sensor and verify the fingerprint is
/// valid.
///
/// Waits for a fingerprint image or returns if `timeout_ms` happens without
/// a touch.
///
/// Returns:
/// - `Ok(index)`: If fingerprint is valid, returns the index of the matched
/// fingerprint.
/// - `Err(e)`: Error if fingerprint is not valid.
fn check_fingerprint(&self, timeout_ms: usize) -> Result<u8, FingerprintCheckError>;

/// Called after checking fingerprints has finished.
fn check_fingerprint_complete(&mut self);

/// Delete the fingerprint enrolled at the given index.
fn delete_enrollment(&self, index: u8);

fn setloglevel(&self, level: u8);

}
1 change: 1 addition & 0 deletions libraries/opensk/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod clock;
pub mod connection;
pub mod crypto;
pub mod customization;
pub mod fingerprint;
pub mod firmware_protection;
pub mod key_store;
pub mod persist;
Expand Down
39 changes: 39 additions & 0 deletions libraries/opensk/src/api/persist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::ctap::status_code::{Ctap2StatusCode, CtapResult};
use crate::ctap::PIN_AUTH_LENGTH;
use alloc::borrow::Cow;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp;
use core::convert::TryFrom;
Expand Down Expand Up @@ -204,6 +205,27 @@ pub trait Persist {
self.remove(keys::PIN_RETRIES)
}

/// Returns the number of failed UV attempts.
fn uv_fails(&self) -> CtapResult<u8> {
match self.find(keys::UV_RETRIES)? {
None => Ok(0),
Some(value) if value.len() == 1 => Ok(value[0]),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Decrements the number of remaining UV retries.
fn incr_uv_fails(&mut self) -> CtapResult<()> {
let old_value = self.uv_fails()?;
let new_value = old_value.saturating_add(1);
self.insert(keys::UV_RETRIES, &[new_value])
}

/// Resets the number of remaining UV retries.
fn reset_uv_retries(&mut self) -> CtapResult<()> {
self.remove(keys::UV_RETRIES)
}

/// Returns the minimum PIN length, if stored.
fn min_pin_length(&self) -> CtapResult<Option<u8>> {
match self.find(keys::MIN_PIN_LENGTH)? {
Expand Down Expand Up @@ -386,6 +408,23 @@ pub trait Persist {
}
}

/// Store a Bio Enrollment friendly name for a given template_id.
fn store_friendly_name(&mut self, template_id: u8, friendly_name: &str) -> CtapResult<()> {
let key = keys::FRIENDLY_NAMES.start + template_id as usize;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check key < keys::FRIENDLY_NAMES.end.

self.insert(key, friendly_name.as_bytes())?;
Ok(())
}

/// Retrieve the Bio Enrollment friendly name for a given template_id.
fn get_friendly_name(&mut self, template_id: u8) -> CtapResult<String> {
let key = keys::FRIENDLY_NAMES.start + template_id as usize;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


match self.find(key)? {
None => Err(Ctap2StatusCode::CTAP1_ERR_OTHER),
Some(value) => Ok(String::from_utf8(value).unwrap_or(String::from(""))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return an internal error if the value is not UTF-8.

}
}

fn get_attestation(&self, id: AttestationId) -> CtapResult<Option<Attestation>> {
let stored_id_bytes = self.find(keys::ATTESTATION_ID)?;
if let Some(bytes) = stored_id_bytes {
Expand Down
6 changes: 6 additions & 0 deletions libraries/opensk/src/api/persist/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ make_partition! {
///
/// If the entry is absent, the counter is 0.
GLOBAL_SIGNATURE_COUNTER = 2047;

/// Stored counter UV retries.
UV_RETRIES = 2048;

/// Stored friendly names for enrolled fingerprints.
FRIENDLY_NAMES = 2100..2105;
Comment on lines +145 to +150
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those should be between 2005 and 2037, or between 21 and 999, as per the comment line 60 reserving keys 2048 and above for migration purposes.

Note that cargo test --features=std keys_are_disjoint fails otherwise.

}

#[cfg(test)]
Expand Down
Loading