CTAP library move (#602)

* Moves all CTAP logic into its own library

* workflows fix test

* more coveralls workflow tests
This commit is contained in:
kaczmarczyck
2023-03-07 15:56:46 +01:00
committed by GitHub
parent 03031e6970
commit ca65902a8f
80 changed files with 412 additions and 2000 deletions

View File

@@ -1,114 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use alloc::vec::Vec;
use persistent_store::{StoreError, StoreUpdate};
use crate::env::Env;
/// Identifies an attestation.
#[derive(Clone, PartialEq, Eq)]
pub enum Id {
Batch,
Enterprise,
}
#[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))]
pub struct Attestation {
/// ECDSA private key (big-endian).
pub private_key: [u8; 32],
pub certificate: Vec<u8>,
}
/// Stores enterprise or batch attestations.
pub trait AttestationStore {
/// Returns an attestation given its id, if it exists.
///
/// This should always return the attestation. Checking whether it is ok to use the attestation
/// is done in the CTAP library.
fn get(&mut self, id: &Id) -> Result<Option<Attestation>, Error>;
/// Sets the attestation for a given id.
///
/// This function may not be supported.
fn set(&mut self, id: &Id, attestation: Option<&Attestation>) -> Result<(), Error>;
}
/// Attestation store errors.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Storage,
Internal,
NoSupport,
}
/// Keys of the environment store reserved for the attestation store.
pub const STORAGE_KEYS: &[usize] = &[1, 2];
pub fn helper_get(env: &mut impl Env) -> Result<Option<Attestation>, Error> {
let private_key = env.store().find(PRIVATE_KEY_STORAGE_KEY)?;
let certificate = env.store().find(CERTIFICATE_STORAGE_KEY)?;
let (private_key, certificate) = match (private_key, certificate) {
(Some(x), Some(y)) => (x, y),
(None, None) => return Ok(None),
_ => return Err(Error::Internal),
};
if private_key.len() != 32 {
return Err(Error::Internal);
}
Ok(Some(Attestation {
private_key: *array_ref![private_key, 0, 32],
certificate,
}))
}
pub fn helper_set(env: &mut impl Env, attestation: Option<&Attestation>) -> Result<(), Error> {
let updates = match attestation {
None => [
StoreUpdate::Remove {
key: PRIVATE_KEY_STORAGE_KEY,
},
StoreUpdate::Remove {
key: CERTIFICATE_STORAGE_KEY,
},
],
Some(attestation) => [
StoreUpdate::Insert {
key: PRIVATE_KEY_STORAGE_KEY,
value: &attestation.private_key[..],
},
StoreUpdate::Insert {
key: CERTIFICATE_STORAGE_KEY,
value: &attestation.certificate[..],
},
],
};
Ok(env.store().transaction(&updates)?)
}
const PRIVATE_KEY_STORAGE_KEY: usize = STORAGE_KEYS[0];
const CERTIFICATE_STORAGE_KEY: usize = STORAGE_KEYS[1];
impl From<StoreError> for Error {
fn from(error: StoreError) -> Self {
match error {
StoreError::InvalidArgument
| StoreError::NoCapacity
| StoreError::NoLifetime
| StoreError::InvalidStorage => Error::Internal,
StoreError::StorageError => Error::Storage,
}
}
}

View File

@@ -1,39 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub trait Clock {
/// Stores data for the clock to recognize if this timer is elapsed or not.
///
/// The Clock does not keep track of the timers it creates. Therefore, they should not wrap
/// unexpectedly. A timer that is elapsed may never return to a non-elapsed state.
///
/// A default Timer should return `true` when checked with `is_elapsed`.
type Timer: Default;
/// Creates a new timer that expires after the given time in ms.
fn make_timer(&mut self, milliseconds: usize) -> Self::Timer;
/// Checks whether a given timer is expired.
///
/// Until a timer expires, this function consistently returns false. Once it expires, this
/// function consistently returns true. In particular, it is valid to continue calling this
/// function after the first time it returns true.
fn is_elapsed(&mut self, timer: &Self::Timer) -> bool;
/// Timestamp in microseconds.
///
/// Normal operation only needs relative time, absolute timestamps are useful for debugging.
#[cfg(feature = "debug_ctap")]
fn timestamp_us(&mut self) -> usize;
}

View File

@@ -1,49 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use core::convert::TryFrom;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum UsbEndpoint {
MainHid = 1,
#[cfg(feature = "vendor_hid")]
VendorHid = 2,
}
impl TryFrom<usize> for UsbEndpoint {
type Error = SendOrRecvError;
fn try_from(endpoint_num: usize) -> Result<Self, SendOrRecvError> {
match endpoint_num {
1 => Ok(UsbEndpoint::MainHid),
#[cfg(feature = "vendor_hid")]
2 => Ok(UsbEndpoint::VendorHid),
_ => Err(SendOrRecvError),
}
}
}
pub enum SendOrRecvStatus {
Timeout,
Sent,
Received(UsbEndpoint),
}
pub struct SendOrRecvError;
pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;
pub trait HidConnection {
fn send_and_maybe_recv(&mut self, buf: &mut [u8; 64], timeout_ms: usize) -> SendOrRecvResult;
}

View File

@@ -1,460 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This file contains all customizable constants.
//!
//! If you adapt them, make sure to run the tests before flashing the firmware.
//! Our deploy script enforces the invariants.
use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode};
use alloc::string::String;
use alloc::vec::Vec;
pub const AAGUID_LENGTH: usize = 16;
pub trait Customization {
/// Authenticator Attestation Global Unique Identifier
fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH];
// ###########################################################################
// Constants for adjusting privacy and protection levels.
// ###########################################################################
/// Removes support for PIN protocol v1.
///
/// We support PIN protocol v2, "intended to aid FIPS certification".
/// To certify, you might want to remove support for v1 using this customization.
fn allows_pin_protocol_v1(&self) -> bool;
/// Changes the default level for the credProtect extension.
///
/// You can change this value to one of the following for more privacy:
/// - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList
/// - CredentialProtectionPolicy::UserVerificationRequired
///
/// UserVerificationOptionalWithCredentialIdList
/// Resident credentials are discoverable with
/// - an allowList,
/// - an excludeList,
/// - user verification.
///
/// UserVerificationRequired
/// Resident credentials are discoverable with user verification only.
///
/// This can improve privacy, but can make usage less comfortable.
fn default_cred_protect(&self) -> Option<CredentialProtectionPolicy>;
/// Sets the initial minimum PIN length in code points.
///
/// # Invariant
///
/// - The minimum PIN length must be at least 4.
/// - The minimum PIN length must be at most 63.
/// - default_min_pin_length_rp_ids() must be non-empty if max_rp_ids_length() is 0.
///
/// Requiring longer PINs can help establish trust between users and relying
/// parties. It makes user verification harder to break, but less convenient.
/// NIST recommends at least 6-digit PINs in section 5.1.9.1:
/// https://pages.nist.gov/800-63-3/sp800-63b.html
///
/// Reset reverts the minimum PIN length to this DEFAULT_MIN_PIN_LENGTH.
fn default_min_pin_length(&self) -> u8;
/// Lists relying parties that can read the minimum PIN length.
///
/// # Invariant
///
/// - default_min_pin_length_rp_ids() must be non-empty if max_rp_ids_length() is 0
///
/// Only the RP IDs listed in default_min_pin_length_rp_ids are allowed to read
/// the minimum PIN length with the minPinLength extension.
fn default_min_pin_length_rp_ids(&self) -> Vec<String>;
/// Enforces the alwaysUv option.
///
/// When setting to true, commands require a PIN.
/// Also, alwaysUv can not be disabled by commands.
///
/// A certification (additional to FIDO Alliance's) might require enforcing
/// alwaysUv. Otherwise, users should have the choice to configure alwaysUv.
/// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here.
fn enforce_always_uv(&self) -> bool;
/// Allows usage of enterprise attestation.
///
/// # Invariant
///
/// - Enterprise and batch attestation can not both be active.
/// - If the mode is VendorFacilitated, enterprise_attestation_mode() must be non-empty.
///
/// For privacy reasons, it is disabled by default. You can choose between:
/// - EnterpriseAttestationMode::VendorFacilitated
/// - EnterpriseAttestationMode::PlatformManaged
///
/// VendorFacilitated
/// Enterprise attestation is restricted to enterprise_attestation_mode(). Add your
/// enterprises domain, e.g. "example.com", to the list below.
///
/// PlatformManaged
/// All relying parties can request an enterprise attestation. The authenticator
/// trusts the platform to filter requests.
///
/// To enable the feature, send the subcommand enableEnterpriseAttestation in
/// AuthenticatorConfig. An enterprise might want to customize the type of
/// attestation that is used. OpenSK defaults to batch attestation. Configuring
/// individual certificates then makes authenticators identifiable.
///
/// OpenSK prevents activating batch and enterprise attestation together. The
/// current implementation uses the same key material at the moment, and these
/// two modes have conflicting privacy guarantees.
/// If you implement your own enterprise attestation mechanism, and you want
/// batch attestation at the same time, proceed carefully and remove the
/// assertion.
fn enterprise_attestation_mode(&self) -> Option<EnterpriseAttestationMode>;
/// Lists relying party IDs that can perform enterprise attestation.
///
/// # Invariant
///
/// - If the mode is VendorFacilitated, enterprise_attestation_mode() must be non-empty.
///
/// This list is only considered if enterprise attestation is used.
#[cfg(feature = "std")]
fn enterprise_rp_id_list(&self) -> Vec<String>;
/// Returns whether the rp_id is contained in enterprise_rp_id_list().
fn is_enterprise_rp_id(&self, rp_id: &str) -> bool;
/// Maximum message size send for CTAP commands.
///
/// The maximum value is 7609, as HID packets can not encode longer messages.
/// 1024 is the default mentioned in the authenticatorLargeBlobs commands.
/// Larger values are preferred, as that allows more parameters in commands.
/// If long commands are too unreliable on your hardware, consider decreasing
/// this value.
fn max_msg_size(&self) -> usize;
/// Sets the number of consecutive failed PINs before blocking interaction.
///
/// # Invariant
///
/// - CTAP2.0: Maximum PIN retries must be 8.
/// - CTAP2.1: Maximum PIN retries must be 8 at most.
///
/// The fail retry counter is reset after entering the correct PIN.
fn max_pin_retries(&self) -> u8;
/// Enables or disables basic attestation for FIDO2.
///
/// # Invariant
///
/// - Enterprise and batch attestation can not both be active (see above).
///
/// The basic attestation uses the signing key configured with a vendor command
/// as a batch key. If you turn batch attestation on, be aware that it is your
/// responsibility to safely generate and store the key material. Also, the
/// batches must have size of at least 100k authenticators before using new key
/// material.
/// U2F is unaffected by this setting.
///
/// https://www.w3.org/TR/webauthn/#attestation
fn use_batch_attestation(&self) -> bool;
/// Enables or disables signature counters.
///
/// The signature counter is currently implemented as a global counter.
/// The specification strongly suggests to have per-credential counters.
/// Implementing those means you can't have an infinite amount of server-side
/// credentials anymore. Also, since counters need frequent writes on the
/// persistent storage, we might need a flash friendly implementation. This
/// solution is a compromise to be compatible with U2F and not wasting storage.
///
/// https://www.w3.org/TR/webauthn/#signature-counter
fn use_signature_counter(&self) -> bool;
// ###########################################################################
// Constants for performance optimization or adapting to different hardware.
//
// Those constants may be modified before compilation to tune the behavior of
// the key.
// ###########################################################################
/// Sets the maximum blob size stored with the credBlob extension.
///
/// # Invariant
///
/// - The length must be at least 32.
/// - OpenSK puts a limit that the length must be at most 64, as it needs to
/// be persisted in the credential ID.
fn max_cred_blob_length(&self) -> usize;
/// Limits the number of considered entries in credential lists.
///
/// # Invariant
///
/// - This value, if present, must be at least 1 (more is preferred).
///
/// Depending on your memory, you can use Some(n) to limit request sizes in
/// MakeCredential and GetAssertion. This affects allowList and excludeList.
fn max_credential_count_in_list(&self) -> Option<usize>;
/// Limits the size of largeBlobs the authenticator stores.
///
/// # Invariant
///
/// - The allowed size must be at least 1024.
/// - The array must fit into the shards reserved in storage/key.rs.
fn max_large_blob_array_size(&self) -> usize;
/// Limits the number of RP IDs that can change the minimum PIN length.
///
/// # Invariant
///
/// - If this value is 0, default_min_pin_length_rp_ids() must be non-empty.
///
/// You can use this constant to have an upper limit in storage requirements.
/// This might be useful if you want to more reliably predict the remaining
/// storage. Stored string can still be of arbitrary length though, until RP ID
/// truncation is implemented.
/// Outside of memory considerations, you can set this value to 0 if only RP IDs
/// in default_min_pin_length_rp_ids() should be allowed to change the minimum
/// PIN length.
fn max_rp_ids_length(&self) -> usize;
/// Sets the number of resident keys you can store.
///
/// # Invariant
///
/// - The storage key CREDENTIALS must fit at least this number of credentials.
///
/// Limiting the number of resident keys permits to ensure a minimum number of
/// counter increments.
/// Let:
/// - P the number of pages (NUM_PAGES in the board definition)
/// - K the maximum number of resident keys (max_supported_resident_keys())
/// - S the maximum size of a resident key (about 500)
/// - C the number of erase cycles (10000)
/// - I the minimum number of counter increments
///
/// We have: I = (P * 4084 - 5107 - K * S) / 8 * C
///
/// 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;
}
#[derive(Clone)]
pub struct CustomizationImpl {
pub aaguid: &'static [u8; AAGUID_LENGTH],
pub allows_pin_protocol_v1: bool,
pub default_cred_protect: Option<CredentialProtectionPolicy>,
pub default_min_pin_length: u8,
pub default_min_pin_length_rp_ids: &'static [&'static str],
pub enforce_always_uv: bool,
pub enterprise_attestation_mode: Option<EnterpriseAttestationMode>,
pub enterprise_rp_id_list: &'static [&'static str],
pub max_msg_size: usize,
pub max_pin_retries: u8,
pub use_batch_attestation: bool,
pub use_signature_counter: bool,
pub max_cred_blob_length: usize,
pub max_credential_count_in_list: Option<usize>,
pub max_large_blob_array_size: usize,
pub max_rp_ids_length: usize,
pub max_supported_resident_keys: usize,
}
pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl {
aaguid: &[0; AAGUID_LENGTH],
allows_pin_protocol_v1: true,
default_cred_protect: None,
default_min_pin_length: 4,
default_min_pin_length_rp_ids: &[],
enforce_always_uv: false,
enterprise_attestation_mode: None,
enterprise_rp_id_list: &[],
max_msg_size: 7609,
max_pin_retries: 8,
use_batch_attestation: false,
use_signature_counter: true,
max_cred_blob_length: 32,
max_credential_count_in_list: None,
max_large_blob_array_size: 2048,
max_rp_ids_length: 8,
max_supported_resident_keys: 150,
};
impl Customization for CustomizationImpl {
fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH] {
self.aaguid
}
fn allows_pin_protocol_v1(&self) -> bool {
self.allows_pin_protocol_v1
}
fn default_cred_protect(&self) -> Option<CredentialProtectionPolicy> {
self.default_cred_protect
}
fn default_min_pin_length(&self) -> u8 {
self.default_min_pin_length
}
fn default_min_pin_length_rp_ids(&self) -> Vec<String> {
self.default_min_pin_length_rp_ids
.iter()
.map(|s| String::from(*s))
.collect()
}
fn enforce_always_uv(&self) -> bool {
self.enforce_always_uv
}
fn enterprise_attestation_mode(&self) -> Option<EnterpriseAttestationMode> {
self.enterprise_attestation_mode
}
#[cfg(feature = "std")]
fn enterprise_rp_id_list(&self) -> Vec<String> {
self.enterprise_rp_id_list
.iter()
.map(|s| String::from(*s))
.collect()
}
fn is_enterprise_rp_id(&self, rp_id: &str) -> bool {
self.enterprise_rp_id_list.contains(&rp_id)
}
fn max_msg_size(&self) -> usize {
self.max_msg_size
}
fn max_pin_retries(&self) -> u8 {
self.max_pin_retries
}
fn use_batch_attestation(&self) -> bool {
self.use_batch_attestation
}
fn use_signature_counter(&self) -> bool {
self.use_signature_counter
}
fn max_cred_blob_length(&self) -> usize {
self.max_cred_blob_length
}
fn max_credential_count_in_list(&self) -> Option<usize> {
self.max_credential_count_in_list
}
fn max_large_blob_array_size(&self) -> usize {
self.max_large_blob_array_size
}
fn max_rp_ids_length(&self) -> usize {
self.max_rp_ids_length
}
fn max_supported_resident_keys(&self) -> usize {
self.max_supported_resident_keys
}
}
#[cfg(feature = "std")]
pub fn is_valid(customization: &impl Customization) -> bool {
// Two invariants are currently tested in different files:
// - storage.rs: if max_large_blob_array_size() fits the shards
// - storage/key.rs: if max_supported_resident_keys() fits CREDENTIALS
// Max message size must be between 1024 and 7609.
if customization.max_msg_size() < 1024 || customization.max_msg_size() > 7609 {
return false;
}
// Default min pin length must be between 4 and 63.
if customization.default_min_pin_length() < 4 || customization.default_min_pin_length() > 63 {
return false;
}
// OpenSK prevents activating batch and enterprise attestation together. The
// current implementation uses the same key material at the moment, and these
// two modes have conflicting privacy guarantees.
if customization.use_batch_attestation()
&& customization.enterprise_attestation_mode().is_some()
{
return false;
}
// enterprise_rp_id_list() should be non-empty in vendor facilitated mode.
if matches!(
customization.enterprise_attestation_mode(),
Some(EnterpriseAttestationMode::VendorFacilitated)
) && customization.enterprise_rp_id_list().is_empty()
{
return false;
}
// enterprise_rp_id_list() should be empty without an enterprise attestation mode.
if customization.enterprise_attestation_mode().is_none()
&& !customization.enterprise_rp_id_list().is_empty()
{
return false;
}
// Max pin retries must be less or equal than 8.
if customization.max_pin_retries() > 8 {
return false;
}
// Max cred blob length should be at least 32, and at most 64.
if customization.max_cred_blob_length() < 32 || customization.max_cred_blob_length() > 64 {
return false;
}
// Max credential count in list should be positive if exists.
if let Some(count) = customization.max_credential_count_in_list() {
if count < 1 {
return false;
}
}
// Max large blob array size should not be less than 1024.
if customization.max_large_blob_array_size() < 1024 {
return false;
}
// Default min pin length rp ids must be non-empty if max rp ids length is 0.
if customization.max_rp_ids_length() == 0
&& customization.default_min_pin_length_rp_ids().is_empty()
{
return false;
}
true
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_invariants() {
assert!(is_valid(&DEFAULT_CUSTOMIZATION));
}
}

View File

@@ -1,20 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub trait FirmwareProtection {
/// Locks the firmware.
///
/// Returns whether the operation was successful.
fn lock(&mut self) -> bool;
}

View File

@@ -1,149 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use alloc::vec::Vec;
use crypto::ecdsa::SecKey;
use persistent_store::StoreError;
use rng256::Rng256;
use crate::env::Env;
/// Provides storage for secret keys.
///
/// Implementations may use the environment store: [`STORAGE_KEY`] is reserved for this usage.
pub trait KeyStore {
/// Returns the AES key for key handles encryption.
fn key_handle_encryption(&mut self) -> Result<[u8; 32], Error>;
/// Returns the key for key handles authentication.
fn key_handle_authentication(&mut self) -> Result<[u8; 32], Error>;
/// Derives an ECDSA private key from a seed.
///
/// The result is big-endian.
fn derive_ecdsa(&mut self, seed: &[u8; 32]) -> Result<[u8; 32], Error>;
/// Generates a seed to derive an ECDSA private key.
fn generate_ecdsa_seed(&mut self) -> Result<[u8; 32], Error>;
/// Resets the key store.
fn reset(&mut self) -> Result<(), Error>;
}
/// Key store errors.
///
/// They are deliberately indistinguishable to avoid leaking information.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Error;
/// Key of the environment store reserved for the key store.
pub const STORAGE_KEY: usize = 2046;
/// Implements a default key store using the environment rng and store.
pub trait Helper: Env {}
impl<T: Helper> KeyStore for T {
fn key_handle_encryption(&mut self) -> Result<[u8; 32], Error> {
Ok(get_master_keys(self)?.encryption)
}
fn key_handle_authentication(&mut self) -> Result<[u8; 32], Error> {
Ok(get_master_keys(self)?.authentication)
}
fn derive_ecdsa(&mut self, seed: &[u8; 32]) -> Result<[u8; 32], Error> {
match SecKey::from_bytes(seed) {
None => Err(Error),
Some(_) => Ok(*seed),
}
}
fn generate_ecdsa_seed(&mut self) -> Result<[u8; 32], Error> {
let mut seed = [0; 32];
SecKey::gensk(self.rng()).to_bytes(&mut seed);
Ok(seed)
}
fn reset(&mut self) -> Result<(), Error> {
Ok(self.store().remove(STORAGE_KEY)?)
}
}
/// Wrapper for master keys.
struct MasterKeys {
/// Master encryption key.
encryption: [u8; 32],
/// Master authentication key.
authentication: [u8; 32],
}
fn get_master_keys(env: &mut impl Env) -> Result<MasterKeys, Error> {
let master_keys = match env.store().find(STORAGE_KEY)? {
Some(x) => x,
None => {
let master_encryption_key = env.rng().gen_uniform_u8x32();
let master_authentication_key = env.rng().gen_uniform_u8x32();
let mut master_keys = Vec::with_capacity(64);
master_keys.extend_from_slice(&master_encryption_key);
master_keys.extend_from_slice(&master_authentication_key);
env.store().insert(STORAGE_KEY, &master_keys)?;
master_keys
}
};
if master_keys.len() != 64 {
return Err(Error);
}
Ok(MasterKeys {
encryption: *array_ref![master_keys, 0, 32],
authentication: *array_ref![master_keys, 32, 32],
})
}
impl From<StoreError> for Error {
fn from(_: StoreError) -> Self {
Error
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_key_store() {
let mut env = crate::env::test::TestEnv::default();
let key_store = env.key_store();
// Master keys are well-defined and stable.
let encryption_key = key_store.key_handle_encryption().unwrap();
let authentication_key = key_store.key_handle_authentication().unwrap();
assert_eq!(key_store.key_handle_encryption(), Ok(encryption_key));
assert_eq!(
key_store.key_handle_authentication(),
Ok(authentication_key)
);
// ECDSA seeds are well-defined and stable.
let ecdsa_seed = key_store.generate_ecdsa_seed().unwrap();
let ecdsa_key = key_store.derive_ecdsa(&ecdsa_seed).unwrap();
assert_eq!(key_store.derive_ecdsa(&ecdsa_seed), Ok(ecdsa_key));
// Master keys change after reset. We don't require this for ECDSA seeds because it's not
// the case, but it might be better.
key_store.reset().unwrap();
assert!(key_store.key_handle_encryption().unwrap() != encryption_key);
assert!(key_store.key_handle_authentication().unwrap() != authentication_key);
}
}

View File

@@ -1,27 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! APIs for the environment.
//!
//! The [environment](crate::env::Env) is split into components. Each component has an API described
//! by a trait. This module gathers the API of those components.
pub mod attestation_store;
pub mod clock;
pub mod connection;
pub mod customization;
pub mod firmware_protection;
pub mod key_store;
pub mod upgrade_storage;
pub mod user_presence;

View File

@@ -1,384 +0,0 @@
// Copyright 2019-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// For compiling with std outside of tests.
#![cfg_attr(feature = "std", allow(dead_code))]
use alloc::vec::Vec;
use core::iter::Iterator;
use persistent_store::{StorageError, StorageResult};
/// Reads a slice from a list of slices.
///
/// The returned slice contains the interval `[start, start+length)`.
///
/// # Errors
///
/// Returns [`StorageError::OutOfBounds`] if the range is not within exactly one slice.
pub fn find_slice<'a>(
slices: &'a [&'a [u8]],
mut start: usize,
length: usize,
) -> StorageResult<&'a [u8]> {
for slice in slices {
if start >= slice.len() {
start -= slice.len();
continue;
}
if start + length > slice.len() {
break;
}
return Ok(&slice[start..][..length]);
}
Err(StorageError::OutOfBounds)
}
/// Checks whether the address is aligned with the block size.
///
/// Requires `block_size` to be a power of two.
pub fn is_aligned(block_size: usize, address: usize) -> bool {
debug_assert!(block_size.is_power_of_two());
address & (block_size - 1) == 0
}
/// A range implementation using start and length.
///
/// The range is treated as the interval `[start, start + length)`.
/// All objects with length of 0, regardless of the start value, are considered empty.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModRange {
start: usize,
length: usize,
}
impl ModRange {
/// Returns a new range of given start and length.
pub fn new(start: usize, length: usize) -> ModRange {
ModRange { start, length }
}
/// Create a new empty range.
pub fn new_empty() -> ModRange {
ModRange::new(0, 0)
}
/// Returns the start of the range.
pub fn start(&self) -> usize {
self.start
}
/// Returns the length of the range.
pub fn length(&self) -> usize {
self.length
}
/// Returns whether this range contains any addresses.
pub fn is_empty(&self) -> bool {
self.length == 0
}
/// Returns the disjoint union with the other range, if consecutive.
///
/// Appending empty ranges is not possible.
/// Appending to the empty range returns the other range.
///
/// Returns true if successful.
pub fn append(&mut self, other: &ModRange) -> bool {
if self.is_empty() {
self.start = other.start;
self.length = other.length;
return true;
}
if other.is_empty() {
return false;
}
if self.start >= other.start {
return false;
}
if self.length != other.start - self.start {
return false;
}
if let Some(new_length) = self.length.checked_add(other.length) {
self.length = new_length;
true
} else {
false
}
}
/// Helper function to check whether a range starts within another.
fn starts_inside(&self, range: &ModRange) -> bool {
!range.is_empty() && self.start >= range.start && self.start - range.start < range.length
}
/// Returns whether the given range has intersects.
///
/// Mathematically, we calculate whether: `self ∩ range ≠ ∅`.
pub fn intersects_range(&self, range: &ModRange) -> bool {
self.starts_inside(range) || range.starts_inside(self)
}
/// Returns whether the given range is fully contained.
///
/// Mathematically, we calculate whether: `self ∩ range = range`.
pub fn contains_range(&self, range: &ModRange) -> bool {
range.is_empty()
|| (self.start <= range.start
&& range.length <= self.length
&& range.start - self.start <= self.length - range.length)
}
/// Returns an iterator for all contained numbers that are divisible by the modulus.
pub fn aligned_iter(&self, modulus: usize) -> impl Iterator<Item = usize> {
(self.start..=usize::MAX)
.take(self.length)
// Skip the minimum number of elements to align.
.skip((modulus - self.start % modulus) % modulus)
// Only return aligned elements.
.step_by(modulus)
}
}
pub struct Partition {
ranges: Vec<ModRange>,
}
impl Partition {
pub fn new() -> Partition {
Partition { ranges: Vec::new() }
}
/// Total length of all ranges.
pub fn length(&self) -> usize {
self.ranges.iter().map(|r| r.length()).sum()
}
/// Appends the given range.
///
/// Ranges should be appending with ascending start addresses.
pub fn append(&mut self, range: ModRange) -> bool {
if let Some(last_range) = self.ranges.last_mut() {
if range.start() <= last_range.start()
|| range.start() - last_range.start() < last_range.length()
{
return false;
}
if !last_range.append(&range) {
self.ranges.push(range);
}
} else {
self.ranges.push(range);
}
true
}
/// Returns the start address that corresponds to the given offset.
///
/// If the offset bigger than the accumulated length or the requested slice doesn't fit a
/// connected component, return `None`.
pub fn find_address(&self, mut offset: usize, length: usize) -> Option<usize> {
for range in &self.ranges {
if offset < range.length() {
return if range.length() - offset >= length {
Some(range.start() + offset)
} else {
None
};
}
offset -= range.length()
}
None
}
pub fn ranges_from(&self, start_address: usize) -> Vec<ModRange> {
let mut result = Vec::new();
for range in &self.ranges {
match start_address.checked_sub(range.start()) {
None | Some(0) => result.push(range.clone()),
Some(offset) => {
if range.length() > offset {
result.push(ModRange::new(start_address, range.length() - offset));
}
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_slice_ok() {
assert_eq!(
find_slice(&[&[1, 2, 3, 4]], 0, 4).ok(),
Some(&[1u8, 2, 3, 4] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 1, 2).ok(),
Some(&[2u8, 3] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 2).ok(),
Some(&[5u8, 6] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 0).ok(),
Some(&[] as &[u8])
);
assert!(find_slice(&[], 0, 1).is_err());
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 6, 0).is_err());
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 3, 2).is_err());
}
#[test]
fn alignment() {
for exponent in 0..8 {
let block_size = 1 << exponent;
for i in 0..10 {
assert!(is_aligned(block_size, block_size * i));
}
for i in 1..block_size {
assert!(!is_aligned(block_size, block_size + i));
}
}
}
#[test]
fn mod_range_parameters() {
let range = ModRange::new(200, 100);
assert_eq!(range.start(), 200);
assert_eq!(range.length(), 100);
assert_eq!(ModRange::new_empty().length(), 0);
}
#[test]
fn mod_range_is_empty() {
assert!(!ModRange::new(200, 100).is_empty());
assert!(ModRange::new(200, 0).is_empty());
assert!(ModRange::new_empty().is_empty());
assert!(!ModRange::new(usize::MAX, 2).is_empty());
}
#[test]
fn mod_range_append() {
let mut range = ModRange::new(200, 100);
assert!(range.append(&ModRange::new(300, 400)));
assert!(range.start() == 200);
assert!(range.length() == 500);
assert!(!range.append(&ModRange::new(499, 400)));
assert!(!range.append(&ModRange::new(501, 400)));
assert!(!range.append(&ModRange::new(300, 400)));
let mut range = ModRange::new_empty();
assert!(range.append(&ModRange::new(200, 100)));
assert!(range.start() == 200);
assert!(range.length() == 100);
}
#[test]
fn mod_range_contains_range() {
let range = ModRange::new(200, 100);
assert!(!range.contains_range(&ModRange::new(199, 100)));
assert!(!range.contains_range(&ModRange::new(201, 100)));
assert!(!range.contains_range(&ModRange::new(199, 99)));
assert!(!range.contains_range(&ModRange::new(202, 99)));
assert!(!range.contains_range(&ModRange::new(200, 101)));
assert!(range.contains_range(&ModRange::new(200, 100)));
assert!(range.contains_range(&ModRange::new(200, 99)));
assert!(range.contains_range(&ModRange::new(201, 99)));
assert!(ModRange::new_empty().contains_range(&ModRange::new_empty()));
assert!(ModRange::new(usize::MAX, 1).contains_range(&ModRange::new(usize::MAX, 1)));
assert!(ModRange::new(usize::MAX, 2).contains_range(&ModRange::new(usize::MAX, 2)));
}
#[test]
fn mod_range_intersects_range() {
let range = ModRange::new(200, 100);
assert!(range.intersects_range(&ModRange::new(200, 1)));
assert!(range.intersects_range(&ModRange::new(299, 1)));
assert!(!range.intersects_range(&ModRange::new(199, 1)));
assert!(!range.intersects_range(&ModRange::new(300, 1)));
assert!(!ModRange::new_empty().intersects_range(&ModRange::new_empty()));
assert!(!ModRange::new_empty().intersects_range(&ModRange::new(200, 100)));
assert!(!ModRange::new(200, 100).intersects_range(&ModRange::new_empty()));
assert!(ModRange::new(usize::MAX, 1).intersects_range(&ModRange::new(usize::MAX, 1)));
assert!(ModRange::new(usize::MAX, 2).intersects_range(&ModRange::new(usize::MAX, 2)));
}
#[test]
fn mod_range_aligned_iter() {
let mut iter = ModRange::new(200, 100).aligned_iter(100);
assert_eq!(iter.next(), Some(200));
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(200, 101).aligned_iter(100);
assert_eq!(iter.next(), Some(200));
assert_eq!(iter.next(), Some(300));
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(199, 100).aligned_iter(100);
assert_eq!(iter.next(), Some(200));
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(201, 99).aligned_iter(100);
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(usize::MAX - 16, 20).aligned_iter(16);
assert_eq!(iter.next(), Some(0xffff_ffff_ffff_fff0));
assert_eq!(iter.next(), None);
}
#[test]
fn partition_append() {
let mut partition = Partition::new();
partition.append(ModRange::new(0x4000, 0x1000));
partition.append(ModRange::new(0x20000, 0x20000));
partition.append(ModRange::new(0x40000, 0x20000));
assert_eq!(partition.find_address(0, 1), Some(0x4000));
assert_eq!(partition.length(), 0x41000);
}
#[test]
fn partition_find_address() {
let mut partition = Partition::new();
partition.append(ModRange::new(0x4000, 0x1000));
partition.append(ModRange::new(0x20000, 0x20000));
partition.append(ModRange::new(0x40000, 0x20000));
assert_eq!(partition.find_address(0, 0x1000), Some(0x4000));
assert_eq!(partition.find_address(0x1000, 0x1000), Some(0x20000));
assert_eq!(partition.find_address(0x20000, 0x1000), Some(0x3F000));
assert_eq!(partition.find_address(0x21000, 0x1000), Some(0x40000));
assert_eq!(partition.find_address(0x40000, 0x1000), Some(0x5F000));
assert_eq!(partition.find_address(0x41000, 0x1000), None);
assert_eq!(partition.find_address(0x40000, 0x2000), None);
}
#[test]
fn partition_ranges_from() {
let mut partition = Partition::new();
partition.append(ModRange::new(0x4000, 0x1000));
partition.append(ModRange::new(0x20000, 0x20000));
partition.append(ModRange::new(0x40000, 0x20000));
let all_ranges = partition.ranges_from(0);
let from_start_ranges = partition.ranges_from(0x4000);
assert_eq!(&all_ranges, &from_start_ranges);
assert_eq!(all_ranges.len(), 2);
assert_eq!(all_ranges[0], ModRange::new(0x4000, 0x1000));
assert_eq!(all_ranges[1], ModRange::new(0x20000, 0x40000));
let second_range = partition.ranges_from(0x20000);
let same_second_range = partition.ranges_from(0x1F000);
assert_eq!(&second_range, &same_second_range);
assert_eq!(&second_range, &all_ranges[1..]);
let partial_range = partition.ranges_from(0x30000);
assert_eq!(partial_range[0], ModRange::new(0x30000, 0x30000));
}
}

View File

@@ -1,39 +0,0 @@
// Copyright 2021-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use alloc::vec::Vec;
use persistent_store::StorageResult;
pub(crate) mod helper;
/// Accessors to storage locations used for upgrading from a CTAP command.
pub trait UpgradeStorage {
/// Processes the given data as part of an upgrade.
///
/// The offset indicates the data location inside the bundle.
///
/// # Errors
///
/// - Returns [`StorageError::OutOfBounds`] if the data does not fit.
/// - Returns [`StorageError::CustomError`] if any Metadata or other check fails.
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()>;
/// Returns an identifier for the requested bundle.
///
/// Use this to determine whether you are writing to A or B.
fn bundle_identifier(&self) -> u32;
/// Returns the currently running firmware version.
fn running_firmware_version(&self) -> u64;
}

View File

@@ -1,42 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum UserPresenceError {
/// User explicitly declined user presence check.
Declined,
/// User presence check was canceled by User Agent.
Canceled,
/// User presence check timed out.
Timeout,
}
pub type UserPresenceResult = Result<(), UserPresenceError>;
pub trait UserPresence {
/// Initializes for a user presence check.
///
/// Must eventually be followed by a call to [`Self::check_complete`].
fn check_init(&mut self);
/// Waits until user presence is confirmed, rejected, or the given timeout expires.
///
/// Must be called between calls to [`Self::check_init`] and [`Self::check_complete`].
fn wait_with_timeout(&mut self, timeout_ms: usize) -> UserPresenceResult;
/// Finalizes a user presence check.
///
/// Must be called after [`Self::check_init`].
fn check_complete(&mut self);
}

View File

@@ -1,449 +0,0 @@
// Copyright 2020-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use alloc::vec::Vec;
use byteorder::{BigEndian, ByteOrder};
use core::convert::TryFrom;
use crate::api::attestation_store;
const APDU_HEADER_LEN: usize = 4;
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(non_camel_case_types, dead_code)]
pub enum ApduStatusCode {
SW_SUCCESS = 0x90_00,
/// Command successfully executed; 'XX' bytes of data are
/// available and can be requested using GET RESPONSE.
SW_GET_RESPONSE = 0x61_00,
SW_MEMERR = 0x65_01,
SW_WRONG_DATA = 0x6a_80,
SW_WRONG_LENGTH = 0x67_00,
SW_COND_USE_NOT_SATISFIED = 0x69_85,
SW_COMMAND_NOT_ALLOWED = 0x69_86,
SW_FILE_NOT_FOUND = 0x6a_82,
SW_INCORRECT_P1P2 = 0x6a_86,
/// Instruction code not supported or invalid
SW_INS_INVALID = 0x6d_00,
SW_CLA_INVALID = 0x6e_00,
SW_INTERNAL_EXCEPTION = 0x6f_00,
}
impl From<ApduStatusCode> for u16 {
fn from(code: ApduStatusCode) -> Self {
code as u16
}
}
impl From<attestation_store::Error> for ApduStatusCode {
fn from(error: attestation_store::Error) -> Self {
use attestation_store::Error;
match error {
Error::Storage => ApduStatusCode::SW_MEMERR,
Error::Internal => ApduStatusCode::SW_INTERNAL_EXCEPTION,
Error::NoSupport => ApduStatusCode::SW_INTERNAL_EXCEPTION,
}
}
}
#[allow(dead_code)]
pub enum ApduInstructions {
Select = 0xA4,
ReadBinary = 0xB0,
GetResponse = 0xC0,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[allow(dead_code)]
pub struct ApduHeader {
pub cla: u8,
pub ins: u8,
pub p1: u8,
pub p2: u8,
}
impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader {
fn from(header: &[u8; APDU_HEADER_LEN]) -> Self {
ApduHeader {
cla: header[0],
ins: header[1],
p1: header[2],
p2: header[3],
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// The APDU cases
pub enum Case {
Le1,
Lc1Data,
Lc1DataLe1,
Lc3Data,
Lc3DataLe1,
Lc3DataLe2,
Le3,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ApduType {
Instruction,
Short(Case),
Extended(Case),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub struct Apdu {
pub header: ApduHeader,
pub lc: u16,
pub data: Vec<u8>,
pub le: u32,
pub case_type: ApduType,
}
impl TryFrom<&[u8]> for Apdu {
type Error = ApduStatusCode;
fn try_from(frame: &[u8]) -> Result<Self, ApduStatusCode> {
if frame.len() < APDU_HEADER_LEN as usize {
return Err(ApduStatusCode::SW_WRONG_DATA);
}
// +-----+-----+----+----+
// header | CLA | INS | P1 | P2 |
// +-----+-----+----+----+
let (header, payload) = frame.split_at(APDU_HEADER_LEN);
if payload.is_empty() {
// Lc is zero-bytes in length
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: 0x00,
data: Vec::new(),
le: 0x00,
case_type: ApduType::Instruction,
});
}
// Lc is not zero-bytes in length, let's figure out how long it is
let byte_0 = payload[0];
if payload.len() == 1 {
// There is only one byte in the payload, that byte cannot be Lc because that would
// entail at *least* one another byte in the payload (for the command data)
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: 0x00,
data: Vec::new(),
le: if byte_0 == 0x00 {
// Ne = 256
0x100
} else {
byte_0.into()
},
case_type: ApduType::Short(Case::Le1),
});
}
if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 {
// Lc is one-byte long and since the size specified by Lc covers the rest of the
// payload there's no Le at the end
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: byte_0.into(),
data: payload[1..].to_vec(),
case_type: ApduType::Short(Case::Lc1Data),
le: 0,
});
}
if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 {
// Lc is one-byte long and since the size specified by Lc covers the rest of the
// payload with ONE additional byte that byte must be Le
let last_byte: u32 = (*payload.last().unwrap()).into();
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: byte_0.into(),
data: payload[1..(payload.len() - 1)].to_vec(),
le: if last_byte == 0x00 { 0x100 } else { last_byte },
case_type: ApduType::Short(Case::Lc1DataLe1),
});
}
if payload.len() > 2 {
// Lc is possibly three-bytes long
let extended_apdu_lc = BigEndian::read_u16(&payload[1..3]) as usize;
if payload.len() < extended_apdu_lc + 3 {
return Err(ApduStatusCode::SW_WRONG_LENGTH);
}
let extended_apdu_le_len: usize = payload
.len()
.checked_sub(extended_apdu_lc + 3)
.ok_or(ApduStatusCode::SW_WRONG_LENGTH)?;
if extended_apdu_le_len > 3 {
return Err(ApduStatusCode::SW_WRONG_LENGTH);
}
if byte_0 == 0 && extended_apdu_le_len <= 3 {
// If first byte is zero AND the next two bytes can be parsed as a big-endian
// length that covers the rest of the block (plus few additional bytes for Le), we
// have an extended-length APDU
let last_byte: u32 = (*payload.last().unwrap()).into();
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: extended_apdu_lc as u16,
data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(),
le: match extended_apdu_le_len {
0 => 0,
1 => {
if last_byte == 0x00 {
0x100
} else {
last_byte
}
}
2 => {
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
if le_parsed == 0x00 {
0x10000
} else {
le_parsed as u32
}
}
3 => {
let le_first_byte: u32 =
(*payload.get(payload.len() - 3).unwrap()).into();
if le_first_byte != 0x00 {
return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION);
}
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
if le_parsed == 0x00 {
0x10000
} else {
le_parsed as u32
}
}
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
},
case_type: ApduType::Extended(match extended_apdu_le_len {
0 => Case::Lc3Data,
1 => Case::Lc3DataLe1,
2 => Case::Lc3DataLe2,
3 => Case::Le3,
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
}),
});
}
}
Err(ApduStatusCode::SW_INTERNAL_EXCEPTION)
}
}
#[cfg(test)]
mod test {
use super::*;
fn pass_frame(frame: &[u8]) -> Result<Apdu, ApduStatusCode> {
Apdu::try_from(frame)
}
#[test]
fn test_case_type_1() {
let frame: [u8; 4] = [0x00, 0x12, 0x00, 0x80];
let response = pass_frame(&frame);
assert!(response.is_ok());
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0x12,
p1: 0x00,
p2: 0x80,
},
lc: 0x00,
data: Vec::new(),
le: 0x00,
case_type: ApduType::Instruction,
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_2_short() {
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x0f];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xb0,
p1: 0x00,
p2: 0x00,
},
lc: 0x00,
data: Vec::new(),
le: 0x0f,
case_type: ApduType::Short(Case::Le1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_2_short_le() {
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x00];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xb0,
p1: 0x00,
p2: 0x00,
},
lc: 0x00,
data: Vec::new(),
le: 0x100,
case_type: ApduType::Short(Case::Le1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_3_short() {
let frame: [u8; 7] = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
let payload = [0xe1, 0x04];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x00,
p2: 0x0c,
},
lc: 0x02,
data: payload.to_vec(),
le: 0x00,
case_type: ApduType::Short(Case::Lc1Data),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_4_short() {
let frame: [u8; 13] = [
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0xff,
];
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x04,
p2: 0x00,
},
lc: 0x07,
data: payload.to_vec(),
le: 0xff,
case_type: ApduType::Short(Case::Lc1DataLe1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_4_short_le() {
let frame: [u8; 13] = [
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00,
];
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x04,
p2: 0x00,
},
lc: 0x07,
data: payload.to_vec(),
le: 0x100,
case_type: ApduType::Short(Case::Lc1DataLe1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_invalid_apdu_header_length() {
let frame: [u8; 3] = [0x00, 0x12, 0x00];
let response = pass_frame(&frame);
assert_eq!(Err(ApduStatusCode::SW_WRONG_DATA), response);
}
#[test]
fn test_extended_length_apdu() {
let frame: [u8; 186] = [
0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0xb1, 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49,
0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8,
0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72,
0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c,
0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29,
0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16,
0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70,
0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d,
0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40,
0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80,
0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45,
0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7,
0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb,
0xb6, 0x2b, 0x00, 0x00,
];
let payload: &[u8] = &frame[7..frame.len() - 2];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0x02,
p1: 0x03,
p2: 0x00,
},
lc: 0xb1,
data: payload.to_vec(),
le: 0x10000,
case_type: ApduType::Extended(Case::Lc3DataLe2),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_previously_unsupported_case_type() {
let frame: [u8; 73] = [
0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x40, 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d,
0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3,
0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b,
0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7,
0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d,
0xcb, 0x00, 0x00,
];
let payload: &[u8] = &frame[7..frame.len() - 2];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0x01,
p1: 0x03,
p2: 0x00,
},
lc: 0x40,
data: payload.to_vec(),
le: 0x10000,
case_type: ApduType::Extended(Case::Lc3DataLe2),
};
assert_eq!(Ok(expected), response);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,489 +0,0 @@
// Copyright 2020-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorConfigParameters;
use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams};
use super::response::ResponseData;
use super::status_code::Ctap2StatusCode;
use crate::api::customization::Customization;
use crate::ctap::storage;
use crate::env::Env;
use alloc::vec;
/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig.
fn process_enable_enterprise_attestation(
env: &mut impl Env,
) -> Result<ResponseData, Ctap2StatusCode> {
if env.customization().enterprise_attestation_mode().is_some() {
storage::enable_enterprise_attestation(env)?;
Ok(ResponseData::AuthenticatorConfig)
} else {
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
}
/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig.
fn process_toggle_always_uv(env: &mut impl Env) -> Result<ResponseData, Ctap2StatusCode> {
storage::toggle_always_uv(env)?;
Ok(ResponseData::AuthenticatorConfig)
}
/// Processes the subcommand setMinPINLength for AuthenticatorConfig.
fn process_set_min_pin_length(
env: &mut impl Env,
params: SetMinPinLengthParams,
) -> Result<ResponseData, Ctap2StatusCode> {
let SetMinPinLengthParams {
new_min_pin_length,
min_pin_length_rp_ids,
force_change_pin,
} = params;
let store_min_pin_length = storage::min_pin_length(env)?;
let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length);
if new_min_pin_length < store_min_pin_length {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
let mut force_change_pin = force_change_pin.unwrap_or(false);
if force_change_pin && storage::pin_hash(env)?.is_none() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
}
if let Some(old_length) = storage::pin_code_point_length(env)? {
force_change_pin |= new_min_pin_length > old_length;
}
if force_change_pin {
storage::force_pin_change(env)?;
}
storage::set_min_pin_length(env, new_min_pin_length)?;
if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids {
storage::set_min_pin_length_rp_ids(env, min_pin_length_rp_ids)?;
}
Ok(ResponseData::AuthenticatorConfig)
}
/// Processes the AuthenticatorConfig command.
pub fn process_config<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
params: AuthenticatorConfigParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_protocol,
pin_uv_auth_param,
} = params;
let enforce_uv =
!matches!(sub_command, ConfigSubCommand::ToggleAlwaysUv) && storage::has_always_uv(env)?;
if storage::pin_hash(env)?.is_some() || enforce_uv {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
// Constants are taken from the specification, section 6.11, step 4.2.
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, sub_command as u8]);
if let Some(sub_command_params) = sub_command_params.clone() {
super::cbor_write(sub_command_params.into(), &mut config_data)?;
}
client_pin.verify_pin_uv_auth_token(
&config_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
client_pin.has_permission(PinPermission::AuthenticatorConfiguration)?;
}
match sub_command {
ConfigSubCommand::EnableEnterpriseAttestation => process_enable_enterprise_attestation(env),
ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(env),
ConfigSubCommand::SetMinPinLength => {
if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params {
process_set_min_pin_length(env, params)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
}
}
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::ctap::data_formats::PinUvAuthProtocol;
use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token;
use crate::env::test::TestEnv;
#[test]
fn test_process_enable_enterprise_attestation() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
if env.customization().enterprise_attestation_mode().is_some() {
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::enterprise_attestation(&mut env), Ok(true));
} else {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}
#[test]
fn test_process_toggle_always_uv() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(storage::has_always_uv(&mut env).unwrap());
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
if env.customization().enforce_always_uv() {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
);
} else {
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(!storage::has_always_uv(&mut env).unwrap());
}
}
fn test_helper_process_toggle_always_uv_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
pin_uv_auth_protocol,
);
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]);
let pin_uv_auth_param =
authenticate_pin_uv_auth_token(&pin_uv_auth_token, &config_data, pin_uv_auth_protocol);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
if env.customization().enforce_always_uv() {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
);
return;
}
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(storage::has_always_uv(&mut env).unwrap());
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: Some(pin_uv_auth_param),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(!storage::has_always_uv(&mut env).unwrap());
}
#[test]
fn test_process_toggle_always_uv_with_pin_v1() {
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_toggle_always_uv_with_pin_v2() {
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V2);
}
fn create_min_pin_config_params(
min_pin_length: u8,
min_pin_length_rp_ids: Option<Vec<String>>,
) -> AuthenticatorConfigParameters {
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(min_pin_length),
min_pin_length_rp_ids,
force_change_pin: None,
};
AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
}
}
#[test]
fn test_process_set_min_pin_length() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
// First, increase minimum PIN length from 4 to 6 without PIN auth.
let min_pin_length = 6;
let config_params = create_min_pin_config_params(min_pin_length, None);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
// Second, increase minimum PIN length from 6 to 8 with PIN auth.
// The stored PIN or its length don't matter since we control the token.
storage::set_pin(&mut env, &[0x88; 16], 8).unwrap();
let min_pin_length = 8;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_uv_auth_param = vec![
0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29,
0xB2, 0xDE,
];
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
// Third, decreasing the minimum PIN length from 8 to 7 fails.
let mut config_params = create_min_pin_config_params(7, None);
let pin_uv_auth_param = vec![
0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8,
0xA7, 0x71,
];
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
);
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
}
#[test]
fn test_process_set_min_pin_length_rp_ids() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
// First, set RP IDs without PIN auth.
let min_pin_length = 6;
let min_pin_length_rp_ids = vec!["example.com".to_string()];
let config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids)
);
// Second, change the RP IDs with PIN auth.
let min_pin_length = 8;
let min_pin_length_rp_ids = vec!["another.example.com".to_string()];
// The stored PIN or its length don't matter since we control the token.
storage::set_pin(&mut env, &[0x88; 16], 8).unwrap();
let mut config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let pin_uv_auth_param = vec![
0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D,
0xD6, 0xDA,
];
config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone());
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids.clone())
);
// Third, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different lengths.
let mut config_params =
create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone()));
config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone());
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids.clone())
);
// Forth, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different RP IDs.
let mut config_params = create_min_pin_config_params(
min_pin_length,
Some(vec!["counter.example.com".to_string()]),
);
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids)
);
}
#[test]
fn test_process_set_min_pin_length_force_pin_change_implicit() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
// Increase min PIN, force PIN change.
let min_pin_length = 6;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_uv_auth_param = Some(vec![
0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E,
0xA8, 0xC8,
]);
config_params.pin_uv_auth_param = pin_uv_auth_param;
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(storage::has_force_pin_change(&mut env), Ok(true));
}
#[test]
fn test_process_set_min_pin_length_force_pin_change_explicit() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1,
0x53, 0xD7,
]);
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(storage::min_pin_length(&mut env).unwrap()),
min_pin_length_rp_ids: None,
force_change_pin: Some(true),
};
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::has_force_pin_change(&mut env), Ok(true));
}
#[test]
fn test_process_config_vendor_prototype() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::VendorPrototype,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}

View File

@@ -1,494 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt, PrivateKey};
use super::data_formats::{
CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType,
};
use super::status_code::Ctap2StatusCode;
use super::{cbor_read, cbor_write};
use crate::api::key_store::KeyStore;
use crate::ctap::data_formats::{extract_byte_string, extract_map};
use crate::env::Env;
use alloc::string::String;
use alloc::vec::Vec;
use core::convert::{TryFrom, TryInto};
use crypto::hmac::{hmac_256, verify_hmac_256};
use crypto::sha256::Sha256;
use sk_cbor::{cbor_map_options, destructure_cbor_map};
pub const LEGACY_CREDENTIAL_ID_SIZE: usize = 112;
// CBOR credential IDs consist of
// - 1 byte : version number
// - 16 bytes: initialization vector for AES-256,
// - 192 bytes: encrypted block of the key handle cbor,
// - 32 bytes: HMAC-SHA256 over everything else.
pub const CBOR_CREDENTIAL_ID_SIZE: usize = 241;
pub const MIN_CREDENTIAL_ID_SIZE: usize = LEGACY_CREDENTIAL_ID_SIZE;
pub const MAX_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE;
pub const CBOR_CREDENTIAL_ID_VERSION: u8 = 0x01;
pub const MAX_PADDING_LENGTH: u8 = 0xBF;
// Data fields that are contained in the credential ID of non-discoverable credentials.
struct CredentialSource {
private_key: PrivateKey,
rp_id_hash: [u8; 32],
cred_protect_policy: Option<CredentialProtectionPolicy>,
cred_blob: Option<Vec<u8>>,
}
// The data fields contained in the credential ID are serialized using CBOR maps.
// Each field is associated with a unique tag, implemented with a CBOR unsigned key.
enum CredentialSourceField {
PrivateKey = 0,
RpIdHash = 1,
CredProtectPolicy = 2,
CredBlob = 3,
}
impl From<CredentialSourceField> for sk_cbor::Value {
fn from(field: CredentialSourceField) -> sk_cbor::Value {
(field as u64).into()
}
}
fn decrypt_legacy_credential_id(
env: &mut impl Env,
bytes: &[u8],
) -> Result<Option<CredentialSource>, Ctap2StatusCode> {
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let plaintext = aes256_cbc_decrypt(&aes_enc_key, bytes, true)?;
if plaintext.len() != 64 {
return Ok(None);
}
let private_key = if let Some(key) = PrivateKey::new_ecdsa_from_bytes(&plaintext[..32]) {
key
} else {
return Ok(None);
};
Ok(Some(CredentialSource {
private_key,
rp_id_hash: plaintext[32..64].try_into().unwrap(),
cred_protect_policy: None,
cred_blob: None,
}))
}
fn decrypt_cbor_credential_id(
env: &mut impl Env,
bytes: &[u8],
) -> Result<Option<CredentialSource>, Ctap2StatusCode> {
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let mut plaintext = aes256_cbc_decrypt(&aes_enc_key, bytes, true)?;
remove_padding(&mut plaintext)?;
let cbor_credential_source = cbor_read(plaintext.as_slice())?;
destructure_cbor_map! {
let {
CredentialSourceField::PrivateKey => private_key,
CredentialSourceField::RpIdHash=> rp_id_hash,
CredentialSourceField::CredProtectPolicy => cred_protect_policy,
CredentialSourceField::CredBlob => cred_blob,
} = extract_map(cbor_credential_source)?;
}
Ok(match (private_key, rp_id_hash) {
(Some(private_key), Some(rp_id_hash)) => {
let private_key = PrivateKey::try_from(private_key)?;
let rp_id_hash = extract_byte_string(rp_id_hash)?;
if rp_id_hash.len() != 32 {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let cred_protect_policy = cred_protect_policy
.map(CredentialProtectionPolicy::try_from)
.transpose()?;
let cred_blob = cred_blob.map(extract_byte_string).transpose()?;
Some(CredentialSource {
private_key,
rp_id_hash: rp_id_hash.try_into().unwrap(),
cred_protect_policy,
cred_blob,
})
}
_ => None,
})
}
/// Pad data to MAX_PADDING_LENGTH+1 (192) bytes using PKCS padding scheme.
/// Let N = 192 - data.len(), the PKCS padding scheme would pad N bytes of N after the data.
fn add_padding(data: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
// The data should be between 1 to MAX_PADDING_LENGTH bytes for the padding scheme to be valid.
if data.is_empty() || data.len() > MAX_PADDING_LENGTH as usize {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let pad_length = MAX_PADDING_LENGTH - (data.len() as u8 - 1);
data.extend(core::iter::repeat(pad_length).take(pad_length as usize));
Ok(())
}
fn remove_padding(data: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
if data.len() != MAX_PADDING_LENGTH as usize + 1 {
// This is an internal error instead of corrupted credential ID which we should just ignore because
// we've already checked that the HMAC matched.
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let pad_length = *data.last().unwrap();
if pad_length == 0 || pad_length > MAX_PADDING_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
if !data
.drain((data.len() - pad_length as usize)..)
.all(|x| x == pad_length)
{
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
Ok(())
}
/// Encrypts the given private key, relying party ID hash, and some other metadata into a credential ID.
///
/// Other information, such as a user name, are not stored. Since encrypted credential IDs are
/// stored server-side, this information is already available (unencrypted).
pub fn encrypt_to_credential_id(
env: &mut impl Env,
private_key: &PrivateKey,
rp_id_hash: &[u8; 32],
cred_protect_policy: Option<CredentialProtectionPolicy>,
cred_blob: Option<Vec<u8>>,
) -> Result<Vec<u8>, Ctap2StatusCode> {
let mut payload = Vec::new();
let cbor = cbor_map_options! {
CredentialSourceField::PrivateKey => private_key,
CredentialSourceField::RpIdHash=> rp_id_hash,
CredentialSourceField::CredProtectPolicy => cred_protect_policy,
CredentialSourceField::CredBlob => cred_blob,
};
cbor_write(cbor, &mut payload)?;
add_padding(&mut payload)?;
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let encrypted_payload = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &payload, true)?;
let mut credential_id = encrypted_payload;
credential_id.insert(0, CBOR_CREDENTIAL_ID_VERSION);
let id_hmac = hmac_256::<Sha256>(
&env.key_store().key_handle_authentication()?,
&credential_id[..],
);
credential_id.extend(&id_hmac);
Ok(credential_id)
}
/// Decrypts the given credential ID into a PublicKeyCredentialSource, populating only the recorded fields.
///
/// Returns None if
/// - the format does not match any known versions, or
/// - the HMAC test fails.
///
/// For v0 (legacy U2F) the credential ID consists of:
/// - 16 bytes: initialization vector for AES-256,
/// - 32 bytes: encrypted ECDSA private key for the credential,
/// - 32 bytes: encrypted relying party ID hashed with SHA256,
/// - 32 bytes: HMAC-SHA256 over everything else.
///
/// For v1 (CBOR) the credential ID consists of:
/// - 1 byte : version number,
/// - 16 bytes: initialization vector for AES-256,
/// - 192 bytes: encrypted CBOR-encoded credential source fields,
/// - 32 bytes: HMAC-SHA256 over everything else.
pub fn decrypt_credential_id(
env: &mut impl Env,
credential_id: Vec<u8>,
rp_id_hash: &[u8],
) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> {
if credential_id.len() < MIN_CREDENTIAL_ID_SIZE {
return Ok(None);
}
let hmac_message_size = credential_id.len() - 32;
if !verify_hmac_256::<Sha256>(
&env.key_store().key_handle_authentication()?,
&credential_id[..hmac_message_size],
array_ref![credential_id, hmac_message_size, 32],
) {
return Ok(None);
}
let credential_source = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE {
decrypt_legacy_credential_id(env, &credential_id[..hmac_message_size])?
} else {
match credential_id[0] {
CBOR_CREDENTIAL_ID_VERSION => {
if credential_id.len() != CBOR_CREDENTIAL_ID_SIZE {
return Ok(None);
}
decrypt_cbor_credential_id(env, &credential_id[1..hmac_message_size])?
}
_ => return Ok(None),
}
};
let credential_source = if let Some(credential_source) = credential_source {
credential_source
} else {
return Ok(None);
};
if rp_id_hash != credential_source.rp_id_hash {
return Ok(None);
}
Ok(Some(PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id,
private_key: credential_source.private_key,
rp_id: String::new(),
user_handle: Vec::new(),
user_display_name: None,
cred_protect_policy: credential_source.cred_protect_policy,
creation_order: 0,
user_name: None,
user_icon: None,
cred_blob: credential_source.cred_blob,
large_blob_key: None,
}))
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::ctap::credential_id::CBOR_CREDENTIAL_ID_SIZE;
use crate::ctap::SignatureAlgorithm;
use crate::env::test::TestEnv;
use crypto::hmac::hmac_256;
const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80;
fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(private_key, decrypted_source.private_key);
}
#[test]
fn test_encrypt_decrypt_ecdsa_credential() {
test_encrypt_decrypt_credential(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_encrypt_decrypt_ed25519_credential() {
test_encrypt_decrypt_credential(SignatureAlgorithm::Eddsa);
}
#[test]
fn test_encrypt_decrypt_bad_version() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let mut encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION;
// Override the HMAC to pass the check.
encrypted_id.truncate(&encrypted_id.len() - 32);
let hmac_key = env.key_store().key_handle_authentication().unwrap();
let id_hmac = hmac_256::<Sha256>(&hmac_key, &encrypted_id[..]);
encrypted_id.extend(&id_hmac);
assert_eq!(
decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash),
Ok(None)
);
}
fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
for i in 0..encrypted_id.len() {
let mut modified_id = encrypted_id.clone();
modified_id[i] ^= 0x01;
assert_eq!(
decrypt_credential_id(&mut env, modified_id, &rp_id_hash),
Ok(None)
);
}
}
#[test]
fn test_ecdsa_encrypt_decrypt_bad_hmac() {
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_encrypt_decrypt_bad_hmac() {
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Eddsa);
}
fn test_decrypt_credential_missing_blocks(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
for length in (1..CBOR_CREDENTIAL_ID_SIZE).step_by(16) {
assert_eq!(
decrypt_credential_id(&mut env, encrypted_id[..length].to_vec(), &rp_id_hash),
Ok(None)
);
}
}
#[test]
fn test_ecdsa_decrypt_credential_missing_blocks() {
test_decrypt_credential_missing_blocks(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_decrypt_credential_missing_blocks() {
test_decrypt_credential_missing_blocks(SignatureAlgorithm::Eddsa);
}
/// This is a copy of the function that genereated deprecated key handles.
fn legacy_encrypt_to_credential_id(
env: &mut impl Env,
private_key: crypto::ecdsa::SecKey,
application: &[u8; 32],
) -> Result<Vec<u8>, Ctap2StatusCode> {
let aes_enc_key =
crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let mut plaintext = [0; 64];
private_key.to_bytes(array_mut_ref!(plaintext, 0, 32));
plaintext[32..64].copy_from_slice(application);
let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?;
let id_hmac = hmac_256::<Sha256>(
&env.key_store().key_handle_authentication()?,
&encrypted_id[..],
);
encrypted_id.extend(&id_hmac);
Ok(encrypted_id)
}
#[test]
fn test_encrypt_decrypt_credential_legacy() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new_ecdsa(&mut env);
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
let rp_id_hash = [0x55; 32];
let encrypted_id =
legacy_encrypt_to_credential_id(&mut env, ecdsa_key, &rp_id_hash).unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(private_key, decrypted_source.private_key);
// Legacy credentials didn't persist credProtectPolicy info, so it should be treated as None.
assert!(decrypted_source.cred_protect_policy.is_none());
}
#[test]
fn test_encrypt_credential_size() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
assert_eq!(encrypted_id.len(), CBOR_CREDENTIAL_ID_SIZE);
}
#[test]
fn test_encrypt_credential_max_cbor_size() {
// The cbor encoding length is variadic and depends on size of fields. Try to put maximum length
// for each encoded field and ensure that it doesn't go over the padding size.
let mut env = TestEnv::default();
// Currently all private key types have same length when transformed to bytes.
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let cred_protect_policy = Some(CredentialProtectionPolicy::UserVerificationOptional);
let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]);
let encrypted_id = encrypt_to_credential_id(
&mut env,
&private_key,
&rp_id_hash,
cred_protect_policy,
cred_blob,
);
assert!(encrypted_id.is_ok());
}
#[test]
fn test_cred_protect_persisted() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let encrypted_id = encrypt_to_credential_id(
&mut env,
&private_key,
&rp_id_hash,
Some(CredentialProtectionPolicy::UserVerificationRequired),
None,
)
.unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(decrypted_source.private_key, private_key);
assert_eq!(
decrypted_source.cred_protect_policy,
Some(CredentialProtectionPolicy::UserVerificationRequired)
);
}
#[test]
fn test_cred_blob_persisted() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]);
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, cred_blob.clone())
.unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(decrypted_source.private_key, private_key);
assert_eq!(decrypted_source.cred_blob, cred_blob);
}
}

View File

@@ -1,917 +0,0 @@
// Copyright 2020-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorCredentialManagementParameters;
use super::data_formats::{
CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource,
PublicKeyCredentialUserEntity,
};
use super::response::{AuthenticatorCredentialManagementResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::{Channel, StatefulCommand, StatefulPermission};
use crate::ctap::storage;
use crate::env::Env;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crypto::sha256::Sha256;
use crypto::Hash256;
/// Generates a set with all existing RP IDs.
fn get_stored_rp_ids(env: &mut impl Env) -> Result<BTreeSet<String>, Ctap2StatusCode> {
let mut rp_set = BTreeSet::new();
let mut iter_result = Ok(());
for (_, credential) in storage::iter_credentials(env, &mut iter_result)? {
rp_set.insert(credential.rp_id);
}
iter_result?;
Ok(rp_set)
}
/// Generates the response for subcommands enumerating RPs.
fn enumerate_rps_response(
rp_id: String,
total_rps: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = Some(Sha256::hash(rp_id.as_bytes()).to_vec());
let rp = Some(PublicKeyCredentialRpEntity {
rp_id,
rp_name: None,
rp_icon: None,
});
Ok(AuthenticatorCredentialManagementResponse {
rp,
rp_id_hash,
total_rps,
..Default::default()
})
}
/// Generates the response for subcommands enumerating credentials.
fn enumerate_credentials_response(
env: &mut impl Env,
credential: PublicKeyCredentialSource,
total_credentials: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let PublicKeyCredentialSource {
key_type,
credential_id,
private_key,
rp_id: _,
user_handle,
user_display_name,
cred_protect_policy,
creation_order: _,
user_name,
user_icon,
cred_blob: _,
large_blob_key,
} = credential;
let user = PublicKeyCredentialUserEntity {
user_id: user_handle,
user_name,
user_display_name,
user_icon,
};
let credential_id = PublicKeyCredentialDescriptor {
key_type,
key_id: credential_id,
transports: None, // You can set USB as a hint here.
};
let public_key = private_key.get_pub_key(env)?;
Ok(AuthenticatorCredentialManagementResponse {
user: Some(user),
credential_id: Some(credential_id),
public_key: Some(public_key),
total_credentials,
cred_protect: cred_protect_policy,
large_blob_key,
..Default::default()
})
}
/// Check if the token permissions have the correct associated RP ID.
///
/// Either no RP ID is associated, or the RP ID matches the stored credential.
fn check_rp_id_permissions<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
credential_id: &[u8],
) -> Result<(), Ctap2StatusCode> {
// Pre-check a sufficient condition before calling the store.
if client_pin.has_no_rp_id_permission().is_ok() {
return Ok(());
}
let (_, credential) = storage::find_credential_item(env, credential_id)?;
client_pin.has_no_or_rp_id_permission(&credential.rp_id)
}
/// Processes the subcommand getCredsMetadata for CredentialManagement.
fn process_get_creds_metadata(
env: &mut impl Env,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
Ok(AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(storage::count_credentials(env)? as u64),
max_possible_remaining_resident_credentials_count: Some(
storage::remaining_credentials(env)? as u64,
),
..Default::default()
})
}
/// Processes the subcommand enumerateRPsBegin for CredentialManagement.
fn process_enumerate_rps_begin<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
channel: Channel,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_set = get_stored_rp_ids(env)?;
let total_rps = rp_set.len();
if total_rps > 1 {
stateful_command_permission.set_command(env, StatefulCommand::EnumerateRps(1), channel);
}
// TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first()
let rp_id = rp_set
.into_iter()
.next()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
enumerate_rps_response(rp_id, Some(total_rps as u64))
}
/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement.
fn process_enumerate_rps_get_next_rp<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_index = stateful_command_permission.next_enumerate_rp(env)?;
let rp_set = get_stored_rp_ids(env)?;
// A BTreeSet is already sorted.
let rp_id = rp_set
.into_iter()
.nth(rp_id_index)
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
enumerate_rps_response(rp_id, None)
}
/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement.
fn process_enumerate_credentials_begin<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
client_pin: &mut ClientPin<E>,
sub_command_params: CredentialManagementSubCommandParameters,
channel: Channel,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = sub_command_params
.rp_id_hash
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?;
let mut iter_result = Ok(());
let iter = storage::iter_credentials(env, &mut iter_result)?;
let mut rp_credentials: Vec<usize> = iter
.filter_map(|(key, credential)| {
let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes());
if cred_rp_id_hash == rp_id_hash.as_slice() {
Some(key)
} else {
None
}
})
.collect();
iter_result?;
let total_credentials = rp_credentials.len();
let current_key = rp_credentials
.pop()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
let credential = storage::get_credential(env, current_key)?;
if total_credentials > 1 {
stateful_command_permission.set_command(
env,
StatefulCommand::EnumerateCredentials(rp_credentials),
channel,
);
}
enumerate_credentials_response(env, credential, Some(total_credentials as u64))
}
/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement.
fn process_enumerate_credentials_get_next_credential<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let credential_key = stateful_command_permission.next_enumerate_credential(env)?;
let credential = storage::get_credential(env, credential_key)?;
enumerate_credentials_response(env, credential, None)
}
/// Processes the subcommand deleteCredential for CredentialManagement.
fn process_delete_credential<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
check_rp_id_permissions(env, client_pin, &credential_id)?;
storage::delete_credential(env, &credential_id)
}
/// Processes the subcommand updateUserInformation for CredentialManagement.
fn process_update_user_information<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
let user = sub_command_params
.user
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
check_rp_id_permissions(env, client_pin, &credential_id)?;
storage::update_credential(env, &credential_id, user)
}
/// Processes the CredentialManagement command and all its subcommands.
pub fn process_credential_management<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
client_pin: &mut ClientPin<E>,
cred_management_params: AuthenticatorCredentialManagementParameters,
channel: Channel,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorCredentialManagementParameters {
sub_command,
sub_command_params,
pin_uv_auth_protocol,
pin_uv_auth_param,
} = cred_management_params;
match (sub_command, stateful_command_permission.get_command(env)) {
(
CredentialManagementSubCommand::EnumerateRpsGetNextRp,
Ok(StatefulCommand::EnumerateRps(_)),
)
| (
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
Ok(StatefulCommand::EnumerateCredentials(_)),
) => (),
(_, _) => {
stateful_command_permission.clear();
}
}
match sub_command {
CredentialManagementSubCommand::GetCredsMetadata
| CredentialManagementSubCommand::EnumerateRpsBegin
| CredentialManagementSubCommand::EnumerateCredentialsBegin
| CredentialManagementSubCommand::DeleteCredential
| CredentialManagementSubCommand::UpdateUserInformation => {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut management_data = vec![sub_command as u8];
if let Some(sub_command_params) = sub_command_params.clone() {
super::cbor_write(sub_command_params.into(), &mut management_data)?;
}
client_pin.verify_pin_uv_auth_token(
&management_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
// The RP ID permission is handled differently per subcommand below.
client_pin.has_permission(PinPermission::CredentialManagement)?;
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp
| CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {}
}
let response = match sub_command {
CredentialManagementSubCommand::GetCredsMetadata => {
client_pin.has_no_rp_id_permission()?;
Some(process_get_creds_metadata(env)?)
}
CredentialManagementSubCommand::EnumerateRpsBegin => {
client_pin.has_no_rp_id_permission()?;
Some(process_enumerate_rps_begin(
env,
stateful_command_permission,
channel,
)?)
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some(
process_enumerate_rps_get_next_rp(env, stateful_command_permission)?,
),
CredentialManagementSubCommand::EnumerateCredentialsBegin => {
Some(process_enumerate_credentials_begin(
env,
stateful_command_permission,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
channel,
)?)
}
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => Some(
process_enumerate_credentials_get_next_credential(env, stateful_command_permission)?,
),
CredentialManagementSubCommand::DeleteCredential => {
process_delete_credential(
env,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
CredentialManagementSubCommand::UpdateUserInformation => {
process_update_user_information(
env,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
};
Ok(ResponseData::AuthenticatorCredentialManagement(response))
}
#[cfg(test)]
mod test {
use super::super::crypto_wrapper::PrivateKey;
use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType};
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::super::CtapState;
use super::*;
use crate::env::test::TestEnv;
use rng256::Rng256;
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
fn create_credential_source(env: &mut TestEnv) -> PublicKeyCredentialSource {
let private_key = PrivateKey::new_ecdsa(env);
PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: env.rng().gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x01],
user_display_name: Some("display_name".to_string()),
cred_protect_policy: None,
creation_order: 0,
user_name: Some("name".to_string()),
user_icon: Some("icon".to_string()),
cred_blob: None,
large_blob_key: None,
}
}
fn test_helper_process_get_creds_metadata(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
pin_uv_auth_protocol,
);
let credential_source = create_credential_source(&mut env);
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8];
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
&pin_uv_auth_token,
&management_data,
pin_uv_auth_protocol,
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let initial_capacity = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(0));
response
.max_possible_remaining_resident_credentials_count
.unwrap()
}
_ => panic!("Invalid response type"),
};
storage::store_credential(&mut env, credential_source).unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
pin_uv_auth_param: Some(pin_uv_auth_param),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(1));
assert_eq!(
response.max_possible_remaining_resident_credentials_count,
Some(initial_capacity - 1)
);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_get_creds_metadata_v1() {
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_creds_metadata_v2() {
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V2);
}
#[test]
fn test_process_enumerate_rps_with_uv() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let credential_source1 = create_credential_source(&mut env);
let mut credential_source2 = create_credential_source(&mut env);
credential_source2.rp_id = "another.example.com".to_string();
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source1).unwrap();
storage::store_credential(&mut env, credential_source2).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let first_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, Some(2));
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let second_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, None);
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
assert!(first_rp_id != second_rp_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_rps_completeness() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let credential_source = create_credential_source(&mut env);
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
const NUM_CREDENTIALS: usize = 20;
for i in 0..NUM_CREDENTIALS {
let mut credential = credential_source.clone();
credential.rp_id = i.to_string();
storage::store_credential(&mut env, credential).unwrap();
}
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let mut rp_set = BTreeSet::new();
// This mut is just to make the test code shorter.
// The command is different on the first loop iteration.
let mut cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
for _ in 0..NUM_CREDENTIALS {
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
if rp_set.is_empty() {
assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64));
} else {
assert_eq!(response.total_rps, None);
}
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
assert!(!rp_set.contains(&rp_id));
rp_set.insert(rp_id);
}
_ => panic!("Invalid response type"),
};
cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
}
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_credentials_with_uv() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let credential_source1 = create_credential_source(&mut env);
let mut credential_source2 = create_credential_source(&mut env);
credential_source2.user_handle = vec![0x02];
credential_source2.user_name = Some("user2".to_string());
credential_source2.user_display_name = Some("User Two".to_string());
credential_source2.user_icon = Some("icon2".to_string());
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source1).unwrap();
storage::store_credential(&mut env, credential_source2).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28,
0x2B, 0x68,
]);
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()),
credential_id: None,
user: None,
};
// RP ID hash:
// A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let first_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, Some(2));
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let second_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, None);
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
assert!(first_credential_id != second_credential_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_delete_credential() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut credential_source = create_credential_source(&mut env);
credential_source.credential_id = vec![0x1D; 32];
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89,
0x9C, 0x64,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: None,
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params.clone()),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param: pin_uv_auth_param.clone(),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
);
}
#[test]
fn test_process_update_user_information() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut credential_source = create_credential_source(&mut env);
credential_source.credential_id = vec![0x1D; 32];
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD,
0x9D, 0xB7,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let new_user = PublicKeyCredentialUserEntity {
user_id: vec![0xFF],
user_name: Some("new_name".to_string()),
user_display_name: Some("new_display_name".to_string()),
user_icon: Some("new_icon".to_string()),
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: Some(new_user),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::UpdateUserInformation,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let updated_credential = storage::find_credential(&mut env, "example.com", &[0x1D; 32])
.unwrap()
.unwrap();
assert_eq!(updated_credential.user_handle, vec![0x01]);
assert_eq!(&updated_credential.user_name.unwrap(), "new_name");
assert_eq!(
&updated_credential.user_display_name.unwrap(),
"new_display_name"
);
assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon");
}
#[test]
fn test_process_credential_management_invalid_pin_uv_auth_param() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::new(&mut env);
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param: Some(vec![0u8; 16]),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

View File

@@ -1,428 +0,0 @@
// Copyright 2021-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::key_store::KeyStore;
use crate::ctap::data_formats::{extract_array, extract_byte_string, CoseKey, SignatureAlgorithm};
use crate::ctap::status_code::Ctap2StatusCode;
use crate::env::Env;
use alloc::vec;
use alloc::vec::Vec;
use core::convert::TryFrom;
use crypto::cbc::{cbc_decrypt, cbc_encrypt};
use crypto::ecdsa;
use crypto::sha256::Sha256;
use rng256::Rng256;
use sk_cbor as cbor;
use sk_cbor::{cbor_array, cbor_bytes, cbor_int};
/// Wraps the AES256-CBC encryption to match what we need in CTAP.
pub fn aes256_cbc_encrypt(
rng: &mut dyn Rng256,
aes_enc_key: &crypto::aes256::EncryptionKey,
plaintext: &[u8],
embeds_iv: bool,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if plaintext.len() % 16 != 0 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
// The extra 1 capacity is because encrypt_key_handle adds a version number.
let mut ciphertext = Vec::with_capacity(plaintext.len() + 16 * embeds_iv as usize + 1);
let iv = if embeds_iv {
let random_bytes = rng.gen_uniform_u8x32();
ciphertext.extend_from_slice(&random_bytes[..16]);
*array_ref!(ciphertext, 0, 16)
} else {
[0u8; 16]
};
let start = ciphertext.len();
ciphertext.extend_from_slice(plaintext);
cbc_encrypt(aes_enc_key, iv, &mut ciphertext[start..]);
Ok(ciphertext)
}
/// Wraps the AES256-CBC decryption to match what we need in CTAP.
pub fn aes256_cbc_decrypt(
aes_enc_key: &crypto::aes256::EncryptionKey,
ciphertext: &[u8],
embeds_iv: bool,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if ciphertext.len() % 16 != 0 || (embeds_iv && ciphertext.is_empty()) {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let (iv, ciphertext) = if embeds_iv {
let (iv, ciphertext) = ciphertext.split_at(16);
(*array_ref!(iv, 0, 16), ciphertext)
} else {
([0u8; 16], ciphertext)
};
let mut plaintext = ciphertext.to_vec();
let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key);
cbc_decrypt(&aes_dec_key, iv, &mut plaintext);
Ok(plaintext)
}
/// An asymmetric private key that can sign messages.
#[derive(Clone, Debug)]
// We shouldn't compare private keys in prod without constant-time operations.
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum PrivateKey {
// We store the seed instead of the key since we can't get the seed back from the key. We could
// store both if we believe deriving the key is done more than once and costly.
Ecdsa([u8; 32]),
#[cfg(feature = "ed25519")]
Ed25519(ed25519_compact::SecretKey),
}
impl PrivateKey {
/// Creates a new private key for the given algorithm.
///
/// # Panics
///
/// Panics if the algorithm is [`SignatureAlgorithm::Unknown`].
pub fn new(env: &mut impl Env, alg: SignatureAlgorithm) -> Self {
match alg {
SignatureAlgorithm::Es256 => {
PrivateKey::Ecdsa(env.key_store().generate_ecdsa_seed().unwrap())
}
#[cfg(feature = "ed25519")]
SignatureAlgorithm::Eddsa => {
let bytes = env.rng().gen_uniform_u8x32();
Self::new_ed25519_from_bytes(&bytes).unwrap()
}
SignatureAlgorithm::Unknown => unreachable!(),
}
}
/// Creates a new ecdsa private key.
pub fn new_ecdsa(env: &mut impl Env) -> PrivateKey {
Self::new(env, SignatureAlgorithm::Es256)
}
/// Helper function that creates a private key of type ECDSA.
///
/// This function is public for legacy credential source parsing only.
pub fn new_ecdsa_from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 32 {
return None;
}
Some(PrivateKey::Ecdsa(*array_ref!(bytes, 0, 32)))
}
#[cfg(feature = "ed25519")]
pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 32 {
return None;
}
let seed = ed25519_compact::Seed::from_slice(bytes).unwrap();
Some(Self::Ed25519(ed25519_compact::KeyPair::from_seed(seed).sk))
}
/// Returns the ECDSA private key.
pub fn ecdsa_key(&self, env: &mut impl Env) -> Result<ecdsa::SecKey, Ctap2StatusCode> {
match self {
PrivateKey::Ecdsa(seed) => ecdsa_key_from_seed(env, seed),
#[allow(unreachable_patterns)]
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}
/// Returns the corresponding public key.
pub fn get_pub_key(&self, env: &mut impl Env) -> Result<CoseKey, Ctap2StatusCode> {
Ok(match self {
PrivateKey::Ecdsa(ecdsa_seed) => {
CoseKey::from(ecdsa_key_from_seed(env, ecdsa_seed)?.genpk())
}
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(ed25519_key) => CoseKey::from(ed25519_key.public_key()),
})
}
/// Returns the encoded signature for a given message.
pub fn sign_and_encode(
&self,
env: &mut impl Env,
message: &[u8],
) -> Result<Vec<u8>, Ctap2StatusCode> {
Ok(match self {
PrivateKey::Ecdsa(ecdsa_seed) => ecdsa_key_from_seed(env, ecdsa_seed)?
.sign_rfc6979::<Sha256>(message)
.to_asn1_der(),
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(),
})
}
/// The associated COSE signature algorithm identifier.
pub fn signature_algorithm(&self) -> SignatureAlgorithm {
match self {
PrivateKey::Ecdsa(_) => SignatureAlgorithm::Es256,
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(_) => SignatureAlgorithm::Eddsa,
}
}
/// Writes the key bytes.
pub fn to_bytes(&self) -> Vec<u8> {
match self {
PrivateKey::Ecdsa(ecdsa_seed) => ecdsa_seed.to_vec(),
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(ed25519_key) => ed25519_key.seed().to_vec(),
}
}
}
fn ecdsa_key_from_seed(
env: &mut impl Env,
seed: &[u8; 32],
) -> Result<ecdsa::SecKey, Ctap2StatusCode> {
let ecdsa_bytes = env.key_store().derive_ecdsa(seed)?;
Ok(ecdsa::SecKey::from_bytes(&ecdsa_bytes).unwrap())
}
impl From<&PrivateKey> for cbor::Value {
fn from(private_key: &PrivateKey) -> Self {
cbor_array![
cbor_int!(private_key.signature_algorithm() as i64),
cbor_bytes!(private_key.to_bytes()),
]
}
}
impl TryFrom<cbor::Value> for PrivateKey {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let mut array = extract_array(cbor_value)?;
if array.len() != 2 {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
}
let key_bytes = extract_byte_string(array.pop().unwrap())?;
match SignatureAlgorithm::try_from(array.pop().unwrap())? {
SignatureAlgorithm::Es256 => PrivateKey::new_ecdsa_from_bytes(&key_bytes)
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
#[cfg(feature = "ed25519")]
SignatureAlgorithm::Eddsa => PrivateKey::new_ed25519_from_bytes(&key_bytes)
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
#[test]
fn test_encrypt_decrypt_with_iv() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_decrypt_without_iv() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap();
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_correct_iv_usage() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let mut ciphertext_no_iv =
aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap();
let mut ciphertext_with_iv = vec![0u8; 16];
ciphertext_with_iv.append(&mut ciphertext_no_iv);
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_iv_manipulation_property() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let mut ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
let mut expected_plaintext = plaintext;
for i in 0..16 {
ciphertext[i] ^= 0xBB;
expected_plaintext[i] ^= 0xBB;
}
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
assert_eq!(decrypted, expected_plaintext);
}
#[test]
fn test_chaining() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext1 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
let ciphertext2 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
assert_eq!(ciphertext1.len(), 80);
assert_eq!(ciphertext2.len(), 80);
// The ciphertext should mutate in all blocks with a different IV.
let block_iter1 = ciphertext1.chunks_exact(16);
let block_iter2 = ciphertext2.chunks_exact(16);
for (block1, block2) in block_iter1.zip(block_iter2) {
assert_ne!(block1, block2);
}
}
#[test]
fn test_new_ecdsa_from_bytes() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let key_bytes = private_key.to_bytes();
assert_eq!(
PrivateKey::new_ecdsa_from_bytes(&key_bytes),
Some(private_key)
);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_new_ed25519_from_bytes() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Eddsa);
let key_bytes = private_key.to_bytes();
assert_eq!(
PrivateKey::new_ed25519_from_bytes(&key_bytes),
Some(private_key)
);
}
#[test]
fn test_new_ecdsa_from_bytes_wrong_length() {
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 16]), None);
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 31]), None);
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 33]), None);
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_new_ed25519_from_bytes_wrong_length() {
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 16]), None);
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 31]), None);
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 33]), None);
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 64]), None);
}
#[test]
fn test_private_key_get_pub_key() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new_ecdsa(&mut env);
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
let public_key = ecdsa_key.genpk();
assert_eq!(
private_key.get_pub_key(&mut env),
Ok(CoseKey::from(public_key))
);
}
#[test]
fn test_private_key_sign_and_encode() {
let mut env = TestEnv::default();
let message = [0x5A; 32];
let private_key = PrivateKey::new_ecdsa(&mut env);
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
let signature = ecdsa_key.sign_rfc6979::<Sha256>(&message).to_asn1_der();
assert_eq!(
private_key.sign_and_encode(&mut env, &message),
Ok(signature)
);
}
fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
assert_eq!(private_key.signature_algorithm(), signature_algorithm);
}
#[test]
fn test_ecdsa_private_key_signature_algorithm() {
test_private_key_signature_algorithm(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_private_key_signature_algorithm() {
test_private_key_signature_algorithm(SignatureAlgorithm::Eddsa);
}
fn test_private_key_from_to_cbor(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let cbor = cbor::Value::from(&private_key);
assert_eq!(PrivateKey::try_from(cbor), Ok(private_key),);
}
#[test]
fn test_ecdsa_private_key_from_to_cbor() {
test_private_key_from_to_cbor(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_private_key_from_to_cbor() {
test_private_key_from_to_cbor(SignatureAlgorithm::Eddsa);
}
fn test_private_key_from_bad_cbor(signature_algorithm: SignatureAlgorithm) {
let cbor = cbor_array![
cbor_int!(signature_algorithm as i64),
cbor_bytes!(vec![0x88; 32]),
// The array is too long.
cbor_int!(0),
];
assert_eq!(
PrivateKey::try_from(cbor),
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
);
}
#[test]
fn test_ecdsa_private_key_from_bad_cbor() {
test_private_key_from_bad_cbor(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_private_key_from_bad_cbor() {
test_private_key_from_bad_cbor(SignatureAlgorithm::Eddsa);
}
#[test]
fn test_private_key_from_bad_cbor_unsupported_algo() {
let cbor = cbor_array![
// This algorithms doesn't exist.
cbor_int!(-1),
cbor_bytes!(vec![0x88; 32]),
];
assert_eq!(
PrivateKey::try_from(cbor),
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
);
}
}

View File

@@ -1,702 +0,0 @@
// Copyright 2019-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::apdu::{Apdu, ApduStatusCode};
use super::credential_id::{decrypt_credential_id, encrypt_to_credential_id};
use super::crypto_wrapper::PrivateKey;
use super::CtapState;
use crate::api::attestation_store::{self, Attestation, AttestationStore};
use crate::env::Env;
use alloc::vec::Vec;
use arrayref::array_ref;
use core::convert::TryFrom;
// For now, they're the same thing with apdu.rs containing the authoritative definition
pub type Ctap1StatusCode = ApduStatusCode;
// The specification referenced in this file is at:
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Ctap1Flags {
CheckOnly = 0x07,
EnforceUpAndSign = 0x03,
DontEnforceUpAndSign = 0x08,
}
impl TryFrom<u8> for Ctap1Flags {
type Error = Ctap1StatusCode;
fn try_from(value: u8) -> Result<Ctap1Flags, Ctap1StatusCode> {
match value {
0x07 => Ok(Ctap1Flags::CheckOnly),
0x03 => Ok(Ctap1Flags::EnforceUpAndSign),
0x08 => Ok(Ctap1Flags::DontEnforceUpAndSign),
_ => Err(Ctap1StatusCode::SW_WRONG_DATA),
}
}
}
impl From<Ctap1Flags> for u8 {
fn from(flags: Ctap1Flags) -> u8 {
flags as u8
}
}
#[derive(Debug, PartialEq, Eq)]
enum U2fCommand {
Register {
challenge: [u8; 32],
application: [u8; 32],
},
Authenticate {
challenge: [u8; 32],
application: [u8; 32],
key_handle: Vec<u8>,
flags: Ctap1Flags,
},
Version,
VendorSpecific {
payload: Vec<u8>,
},
}
impl TryFrom<&[u8]> for U2fCommand {
type Error = Ctap1StatusCode;
fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> {
let apdu: Apdu = match Apdu::try_from(message) {
Ok(apdu) => apdu,
Err(apdu_status_code) => return Err(apdu_status_code),
};
let lc = apdu.lc as usize;
// ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always
// encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected).
// We keep the 2 byte of "Le" for the packet length in mind, but always ignore its value.
// Lc is using big-endian encoding
// +-----+-----+----+----+-----+-----+-----+
// | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 |
// +-----+-----+----+----+-----+-----+-----+
if apdu.header.cla != Ctap1Command::CTAP1_CLA {
return Err(Ctap1StatusCode::SW_CLA_INVALID);
}
// Since there is always request data, the expected length is either omitted or
// encoded in 2 bytes.
if lc != apdu.data.len() && lc + 2 != apdu.data.len() {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
match apdu.header.ins {
// U2F raw message format specification, Section 4.1
// +-----------------+-------------------+
// + Challenge (32B) | Application (32B) |
// +-----------------+-------------------+
Ctap1Command::U2F_REGISTER => {
if lc != 64 {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
Ok(Self::Register {
challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(apdu.data, 32, 32),
})
}
// U2F raw message format specification, Section 5.1
// +-----------------+-------------------+---------------------+------------+
// + Challenge (32B) | Application (32B) | key handle len (1B) | key handle |
// +-----------------+-------------------+---------------------+------------+
Ctap1Command::U2F_AUTHENTICATE => {
if lc < 65 {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
let handle_length = apdu.data[64] as usize;
if lc != 65 + handle_length {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
let flag = Ctap1Flags::try_from(apdu.header.p1)?;
Ok(Self::Authenticate {
challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(apdu.data, 32, 32),
key_handle: apdu.data[65..].to_vec(),
flags: flag,
})
}
// U2F raw message format specification, Section 6.1
Ctap1Command::U2F_VERSION => {
if lc != 0 {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
Ok(Self::Version)
}
// For Vendor specific command.
Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => {
Ok(Self::VendorSpecific {
payload: apdu.data.to_vec(),
})
}
_ => Err(Ctap1StatusCode::SW_INS_INVALID),
}
}
}
pub struct Ctap1Command {}
impl Ctap1Command {
const CTAP1_CLA: u8 = 0;
// This byte is used in Register, but only serves backwards compatibility.
const LEGACY_BYTE: u8 = 0x05;
// This byte is hardcoded into the specification of Authenticate.
const USER_PRESENCE_INDICATOR_BYTE: u8 = 0x01;
// CTAP1/U2F commands
// U2F raw message format specification 1.2 (version 20170411)
const U2F_REGISTER: u8 = 0x01;
const U2F_AUTHENTICATE: u8 = 0x02;
const U2F_VERSION: u8 = 0x03;
const VENDOR_SPECIFIC_FIRST: u8 = 0x40;
const VENDOR_SPECIFIC_LAST: u8 = 0xBF;
pub fn process_command<E: Env>(
env: &mut E,
message: &[u8],
ctap_state: &mut CtapState<E>,
) -> Result<Vec<u8>, Ctap1StatusCode> {
if !ctap_state
.allows_ctap1(env)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
{
return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED);
}
let command = U2fCommand::try_from(message)?;
match command {
U2fCommand::Register {
challenge,
application,
} => {
if !ctap_state.u2f_up_state.consume_up(env) {
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
Ctap1Command::process_register(env, challenge, application)
}
U2fCommand::Authenticate {
challenge,
application,
key_handle,
flags,
} => {
// The order is important due to side effects of checking user presence.
if flags == Ctap1Flags::EnforceUpAndSign && !ctap_state.u2f_up_state.consume_up(env)
{
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
Ctap1Command::process_authenticate(
env,
challenge,
application,
key_handle,
flags,
ctap_state,
)
}
// U2F raw message format specification (version 20170411) section 6.3
U2fCommand::Version => Ok(Vec::<u8>::from(super::U2F_VERSION_STRING)),
// TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED?
U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_SUCCESS),
}
}
// U2F raw message format specification (version 20170411) section 4.3
// In case of success we need to send back the following reply
// (excluding ISO7816 success code)
// +------+--------------------+---------------------+------------+------------+------+
// + 0x05 | User pub key (65B) | key handle len (1B) | key handle | X.509 Cert | Sign |
// +------+--------------------+---------------------+------------+------------+------+
//
// Where Sign is an ECDSA signature over the following structure:
// +------+-------------------+-----------------+------------+--------------------+
// + 0x00 | application (32B) | challenge (32B) | key handle | User pub key (65B) |
// +------+-------------------+-----------------+------------+--------------------+
fn process_register(
env: &mut impl Env,
challenge: [u8; 32],
application: [u8; 32],
) -> Result<Vec<u8>, Ctap1StatusCode> {
let private_key = PrivateKey::new_ecdsa(env);
let sk = private_key
.ecdsa_key(env)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let pk = sk.genpk();
let key_handle = encrypt_to_credential_id(env, &private_key, &application, None, None)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
if key_handle.len() > 0xFF {
// This is just being defensive with unreachable code.
return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION);
}
let Attestation {
private_key,
certificate,
} = env
.attestation_store()
.get(&attestation_store::Id::Batch)?
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len());
response.push(Ctap1Command::LEGACY_BYTE);
let user_pk = pk.to_uncompressed();
response.extend_from_slice(&user_pk);
response.push(key_handle.len() as u8);
response.extend(key_handle.clone());
response.extend_from_slice(&certificate);
// The first byte is reserved.
let mut signature_data = Vec::with_capacity(66 + key_handle.len());
signature_data.push(0x00);
signature_data.extend(&application);
signature_data.extend(&challenge);
signature_data.extend(key_handle);
signature_data.extend_from_slice(&user_pk);
let attestation_key = crypto::ecdsa::SecKey::from_bytes(&private_key).unwrap();
let signature = attestation_key.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
response.extend(signature.to_asn1_der());
Ok(response)
}
// U2F raw message format specification (version 20170411) section 5.4
// In case of success we need to send back the following reply
// (excluding ISO7816 success code)
// +---------+--------------+-----------+
// + UP (1B) | Counter (4B) | Signature |
// +---------+--------------+-----------+
// UP only has 2 defined values:
// - 0x00: user presence was not verified
// - 0x01: user presence was verified
//
// Where Signature is an ECDSA signature over the following structure:
// +-------------------+---------+--------------+-----------------+
// + application (32B) | UP (1B) | Counter (4B) | challenge (32B) |
// +-------------------+---------+--------------+-----------------+
fn process_authenticate<E: Env>(
env: &mut E,
challenge: [u8; 32],
application: [u8; 32],
key_handle: Vec<u8>,
flags: Ctap1Flags,
ctap_state: &mut CtapState<E>,
) -> Result<Vec<u8>, Ctap1StatusCode> {
let credential_source = decrypt_credential_id(env, key_handle, &application)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
if let Some(credential_source) = credential_source {
let ecdsa_key = credential_source
.private_key
.ecdsa_key(env)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
if flags == Ctap1Flags::CheckOnly {
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
ctap_state
.increment_global_signature_counter(env)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
let mut signature_data = ctap_state
.generate_auth_data(
env,
&application,
Ctap1Command::USER_PRESENCE_INDICATOR_BYTE,
)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
signature_data.extend(&challenge);
let signature = ecdsa_key.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
let mut response = signature_data[application.len()..application.len() + 5].to_vec();
response.extend(signature.to_asn1_der());
Ok(response)
} else {
Err(Ctap1StatusCode::SW_WRONG_DATA)
}
}
}
#[cfg(test)]
mod test {
use super::super::credential_id::CBOR_CREDENTIAL_ID_SIZE;
use super::super::data_formats::SignatureAlgorithm;
use super::super::TOUCH_TIMEOUT_MS;
use super::*;
use crate::api::customization::Customization;
use crate::ctap::storage;
use crate::env::test::TestEnv;
use crypto::Hash256;
fn create_register_message(application: &[u8; 32]) -> Vec<u8> {
let mut message = vec![
Ctap1Command::CTAP1_CLA,
Ctap1Command::U2F_REGISTER,
0x00,
0x00,
0x00,
0x00,
0x40,
];
let challenge = [0x0C; 32];
message.extend(&challenge);
message.extend(application);
message
}
fn create_authenticate_message(
application: &[u8; 32],
flags: Ctap1Flags,
key_handle: &[u8],
) -> Vec<u8> {
let mut message = vec![
Ctap1Command::CTAP1_CLA,
Ctap1Command::U2F_AUTHENTICATE,
flags.into(),
0x00,
0x00,
];
message.extend(&(65 + CBOR_CREDENTIAL_ID_SIZE as u16).to_be_bytes());
let challenge = [0x0C; 32];
message.extend(&challenge);
message.extend(application);
message.push(CBOR_CREDENTIAL_ID_SIZE as u8);
message.extend(key_handle);
message
}
#[test]
fn test_process_allowed() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
storage::toggle_always_uv(&mut env).unwrap();
let application = [0x0A; 32];
let message = create_register_message(&application);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED));
}
#[test]
fn test_process_register() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
let application = [0x0A; 32];
let message = create_register_message(&application);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
// Certificate and private key are missing
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
let attestation = Attestation {
private_key: [0x41; 32],
certificate: vec![0x99; 100],
};
env.attestation_store()
.set(&attestation_store::Id::Batch, Some(&attestation))
.unwrap();
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
assert_eq!(response[0], Ctap1Command::LEGACY_BYTE);
assert_eq!(response[66], CBOR_CREDENTIAL_ID_SIZE as u8);
assert!(decrypt_credential_id(
&mut env,
response[67..67 + CBOR_CREDENTIAL_ID_SIZE].to_vec(),
&application,
)
.unwrap()
.is_some());
const CERT_START: usize = 67 + CBOR_CREDENTIAL_ID_SIZE;
assert_eq!(
&response[CERT_START..][..attestation.certificate.len()],
&attestation.certificate
);
}
#[test]
fn test_process_register_bad_message() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
let application = [0x0A; 32];
let message = create_register_message(&application);
let response =
Ctap1Command::process_command(&mut env, &message[..message.len() - 1], &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
}
#[test]
fn test_process_register_without_up() {
let application = [0x0A; 32];
let message = create_register_message(&application);
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
env.clock().advance(TOUCH_TIMEOUT_MS);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
}
#[test]
fn test_process_authenticate_check_only() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
}
#[test]
fn test_process_authenticate_check_only_wrong_rp() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let application = [0x55; 32];
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
#[test]
fn test_process_authenticate_check_only_wrong_length() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message = create_authenticate_message(
&application,
Ctap1Flags::DontEnforceUpAndSign,
&key_handle,
);
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
}
#[test]
fn test_process_authenticate_check_only_wrong_cla() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
message[0] = 0xEE;
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_INVALID));
}
#[test]
fn test_process_authenticate_check_only_wrong_ins() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
message[1] = 0xEE;
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_INS_INVALID));
}
#[test]
fn test_process_authenticate_check_only_wrong_flags() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
message[2] = 0xEE;
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
fn check_signature_counter(env: &mut impl Env, response: &[u8; 4], signature_counter: u32) {
if env.customization().use_signature_counter() {
assert_eq!(u32::from_be_bytes(*response), signature_counter);
} else {
assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]);
}
}
#[test]
fn test_process_authenticate_enforce() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
assert_eq!(response[0], 0x01);
let global_signature_counter = storage::global_signature_counter(&mut env).unwrap();
check_signature_counter(
&mut env,
array_ref!(response, 1, 4),
global_signature_counter,
);
}
#[test]
fn test_process_authenticate_dont_enforce() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let message = create_authenticate_message(
&application,
Ctap1Flags::DontEnforceUpAndSign,
&key_handle,
);
env.clock().advance(TOUCH_TIMEOUT_MS);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
assert_eq!(response[0], 0x01);
let global_signature_counter = storage::global_signature_counter(&mut env).unwrap();
check_signature_counter(
&mut env,
array_ref!(response, 1, 4),
global_signature_counter,
);
}
#[test]
fn test_process_authenticate_bad_key_handle() {
let application = [0x0A; 32];
let key_handle = vec![0x00; CBOR_CREDENTIAL_ID_SIZE];
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
#[test]
fn test_process_authenticate_without_up() {
let application = [0x0A; 32];
let key_handle = vec![0x00; CBOR_CREDENTIAL_ID_SIZE];
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
env.clock().advance(TOUCH_TIMEOUT_MS);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,622 +0,0 @@
// Copyright 2019-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod receive;
mod send;
// Implementation details must be public for testing (in particular fuzzing).
#[cfg(feature = "std")]
pub use self::receive::MessageAssembler;
#[cfg(not(feature = "std"))]
use self::receive::MessageAssembler;
pub use self::send::HidPacketIterator;
use super::status_code::Ctap2StatusCode;
#[cfg(test)]
use crate::env::test::TestEnv;
use crate::env::Env;
use alloc::vec;
use alloc::vec::Vec;
use arrayref::{array_ref, array_refs};
#[cfg(test)]
use enum_iterator::IntoEnumIterator;
// We implement CTAP 2.1 from 2021-06-15. Please see section
// 11.2. USB Human Interface Device (USB HID)
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
const PACKET_TYPE_MASK: u8 = 0x80;
// See section 11.2.9.1.3. CTAPHID_INIT (0x06).
const PROTOCOL_VERSION: u8 = 2;
// The device version number is vendor-defined.
const DEVICE_VERSION_MAJOR: u8 = 1;
const DEVICE_VERSION_MINOR: u8 = 0;
const DEVICE_VERSION_BUILD: u8 = 0;
pub type HidPacket = [u8; 64];
pub type ChannelID = [u8; 4];
/// CTAPHID commands
///
/// See section 11.2.9. of FIDO 2.1 (2021-06-15).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(test, derive(IntoEnumIterator))]
pub enum CtapHidCommand {
Ping = 0x01,
Msg = 0x03,
// Lock is optional and may be used in the future.
Lock = 0x04,
Init = 0x06,
Wink = 0x08,
Cbor = 0x10,
Cancel = 0x11,
Keepalive = 0x3B,
Error = 0x3F,
// The vendor range starts here, going from 0x40 to 0x7F.
}
impl From<u8> for CtapHidCommand {
fn from(cmd: u8) -> Self {
match cmd {
x if x == CtapHidCommand::Ping as u8 => CtapHidCommand::Ping,
x if x == CtapHidCommand::Msg as u8 => CtapHidCommand::Msg,
x if x == CtapHidCommand::Lock as u8 => CtapHidCommand::Lock,
x if x == CtapHidCommand::Init as u8 => CtapHidCommand::Init,
x if x == CtapHidCommand::Wink as u8 => CtapHidCommand::Wink,
x if x == CtapHidCommand::Cbor as u8 => CtapHidCommand::Cbor,
x if x == CtapHidCommand::Cancel as u8 => CtapHidCommand::Cancel,
x if x == CtapHidCommand::Keepalive as u8 => CtapHidCommand::Keepalive,
// This includes the actual error code 0x3F. Error is not used for incoming packets in
// the specification, so we can safely reuse it for unknown bytes.
_ => CtapHidCommand::Error,
}
}
}
/// CTAPHID errors
///
/// See section 11.2.9.1.6. of FIDO 2.1 (2021-06-15).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CtapHidError {
/// The command in the request is invalid.
InvalidCmd = 0x01,
/// A parameter in the request is invalid.
_InvalidPar = 0x02,
/// The length of a message is too big.
InvalidLen = 0x03,
/// Expected a continuation packet with a specific sequence number, got another sequence number.
///
/// This error code is also used if we expect a continuation packet, and receive an init
/// packet. We interpreted it as invalid seq number 0.
InvalidSeq = 0x04,
/// This packet arrived after a timeout.
MsgTimeout = 0x05,
/// A packet arrived on one channel while another is busy.
ChannelBusy = 0x06,
/// Command requires channel lock.
_LockRequired = 0x0A,
/// The requested channel ID is invalid.
InvalidChannel = 0x0B,
/// Unspecified error.
_Other = 0x7F,
/// This error is silently ignored.
UnexpectedContinuation,
}
/// Describes the structure of a parsed HID packet.
///
/// A packet is either an Init or a Continuation packet.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProcessedPacket<'a> {
InitPacket {
cmd: u8,
len: usize,
data: &'a [u8; 57],
},
ContinuationPacket {
seq: u8,
data: &'a [u8; 59],
},
}
/// An assembled CTAPHID command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Message {
// Channel ID.
pub cid: ChannelID,
// Command.
pub cmd: CtapHidCommand,
// Bytes of the message.
pub payload: Vec<u8>,
}
/// A keepalive packet reports the reason why a command does not finish.
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeepaliveStatus {
Processing = 0x01,
UpNeeded = 0x02,
}
/// Holds all state for receiving and sending HID packets.
///
/// This includes
/// - state from not fully processed messages,
/// - all allocated channels.
///
/// To process a packet and receive the response, call `parse_packet`. If you didn't receive any
/// message or preprocessing discarded it, stop. Else process the message further, by handling the
/// commands:
///
/// - PING (optional)
/// - MSG
/// - WINK
/// - CBOR
///
/// To get packets to send from your processed message, call `split_message`. Summary:
///
/// 1. `HidPacket` -> `Option<Message>`
/// 2. `Option<Message>` -> `Message`
/// 3. `Message` -> `Message`
/// 4. `Message` -> `HidPacketIterator`
///
/// These steps correspond to:
///
/// 1. `parse_packet` assembles the message and preprocesses all pure HID commands and errors.
/// 2. If you didn't receive any message or preprocessing discarded it, stop.
/// 3. Handles all CTAP protocol interactions.
/// 4. `split_message` creates packets out of the response message.
pub struct CtapHid<E: Env> {
assembler: MessageAssembler<E>,
// The specification only requires unique CIDs, the allocation algorithm is vendor specific.
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
// allocated.
// In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the
// u32::to/from_be_bytes methods).
// TODO(kaczmarczyck) We might want to limit or timeout open channels.
allocated_cids: usize,
capabilities: u8,
}
impl<E: Env> CtapHid<E> {
pub const CAPABILITY_WINK: u8 = 0x01;
pub const CAPABILITY_CBOR: u8 = 0x04;
#[cfg(any(not(feature = "with_ctap1"), feature = "vendor_hid"))]
pub const CAPABILITY_NMSG: u8 = 0x08;
/// Creates a new CTAP HID packet parser.
///
/// The capabilities passed in are reported to the client in Init.
pub fn new(capabilities: u8) -> CtapHid<E> {
Self {
assembler: MessageAssembler::default(),
allocated_cids: 0,
capabilities,
}
}
/// Parses a packet, and preprocesses some messages and errors.
///
/// The preprocessed commands are:
/// - INIT
/// - CANCEL
/// - ERROR
/// - Unknown and unexpected commands like KEEPALIVE
/// - LOCK is not implemented and currently treated like an unknown command
///
/// Commands that may still be processed:
/// - PING
/// - MSG
/// - WINK
/// - CBOR
///
/// You may ignore PING, it's behaving correctly by default (input == output).
/// Ignoring the others is incorrect behavior. You have to at least replace them with an error
/// message:
/// `Self::error_message(message.cid, CtapHidError::InvalidCmd)`
pub fn parse_packet(&mut self, env: &mut E, packet: &HidPacket) -> Option<Message> {
match self.assembler.parse_packet(env, packet) {
Ok(Some(message)) => {
debug_ctap!(env, "Received message: {:02x?}", message);
self.preprocess_message(message)
}
Ok(None) => {
// Waiting for more packets to assemble the message, nothing to send for now.
None
}
Err((cid, error)) => {
if matches!(error, CtapHidError::UnexpectedContinuation) {
None
} else if !self.is_allocated_channel(cid) {
Some(Self::error_message(cid, CtapHidError::InvalidChannel))
} else {
Some(Self::error_message(cid, error))
}
}
}
}
/// Processes HID-only commands of a message and returns an outgoing message if necessary.
///
/// The preprocessed commands are:
/// - INIT
/// - CANCEL
/// - ERROR
/// - Unknown and unexpected commands like KEEPALIVE
/// - LOCK is not implemented and currently treated like an unknown command
fn preprocess_message(&mut self, message: Message) -> Option<Message> {
let cid = message.cid;
if !self.has_valid_channel(&message) {
return Some(Self::error_message(cid, CtapHidError::InvalidChannel));
}
match message.cmd {
CtapHidCommand::Msg => Some(message),
CtapHidCommand::Cbor => Some(message),
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.3.
CtapHidCommand::Init => {
if message.payload.len() != 8 {
return Some(Self::error_message(cid, CtapHidError::InvalidLen));
}
let new_cid = if cid == CHANNEL_BROADCAST {
// TODO: Prevent allocating 2^32 channels.
self.allocated_cids += 1;
(self.allocated_cids as u32).to_be_bytes()
} else {
// Sync the channel and discard the current transaction.
cid
};
let mut payload = vec![0; 17];
payload[..8].copy_from_slice(&message.payload);
payload[8..12].copy_from_slice(&new_cid);
payload[12] = PROTOCOL_VERSION;
payload[13] = DEVICE_VERSION_MAJOR;
payload[14] = DEVICE_VERSION_MINOR;
payload[15] = DEVICE_VERSION_BUILD;
payload[16] = self.capabilities;
Some(Message {
cid,
cmd: CtapHidCommand::Init,
payload,
})
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.4.
CtapHidCommand::Ping => {
// Pong the same message.
Some(message)
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.5.
CtapHidCommand::Cancel => {
// Authenticators MUST NOT reply to this message.
// CANCEL is handled during user presence checks in main.
None
}
CtapHidCommand::Wink => Some(message),
_ => {
// Unknown or unsupported command.
Some(Self::error_message(cid, CtapHidError::InvalidCmd))
}
}
}
fn has_valid_channel(&self, message: &Message) -> bool {
match message.cid {
// Only INIT commands use the broadcast channel.
CHANNEL_BROADCAST => message.cmd == CtapHidCommand::Init,
// Check that the channel is allocated.
_ => self.is_allocated_channel(message.cid),
}
}
fn is_allocated_channel(&self, cid: ChannelID) -> bool {
cid != CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids
}
pub fn error_message(cid: ChannelID, error_code: CtapHidError) -> Message {
Message {
cid,
cmd: CtapHidCommand::Error,
payload: vec![error_code as u8],
}
}
/// Helper function to parse a raw packet.
pub fn process_single_packet(packet: &HidPacket) -> (ChannelID, ProcessedPacket) {
let (&cid, rest) = array_refs![packet, 4, 60];
if rest[0] & PACKET_TYPE_MASK != 0 {
let cmd = rest[0] & !PACKET_TYPE_MASK;
let len = (rest[1] as usize) << 8 | (rest[2] as usize);
(
cid,
ProcessedPacket::InitPacket {
cmd,
len,
data: array_ref!(rest, 3, 57),
},
)
} else {
(
cid,
ProcessedPacket::ContinuationPacket {
seq: rest[0],
data: array_ref!(rest, 1, 59),
},
)
}
}
/// Splits the message and unwraps the result.
///
/// Unwrapping handles the case of payload lengths > 7609 bytes. All responses are fixed
/// length, with the exception of:
/// - PING, but here output equals the (validated) input,
/// - CBOR, where long responses are conceivable.
///
/// Long CBOR responses should not happen, but we might not catch all edge cases, like for
/// example long user names that are part of the output of an assertion. These cases should be
/// correctly handled by the CTAP implementation. It is therefore an internal error from the
/// HID perspective.
pub fn split_message(message: Message) -> HidPacketIterator {
let cid = message.cid;
HidPacketIterator::new(message).unwrap_or_else(|| {
// The error payload is 1 <= 7609 bytes, so unwrap() is safe.
HidPacketIterator::new(Message {
cid,
cmd: CtapHidCommand::Cbor,
payload: vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8],
})
.unwrap()
})
}
/// Generates the HID response packets for a keepalive status.
pub fn keepalive(cid: ChannelID, status: KeepaliveStatus) -> HidPacketIterator {
Self::split_message(Message {
cid,
cmd: CtapHidCommand::Keepalive,
payload: vec![status as u8],
})
}
#[cfg(test)]
pub fn new_initialized() -> (Self, ChannelID) {
(
Self {
assembler: MessageAssembler::default(),
allocated_cids: 1,
capabilities: 0x0D,
},
[0x00, 0x00, 0x00, 0x01],
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_split_assemble() {
let mut env = TestEnv::default();
for payload_len in 0..7609 {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xFF; payload_len],
};
let mut messages = Vec::new();
let mut assembler = MessageAssembler::<TestEnv>::default();
for packet in HidPacketIterator::new(message.clone()).unwrap() {
match assembler.parse_packet(&mut env, &packet) {
Ok(Some(msg)) => messages.push(msg),
Ok(None) => (),
Err(_) => panic!("Couldn't assemble packet: {:02x?}", &packet as &[u8]),
}
}
assert_eq!(messages, vec![message]);
}
}
#[test]
fn test_spurious_continuation_packet() {
let mut env = TestEnv::default();
let mut ctap_hid = CtapHid::<TestEnv>::new(0x0D);
let mut packet = [0x00; 64];
packet[0..7].copy_from_slice(&[0xC1, 0xC1, 0xC1, 0xC1, 0x00, 0x51, 0x51]);
// Continuation packets are silently ignored.
assert_eq!(ctap_hid.parse_packet(&mut env, &packet), None);
}
#[test]
fn test_command_init() {
let mut ctap_hid = CtapHid::<TestEnv>::new(0x0D);
let init_message = Message {
cid: CHANNEL_BROADCAST,
cmd: CtapHidCommand::Init,
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
};
let reply = ctap_hid.preprocess_message(init_message);
assert_eq!(
reply,
Some(Message {
cid: CHANNEL_BROADCAST,
cmd: CtapHidCommand::Init,
payload: vec![
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // Nonce
0x00, 0x00, 0x00, 0x01, // Allocated CID
0x02, // Protocol version
0x01, 0x00, 0x00, // Device version
0x0D, // Capabilities
]
})
);
}
#[test]
fn test_command_init_for_sync() {
let mut env = TestEnv::default();
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
// Ping packet with a length longer than one packet.
let mut packet1 = [0x51; 64];
packet1[..4].copy_from_slice(&cid);
packet1[4..7].copy_from_slice(&[0x81, 0x02, 0x00]);
// Init packet on the same channel.
let mut packet2 = [0x00; 64];
packet2[..4].copy_from_slice(&cid);
packet2[4..15].copy_from_slice(&[
0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
]);
assert_eq!(ctap_hid.parse_packet(&mut env, &packet1), None);
assert_eq!(
ctap_hid.parse_packet(&mut env, &packet2),
Some(Message {
cid,
cmd: CtapHidCommand::Init,
payload: vec![
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // Nonce
cid[0], cid[1], cid[2], cid[3], // Allocated CID
0x02, // Protocol version
0x01, 0x00, 0x00, // Device version
0x0D, // Capabilities
]
})
);
}
#[test]
fn test_command_ping() {
let mut env = TestEnv::default();
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
let mut ping_packet = [0x00; 64];
ping_packet[..4].copy_from_slice(&cid);
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
assert_eq!(
ctap_hid.parse_packet(&mut env, &ping_packet),
Some(Message {
cid,
cmd: CtapHidCommand::Ping,
payload: vec![0x99, 0x99]
})
);
}
#[test]
fn test_command_cancel() {
let mut env = TestEnv::default();
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
let mut cancel_packet = [0x00; 64];
cancel_packet[..4].copy_from_slice(&cid);
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
let response = ctap_hid.parse_packet(&mut env, &cancel_packet);
assert_eq!(response, None);
}
#[test]
fn test_split_message() {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x99, 0x99],
};
let mut response = CtapHid::<TestEnv>::split_message(message);
let mut expected_packet = [0x00; 64];
expected_packet[..9]
.copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x02, 0x99, 0x99]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_split_message_too_large() {
let payload = vec![0xFF; 7609 + 1];
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload,
};
let mut response = CtapHid::<TestEnv>::split_message(message);
let mut expected_packet = [0x00; 64];
expected_packet[..8].copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x01, 0xF2]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_keepalive() {
for &status in [KeepaliveStatus::Processing, KeepaliveStatus::UpNeeded].iter() {
let cid = [0x12, 0x34, 0x56, 0x78];
let mut response = CtapHid::<TestEnv>::keepalive(cid, status);
let mut expected_packet = [0x00; 64];
expected_packet[..8].copy_from_slice(&[
0x12,
0x34,
0x56,
0x78,
0xBB,
0x00,
0x01,
status as u8,
]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}
}
#[test]
fn test_process_single_packet() {
let cid = [0x12, 0x34, 0x56, 0x78];
let mut packet = [0x00; 64];
packet[..4].copy_from_slice(&cid);
packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
let (processed_cid, processed_packet) = CtapHid::<TestEnv>::process_single_packet(&packet);
assert_eq!(processed_cid, cid);
let expected_packet = ProcessedPacket::InitPacket {
cmd: CtapHidCommand::Ping as u8,
len: 2,
data: array_ref!(packet, 7, 57),
};
assert_eq!(processed_packet, expected_packet);
}
#[test]
fn test_from_ctap_hid_command() {
// 0x3E is unassigned.
assert_eq!(CtapHidCommand::from(0x3E), CtapHidCommand::Error);
for command in CtapHidCommand::into_enum_iter() {
assert_eq!(CtapHidCommand::from(command as u8), command);
}
}
#[test]
fn test_error_message() {
let cid = [0x12, 0x34, 0x56, 0x78];
assert_eq!(
CtapHid::<TestEnv>::error_message(cid, CtapHidError::InvalidCmd),
Message {
cid,
cmd: CtapHidCommand::Error,
payload: vec![0x01],
}
);
}
}

View File

@@ -1,594 +0,0 @@
// Copyright 2019-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::{
ChannelID, CtapHid, CtapHidCommand, CtapHidError, HidPacket, Message, ProcessedPacket,
};
use crate::api::clock::Clock;
use crate::api::customization::Customization;
use crate::env::Env;
use alloc::vec::Vec;
use core::mem::swap;
// TODO: Is this timeout duration specified?
const TIMEOUT_DURATION_MS: usize = 100;
/// A structure to assemble CTAPHID commands from a series of incoming USB HID packets.
pub struct MessageAssembler<E: Env> {
// Whether this is waiting to receive an initialization packet.
idle: bool,
// Current channel ID.
cid: ChannelID,
// Timestamp of the last packet received on the current channel.
timer: <E::Clock as Clock>::Timer,
// Current command.
cmd: u8,
// Sequence number expected for the next packet.
seq: u8,
// Number of bytes left to fill the current message.
remaining_payload_len: usize,
// Buffer for the current payload.
payload: Vec<u8>,
}
impl<E: Env> Default for MessageAssembler<E> {
fn default() -> MessageAssembler<E> {
MessageAssembler {
idle: true,
cid: [0, 0, 0, 0],
timer: <E::Clock as Clock>::Timer::default(),
cmd: 0,
seq: 0,
remaining_payload_len: 0,
payload: Vec::new(),
}
}
}
impl<E: Env> MessageAssembler<E> {
// Resets the message assembler to the idle state.
// The caller can reset the assembler for example due to a timeout.
fn reset(&mut self) {
self.idle = true;
self.cid = [0, 0, 0, 0];
self.timer = <E::Clock as Clock>::Timer::default();
self.cmd = 0;
self.seq = 0;
self.remaining_payload_len = 0;
self.payload.clear();
}
// Returns:
// - An Ok() result if the packet was parsed correctly. This contains either Some(Vec<u8>) if a
// full message was assembled after this packet, or None if more packets are needed to fill the
// message.
// - An Err() result if there was a parsing error.
pub fn parse_packet(
&mut self,
env: &mut E,
packet: &HidPacket,
) -> Result<Option<Message>, (ChannelID, CtapHidError)> {
// TODO: Support non-full-speed devices (i.e. packet len != 64)? This isn't recommended by
// section 8.8.1
let (cid, processed_packet) = CtapHid::<E>::process_single_packet(packet);
if !self.idle && env.clock().is_elapsed(&self.timer) {
// The current channel timed out.
// Save the channel ID and reset the state.
let current_cid = self.cid;
self.reset();
// If the packet is from the timed-out channel, send back a timeout error.
// Otherwise, proceed with processing the packet.
if cid == current_cid {
return Err((cid, CtapHidError::MsgTimeout));
}
}
if self.idle {
// Expecting an initialization packet.
match processed_packet {
ProcessedPacket::InitPacket { cmd, len, data } => {
self.parse_init_packet(env, cid, cmd, len, data)
}
ProcessedPacket::ContinuationPacket { .. } => {
// CTAP specification (version 20190130) section 8.1.5.4
// Spurious continuation packets will be ignored.
Err((cid, CtapHidError::UnexpectedContinuation))
}
}
} else {
// Expecting a continuation packet from the current channel.
// CTAP specification (version 20190130) section 8.1.5.1
// Reject packets from other channels.
if cid != self.cid {
return Err((cid, CtapHidError::ChannelBusy));
}
match processed_packet {
// Unexpected initialization packet.
ProcessedPacket::InitPacket { cmd, len, data } => {
self.reset();
if cmd == CtapHidCommand::Init as u8 {
self.parse_init_packet(env, cid, cmd, len, data)
} else {
Err((cid, CtapHidError::InvalidSeq))
}
}
ProcessedPacket::ContinuationPacket { seq, data } => {
if seq != self.seq {
// Reject packets with the wrong sequence number.
self.reset();
Err((cid, CtapHidError::InvalidSeq))
} else {
// Update the last timestamp.
self.timer = env.clock().make_timer(TIMEOUT_DURATION_MS);
// Increment the sequence number for the next packet.
self.seq += 1;
Ok(self.append_payload(data))
}
}
}
}
}
fn parse_init_packet(
&mut self,
env: &mut E,
cid: ChannelID,
cmd: u8,
len: usize,
data: &[u8],
) -> Result<Option<Message>, (ChannelID, CtapHidError)> {
// Reject invalid lengths early to reduce the risk of running out of memory.
// TODO: also reject invalid commands early?
if len > env.customization().max_msg_size() {
return Err((cid, CtapHidError::InvalidLen));
}
self.cid = cid;
self.timer = env.clock().make_timer(TIMEOUT_DURATION_MS);
self.cmd = cmd;
self.seq = 0;
self.remaining_payload_len = len;
Ok(self.append_payload(data))
}
fn append_payload(&mut self, data: &[u8]) -> Option<Message> {
if data.len() < self.remaining_payload_len {
self.payload.extend_from_slice(data);
self.idle = false;
self.remaining_payload_len -= data.len();
None
} else {
self.payload
.extend_from_slice(&data[..self.remaining_payload_len]);
self.idle = true;
let mut payload = Vec::new();
swap(&mut self.payload, &mut payload);
Some(Message {
cid: self.cid,
cmd: CtapHidCommand::from(self.cmd),
payload,
})
}
}
}
#[cfg(test)]
mod test {
use crate::env::test::TestEnv;
use super::*;
fn byte_extend(bytes: &[u8], padding: u8) -> HidPacket {
let len = bytes.len();
assert!(len <= 64);
let mut result = [0; 64];
result[..len].copy_from_slice(bytes);
for byte in result[len..].iter_mut() {
*byte = padding;
}
result
}
fn zero_extend(bytes: &[u8]) -> HidPacket {
byte_extend(bytes, 0)
}
#[test]
fn test_empty_payload() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![]
}))
);
}
#[test]
fn test_one_packet() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10]),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0x00; 0x10]
}))
);
}
#[test]
fn test_nonzero_padding() {
let mut env = TestEnv::default();
// CTAP specification (version 20190130) section 8.1.4
// It is written that "Unused bytes SHOULD be set to zero", so we test that non-zero
// padding is accepted as well.
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10], 0xFF),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xFF; 0x10]
}))
);
}
#[test]
fn test_two_packets() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x40]
}))
);
}
#[test]
fn test_three_packets() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x80]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x80]
}))
);
}
#[test]
fn test_max_packets() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
),
Ok(None)
);
for seq in 0..0x7F {
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
Ok(None)
);
}
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x1DB9]
}))
);
}
#[test]
fn test_multiple_messages() {
let mut env = TestEnv::default();
// Check that after yielding a message, the assembler is ready to process new messages.
let mut assembler = MessageAssembler::default();
for i in 0..10 {
// Introduce some variability in the messages.
let cmd = CtapHidCommand::from(i + 1);
let byte = 3 * i;
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(
&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd as u8, 0x00, 0x80],
byte
),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x00], byte),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x01], byte),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd,
payload: vec![byte; 0x80]
}))
);
}
}
#[test]
fn test_channel_switch() {
let mut env = TestEnv::default();
// Check that the assembler can process messages from multiple channels, sequentially.
let mut assembler = MessageAssembler::default();
for i in 0..10 {
// Introduce some variability in the messages.
let cid = 0x78 + i;
let cmd = CtapHidCommand::from(i + 1);
let byte = 3 * i;
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd as u8, 0x00, 0x80], byte),
),
Ok(None)
);
assert_eq!(
assembler
.parse_packet(&mut env, &byte_extend(&[0x12, 0x34, 0x56, cid, 0x00], byte)),
Ok(None)
);
assert_eq!(
assembler
.parse_packet(&mut env, &byte_extend(&[0x12, 0x34, 0x56, cid, 0x01], byte)),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, cid],
cmd,
payload: vec![byte; 0x80]
}))
);
}
}
#[test]
fn test_unexpected_channel() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
// Check that many sorts of packets on another channel are ignored.
for i in 0..=0xFF {
let cmd = CtapHidCommand::from(i);
for byte in 0..=0xFF {
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd as u8, 0x00], byte),
),
Err(([0x12, 0x34, 0x56, 0x9A], CtapHidError::ChannelBusy))
);
}
}
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x40]
}))
);
}
#[test]
fn test_spurious_continuation_packets() {
let mut env = TestEnv::default();
// CTAP specification (version 20190130) section 8.1.5.4
// Spurious continuation packets appearing without a prior initialization packet will be
// ignored.
let mut assembler = MessageAssembler::default();
for i in 0..0x80 {
// Some legit packet.
let byte = 2 * i;
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x10], byte),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![byte; 0x10]
}))
);
// Spurious continuation packet.
let seq = i;
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
Err((
[0x12, 0x34, 0x56, 0x78],
CtapHidError::UnexpectedContinuation
))
);
}
}
#[test]
fn test_unexpected_init() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80])),
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::InvalidSeq))
);
}
#[test]
fn test_unexpected_seq() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01])),
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::InvalidSeq))
);
}
#[test]
fn test_timed_out_packet() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
env.clock().advance(TIMEOUT_DURATION_MS);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::MsgTimeout))
);
}
#[test]
fn test_just_in_time_packets() {
let mut env = TestEnv::default();
// Delay between each packet is just below the threshold.
let delay = TIMEOUT_DURATION_MS - 1;
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
),
Ok(None)
);
for seq in 0..0x7F {
env.clock().advance(delay);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
Ok(None)
);
}
env.clock().advance(delay);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x1DB9]
}))
);
}
#[test]
fn test_init_sync() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
// Ping packet with a length longer than one packet.
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x02, 0x00], 0x51),
),
Ok(None)
);
// Init packet on the same channel.
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[
0x12, 0x34, 0x56, 0x78, 0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
0xDE, 0xF0
]),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Init,
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
}))
);
}
// TODO: more tests
}

View File

@@ -1,316 +0,0 @@
// Copyright 2019-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::{HidPacket, Message};
const TYPE_INIT_BIT: u8 = 0x80;
/// Iterator for HID packets.
///
/// The `new` constructor splits the CTAP `Message` into `HidPacket`s for sending over USB.
pub struct HidPacketIterator(Option<MessageSplitter>);
impl HidPacketIterator {
pub fn new(message: Message) -> Option<HidPacketIterator> {
let splitter = MessageSplitter::new(message);
if splitter.is_some() {
Some(HidPacketIterator(splitter))
} else {
None
}
}
pub fn none() -> HidPacketIterator {
HidPacketIterator(None)
}
pub fn has_data(&self) -> bool {
if let Some(ms) = &self.0 {
ms.finished()
} else {
false
}
}
}
impl Iterator for HidPacketIterator {
type Item = HidPacket;
fn next(&mut self) -> Option<HidPacket> {
match &mut self.0 {
Some(splitter) => splitter.next(),
None => None,
}
}
}
struct MessageSplitter {
message: Message,
packet: HidPacket,
seq: Option<u8>,
i: usize,
}
impl MessageSplitter {
/// Try to split this message into an iterator of HID packets.
///
/// This fails if the message is too long to fit into a sequence of HID packets.
pub fn new(message: Message) -> Option<MessageSplitter> {
if message.payload.len() > 7609 {
None
} else {
// Cache the CID, as it is constant for all packets in this message.
let mut packet = [0; 64];
packet[..4].copy_from_slice(&message.cid);
Some(MessageSplitter {
message,
packet,
seq: None,
i: 0,
})
}
}
/// Copy as many bytes as possible from data to dst, and return how many bytes are copied.
///
/// Contrary to copy_from_slice, this doesn't require slices of the same length.
/// All unused bytes in dst are set to zero, as if the data was padded with zeros to match.
fn consume_data(dst: &mut [u8], data: &[u8]) -> usize {
let dst_len = dst.len();
let data_len = data.len();
if data_len <= dst_len {
// data fits in dst, copy all the bytes.
dst[..data_len].copy_from_slice(data);
for byte in dst[data_len..].iter_mut() {
*byte = 0;
}
data_len
} else {
// Fill all of dst.
dst.copy_from_slice(&data[..dst_len]);
dst_len
}
}
// Is there more data to iterate over?
fn finished(&self) -> bool {
let payload_len = self.message.payload.len();
match self.seq {
None => true,
Some(_) => self.i < payload_len,
}
}
}
impl Iterator for MessageSplitter {
type Item = HidPacket;
fn next(&mut self) -> Option<HidPacket> {
let payload_len = self.message.payload.len();
match self.seq {
None => {
// First, send an initialization packet.
self.packet[4] = self.message.cmd as u8 | TYPE_INIT_BIT;
self.packet[5] = (payload_len >> 8) as u8;
self.packet[6] = payload_len as u8;
self.seq = Some(0);
self.i =
MessageSplitter::consume_data(&mut self.packet[7..], &self.message.payload);
Some(self.packet)
}
Some(seq) => {
// Send the next continuation packet, if any.
if self.i < payload_len {
self.packet[4] = seq;
self.seq = Some(seq + 1);
self.i += MessageSplitter::consume_data(
&mut self.packet[5..],
&self.message.payload[self.i..],
);
Some(self.packet)
} else {
None
}
}
}
}
}
#[cfg(test)]
mod test {
use super::super::CtapHidCommand;
use super::*;
fn assert_packet_output_equality(message: Message, expected_packets: Vec<HidPacket>) {
let packets: Vec<HidPacket> = HidPacketIterator::new(message).unwrap().collect();
assert_eq!(packets.len(), expected_packets.len());
for (packet, expected_packet) in packets.iter().zip(expected_packets.iter()) {
assert_eq!(packet as &[u8], expected_packet as &[u8]);
}
}
#[test]
fn test_hid_packet_iterator_single_packet() {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xAA, 0xBB],
};
let expected_packets: Vec<HidPacket> = vec![[
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x02, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]];
assert_packet_output_equality(message, expected_packets);
}
#[test]
fn test_hid_packet_iterator_big_single_packet() {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xAA; 64 - 7],
};
let expected_packets: Vec<HidPacket> = vec![[
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x39, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
]];
assert_packet_output_equality(message, expected_packets);
}
#[test]
fn test_hid_packet_iterator_two_packets() {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xAA; 64 - 7 + 1],
};
let expected_packets: Vec<HidPacket> = vec![
[
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x3A, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
],
[
0x12, 0x34, 0x56, 0x78, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
];
assert_packet_output_equality(message, expected_packets);
}
#[test]
fn test_hid_packet_iterator_two_full_packets() {
let mut payload = vec![0xAA; 64 - 7];
payload.extend(vec![0xBB; 64 - 5]);
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload,
};
let expected_packets: Vec<HidPacket> = vec![
[
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x74, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
],
[
0x12, 0x34, 0x56, 0x78, 0x00, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
],
];
assert_packet_output_equality(message, expected_packets);
}
#[test]
fn test_hid_packet_iterator_max_packets() {
let mut payload = vec![0xFF; 64 - 7];
for i in 0..128 {
payload.extend(vec![i + 1; 64 - 5]);
}
// Sanity check for the length of the payload.
assert_eq!(payload.len(), 0x1db9);
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Msg,
payload,
};
let mut expected_packets: Vec<HidPacket> = vec![[
0x12, 0x34, 0x56, 0x78, 0x83, 0x1D, 0xB9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
]];
for i in 0..128 {
let mut packet: HidPacket = [0; 64];
packet[0] = 0x12;
packet[1] = 0x34;
packet[2] = 0x56;
packet[3] = 0x78;
packet[4] = i;
for byte in packet.iter_mut().skip(5) {
*byte = i + 1;
}
expected_packets.push(packet);
}
assert_packet_output_equality(message, expected_packets);
}
#[test]
fn test_hid_packet_iterator_payload_one_too_large() {
let payload = vec![0xFF; (64 - 7) + 128 * (64 - 5) + 1];
assert_eq!(payload.len(), 0x1dba);
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Msg,
payload,
};
assert!(HidPacketIterator::new(message).is_none());
}
#[test]
fn test_hid_packet_iterator_payload_way_too_large() {
// Check that overflow of u16 doesn't bypass the size limit.
let payload = vec![0xFF; 0x10000];
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Msg,
payload,
};
assert!(HidPacketIterator::new(message).is_none());
}
}

View File

@@ -1,459 +0,0 @@
// Copyright 2020-2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorLargeBlobsParameters;
use super::response::{AuthenticatorLargeBlobsResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use crate::api::customization::Customization;
use crate::ctap::storage;
use crate::env::Env;
use alloc::vec;
use alloc::vec::Vec;
use byteorder::{ByteOrder, LittleEndian};
use crypto::sha256::Sha256;
use crypto::Hash256;
/// The length of the truncated hash that as appended to the large blob data.
const TRUNCATED_HASH_LEN: usize = 16;
pub struct LargeBlobs {
buffer: Vec<u8>,
expected_length: usize,
expected_next_offset: usize,
}
/// Implements the logic for the AuthenticatorLargeBlobs command and keeps its state.
impl LargeBlobs {
pub fn new() -> LargeBlobs {
LargeBlobs {
buffer: Vec::new(),
expected_length: 0,
expected_next_offset: 0,
}
}
/// Process the large blob command.
pub fn process_command<E: Env>(
&mut self,
env: &mut E,
client_pin: &mut ClientPin<E>,
large_blobs_params: AuthenticatorLargeBlobsParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorLargeBlobsParameters {
get,
set,
offset,
length,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = large_blobs_params;
let max_fragment_size = env.customization().max_msg_size() - 64;
if let Some(get) = get {
if get > max_fragment_size || offset.checked_add(get).is_none() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
let config = storage::get_large_blob_array(env, offset, get)?;
return Ok(ResponseData::AuthenticatorLargeBlobs(Some(
AuthenticatorLargeBlobsResponse { config },
)));
}
if let Some(mut set) = set {
if set.len() > max_fragment_size {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
if offset == 0 {
self.expected_length =
length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
if self.expected_length > env.customization().max_large_blob_array_size() {
return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL);
}
self.expected_next_offset = 0;
}
if offset != self.expected_next_offset {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ);
}
if storage::pin_hash(env)?.is_some() || storage::has_always_uv(env)? {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut large_blob_data = vec![0xFF; 32];
large_blob_data.extend(&[0x0C, 0x00]);
let mut offset_bytes = [0u8; 4];
LittleEndian::write_u32(&mut offset_bytes, offset as u32);
large_blob_data.extend(&offset_bytes);
large_blob_data.extend(&Sha256::hash(set.as_slice()));
client_pin.verify_pin_uv_auth_token(
&large_blob_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
client_pin.has_permission(PinPermission::LargeBlobWrite)?;
}
if offset + set.len() > self.expected_length {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if offset == 0 {
self.buffer = Vec::with_capacity(self.expected_length);
}
self.buffer.append(&mut set);
self.expected_next_offset = self.buffer.len();
if self.expected_next_offset == self.expected_length {
self.expected_length = 0;
self.expected_next_offset = 0;
// Must be a positive number.
let buffer_hash_index = self.buffer.len() - TRUNCATED_HASH_LEN;
if Sha256::hash(&self.buffer[..buffer_hash_index])[..TRUNCATED_HASH_LEN]
!= self.buffer[buffer_hash_index..]
{
self.buffer = Vec::new();
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
}
storage::commit_large_blob_array(env, &self.buffer)?;
self.buffer = Vec::new();
}
return Ok(ResponseData::AuthenticatorLargeBlobs(None));
}
// This should be unreachable, since the command has either get or set.
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::PinUvAuthProtocol;
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::*;
use crate::env::test::TestEnv;
#[test]
fn test_process_command_get_empty() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
let large_blob = vec![
0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5,
0x7A, 0x6D, 0x3C,
];
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(large_blob.len()),
set: None,
offset: 0,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
match large_blobs_response.unwrap() {
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
assert_eq!(response.config, large_blob);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_command_commit_and_get() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
offset: BLOB_LEN / 2,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(BLOB_LEN),
set: None,
offset: 0,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
match large_blobs_response.unwrap() {
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
assert_eq!(response.config, large_blob);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_command_commit_unexpected_offset() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
// The offset is 1 too big.
offset: BLOB_LEN / 2 + 1,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ),
);
}
#[test]
fn test_process_command_commit_unexpected_length() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
// The length is 1 too small.
length: Some(BLOB_LEN - 1),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
offset: BLOB_LEN / 2,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
);
}
#[test]
fn test_process_command_commit_end_offset_overflow() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(1),
set: None,
offset: usize::MAX,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
assert_eq!(
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH),
);
}
#[test]
fn test_process_command_commit_unexpected_hash() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 20;
// This blob does not have an appropriate hash.
let large_blob = vec![0x1B; BLOB_LEN];
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob.to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE),
);
}
fn test_helper_process_command_commit_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
pin_uv_auth_protocol,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 20;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let mut large_blob_data = vec![0xFF; 32];
// Command constant and offset bytes.
large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]);
large_blob_data.extend(&Sha256::hash(&large_blob));
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
&pin_uv_auth_token,
&large_blob_data,
pin_uv_auth_protocol,
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: Some(pin_uv_auth_param),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
}
#[test]
fn test_process_command_commit_with_pin_v1() {
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_command_commit_with_pin_v2() {
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V2);
}
}

View File

@@ -1,215 +0,0 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::clock::Clock;
#[cfg(feature = "with_ctap1")]
use crate::ctap::ctap1;
#[cfg(feature = "with_ctap1")]
use crate::ctap::hid::ChannelID;
use crate::ctap::hid::{
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
};
use crate::ctap::{Channel, CtapState};
use crate::env::Env;
const WINK_TIMEOUT_DURATION_MS: usize = 5000;
/// Implements the standard CTAP command processing for HID.
pub struct MainHid<E: Env> {
hid: CtapHid<E>,
wink_permission: <E::Clock as Clock>::Timer,
}
impl<E: Env> Default for MainHid<E> {
/// Instantiates a HID handler for CTAP1, CTAP2 and Wink.
fn default() -> Self {
#[cfg(feature = "with_ctap1")]
let capabilities = CtapHid::<E>::CAPABILITY_WINK | CtapHid::<E>::CAPABILITY_CBOR;
#[cfg(not(feature = "with_ctap1"))]
let capabilities = CtapHid::<E>::CAPABILITY_WINK
| CtapHid::<E>::CAPABILITY_CBOR
| CtapHid::<E>::CAPABILITY_NMSG;
let hid = CtapHid::new(capabilities);
let wink_permission = <E::Clock as Clock>::Timer::default();
MainHid {
hid,
wink_permission,
}
}
}
impl<E: Env> MainHid<E> {
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
pub fn process_hid_packet(
&mut self,
env: &mut E,
packet: &HidPacket,
ctap_state: &mut CtapState<E>,
) -> HidPacketIterator {
if let Some(message) = self.hid.parse_packet(env, packet) {
let processed_message = self.process_message(env, message, ctap_state);
debug_ctap!(env, "Sending message: {:02x?}", processed_message);
CtapHid::<E>::split_message(processed_message)
} else {
HidPacketIterator::none()
}
}
/// Processes a message's commands that affect the protocol outside HID.
pub fn process_message(
&mut self,
env: &mut E,
message: Message,
ctap_state: &mut CtapState<E>,
) -> Message {
// If another command arrives, stop winking to prevent accidential button touches.
self.wink_permission = <E::Clock as Clock>::Timer::default();
let cid = message.cid;
match message.cmd {
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.1.
CtapHidCommand::Msg => {
// If we don't have CTAP1 backward compatibilty, this command is invalid.
#[cfg(not(feature = "with_ctap1"))]
return CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd);
#[cfg(feature = "with_ctap1")]
match ctap1::Ctap1Command::process_command(env, &message.payload, ctap_state) {
Ok(payload) => Self::ctap1_success_message(cid, &payload),
Err(ctap1_status_code) => Self::ctap1_error_message(cid, ctap1_status_code),
}
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.2.
CtapHidCommand::Cbor => {
// Each transaction is atomic, so we process the command directly here and
// don't handle any other packet in the meantime.
// TODO: Send "Processing" type keep-alive packets in the meantime.
let response =
ctap_state.process_command(env, &message.payload, Channel::MainHid(cid));
Message {
cid,
cmd: CtapHidCommand::Cbor,
payload: response,
}
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.2.1.
CtapHidCommand::Wink => {
if message.payload.is_empty() {
self.wink_permission = env.clock().make_timer(WINK_TIMEOUT_DURATION_MS);
// The response is empty like the request.
message
} else {
CtapHid::<E>::error_message(cid, CtapHidError::InvalidLen)
}
}
// All other commands have already been processed, keep them as is.
_ => message,
}
}
/// Returns whether a wink permission is currently granted.
pub fn should_wink(&self, env: &mut E) -> bool {
!env.clock().is_elapsed(&self.wink_permission)
}
#[cfg(feature = "with_ctap1")]
fn ctap1_error_message(cid: ChannelID, error_code: ctap1::Ctap1StatusCode) -> Message {
let code: u16 = error_code.into();
Message {
cid,
cmd: CtapHidCommand::Msg,
payload: code.to_be_bytes().to_vec(),
}
}
#[cfg(feature = "with_ctap1")]
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> Message {
let mut response = payload.to_vec();
let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into();
response.extend_from_slice(&code.to_be_bytes());
Message {
cid,
cmd: CtapHidCommand::Msg,
payload: response,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ctap::hid::ChannelID;
use crate::env::test::TestEnv;
fn new_initialized() -> (MainHid<TestEnv>, ChannelID) {
let (hid, cid) = CtapHid::new_initialized();
let wink_permission = <<TestEnv as Env>::Clock as Clock>::Timer::default();
(
MainHid::<TestEnv> {
hid,
wink_permission,
},
cid,
)
}
#[test]
fn test_process_hid_packet() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut main_hid, cid) = new_initialized();
let mut ping_packet = [0x00; 64];
ping_packet[..4].copy_from_slice(&cid);
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
let mut response = main_hid.process_hid_packet(&mut env, &ping_packet, &mut ctap_state);
assert_eq!(response.next(), Some(ping_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_process_hid_packet_empty() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut main_hid, cid) = new_initialized();
let mut cancel_packet = [0x00; 64];
cancel_packet[..4].copy_from_slice(&cid);
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
let mut response = main_hid.process_hid_packet(&mut env, &cancel_packet, &mut ctap_state);
assert_eq!(response.next(), None);
}
#[test]
fn test_wink() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut main_hid, cid) = new_initialized();
assert!(!main_hid.should_wink(&mut env));
let mut wink_packet = [0x00; 64];
wink_packet[..4].copy_from_slice(&cid);
wink_packet[4..7].copy_from_slice(&[0x88, 0x00, 0x00]);
let mut response = main_hid.process_hid_packet(&mut env, &wink_packet, &mut ctap_state);
assert_eq!(response.next(), Some(wink_packet));
assert_eq!(response.next(), None);
assert!(main_hid.should_wink(&mut env));
env.clock().advance(WINK_TIMEOUT_DURATION_MS);
assert!(!main_hid.should_wink(&mut env));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,408 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::ctap::client_pin::PIN_TOKEN_LENGTH;
use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt};
use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol};
use crate::ctap::status_code::Ctap2StatusCode;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::convert::TryInto;
use crypto::hkdf::hkdf_empty_salt_256;
#[cfg(test)]
use crypto::hmac::hmac_256;
use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits};
use crypto::sha256::Sha256;
use crypto::Hash256;
use rng256::Rng256;
/// Implements common functions between existing PIN protocols for handshakes.
pub struct PinProtocol {
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
}
impl PinProtocol {
/// This process is run by the authenticator at power-on.
///
/// This function implements "initialize" from the specification.
pub fn new(rng: &mut impl Rng256) -> PinProtocol {
let key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
let pin_uv_auth_token = rng.gen_uniform_u8x32();
PinProtocol {
key_agreement_key,
pin_uv_auth_token,
}
}
/// Generates a fresh public key.
pub fn regenerate(&mut self, rng: &mut impl Rng256) {
self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
}
/// Generates a fresh pinUvAuthToken.
pub fn reset_pin_uv_auth_token(&mut self, rng: &mut impl Rng256) {
self.pin_uv_auth_token = rng.gen_uniform_u8x32();
}
/// Returns the authenticators public key as a CoseKey structure.
pub fn get_public_key(&self) -> CoseKey {
CoseKey::from(self.key_agreement_key.genpk())
}
/// Processes the peer's encapsulated CoseKey and returns the shared secret.
pub fn decapsulate(
&self,
peer_cose_key: CoseKey,
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<Box<dyn SharedSecret>, Ctap2StatusCode> {
let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?;
let handshake = self.key_agreement_key.exchange_x(&pk);
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))),
PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))),
}
}
/// Getter for pinUvAuthToken.
pub fn get_pin_uv_auth_token(&self) -> &[u8; PIN_TOKEN_LENGTH] {
&self.pin_uv_auth_token
}
/// This is used for debugging to inject key material.
#[cfg(test)]
pub fn new_test(
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
) -> PinProtocol {
PinProtocol {
key_agreement_key,
pin_uv_auth_token,
}
}
}
/// Authenticates the pinUvAuthToken for the given PIN protocol.
#[cfg(test)]
pub fn authenticate_pin_uv_auth_token(
token: &[u8; PIN_TOKEN_LENGTH],
message: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Vec<u8> {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => hmac_256::<Sha256>(token, message)[..16].to_vec(),
PinUvAuthProtocol::V2 => hmac_256::<Sha256>(token, message).to_vec(),
}
}
/// Verifies the pinUvAuthToken for the given PIN protocol.
pub fn verify_pin_uv_auth_token(
token: &[u8; PIN_TOKEN_LENGTH],
message: &[u8],
signature: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<(), Ctap2StatusCode> {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => verify_v1(token, message, signature),
PinUvAuthProtocol::V2 => verify_v2(token, message, signature),
}
}
pub trait SharedSecret {
/// Returns the encrypted plaintext.
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
/// Returns the decrypted ciphertext.
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
/// Verifies that the signature is a valid MAC for the given message.
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>;
/// Creates a signature that matches verify.
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8>;
}
fn verify_v1(key: &[u8; 32], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
if signature.len() != 16 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if verify_hmac_256_first_128bits::<Sha256>(key, message, array_ref![signature, 0, 16]) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
fn verify_v2(key: &[u8; 32], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
if signature.len() != 32 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if verify_hmac_256::<Sha256>(key, message, array_ref![signature, 0, 32]) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
pub struct SharedSecretV1 {
common_secret: [u8; 32],
aes_enc_key: crypto::aes256::EncryptionKey,
}
impl SharedSecretV1 {
/// Creates a new shared secret from the handshake result.
fn new(handshake: [u8; 32]) -> SharedSecretV1 {
let common_secret = Sha256::hash(&handshake);
let aes_enc_key = crypto::aes256::EncryptionKey::new(&common_secret);
SharedSecretV1 {
common_secret,
aes_enc_key,
}
}
}
impl SharedSecret for SharedSecretV1 {
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, false)
}
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
verify_v1(&self.common_secret, message, signature)
}
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
hmac_256::<Sha256>(&self.common_secret, message)[..16].to_vec()
}
}
pub struct SharedSecretV2 {
aes_enc_key: crypto::aes256::EncryptionKey,
hmac_key: [u8; 32],
}
impl SharedSecretV2 {
/// Creates a new shared secret from the handshake result.
fn new(handshake: [u8; 32]) -> SharedSecretV2 {
let aes_key = hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 AES key");
SharedSecretV2 {
aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key),
hmac_key: hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 HMAC key"),
}
}
}
impl SharedSecret for SharedSecretV2 {
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, true)
}
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
verify_v2(&self.hmac_key, message, signature)
}
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
hmac_256::<Sha256>(&self.hmac_key, message).to_vec()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
#[test]
fn test_pin_protocol_public_key() {
let mut env = TestEnv::default();
let mut pin_protocol = PinProtocol::new(env.rng());
let public_key = pin_protocol.get_public_key();
pin_protocol.regenerate(env.rng());
let new_public_key = pin_protocol.get_public_key();
assert_ne!(public_key, new_public_key);
}
#[test]
fn test_pin_protocol_pin_uv_auth_token() {
let mut env = TestEnv::default();
let mut pin_protocol = PinProtocol::new(env.rng());
let token = *pin_protocol.get_pin_uv_auth_token();
pin_protocol.reset_pin_uv_auth_token(env.rng());
let new_token = pin_protocol.get_pin_uv_auth_token();
assert_ne!(&token, new_token);
}
#[test]
fn test_shared_secret_v1_encrypt_decrypt() {
let mut env = TestEnv::default();
let shared_secret = SharedSecretV1::new([0x55; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret.encrypt(env.rng(), &plaintext).unwrap();
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
}
#[test]
fn test_shared_secret_v1_authenticate_verify() {
let shared_secret = SharedSecretV1::new([0x55; 32]);
let message = [0xAA; 32];
let signature = shared_secret.authenticate(&message);
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
}
#[test]
fn test_shared_secret_v1_verify() {
let shared_secret = SharedSecretV1::new([0x55; 32]);
let message = [0xAA];
let signature = [
0x8B, 0x60, 0x15, 0x7D, 0xF3, 0x44, 0x82, 0x2E, 0x54, 0x34, 0x7A, 0x01, 0xFB, 0x02,
0x48, 0xA6,
];
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
assert_eq!(
shared_secret.verify(&[0xBB], &signature),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
shared_secret.verify(&message, &[0x12; 16]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_shared_secret_v2_encrypt_decrypt() {
let mut env = TestEnv::default();
let shared_secret = SharedSecretV2::new([0x55; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret.encrypt(env.rng(), &plaintext).unwrap();
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
}
#[test]
fn test_shared_secret_v2_authenticate_verify() {
let shared_secret = SharedSecretV2::new([0x55; 32]);
let message = [0xAA; 32];
let signature = shared_secret.authenticate(&message);
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
}
#[test]
fn test_shared_secret_v2_verify() {
let shared_secret = SharedSecretV2::new([0x55; 32]);
let message = [0xAA];
let signature = [
0xC0, 0x3F, 0x2A, 0x22, 0x5C, 0xC3, 0x4E, 0x05, 0xC1, 0x0E, 0x72, 0x9C, 0x8D, 0xD5,
0x7D, 0xE5, 0x98, 0x9C, 0x68, 0x15, 0xEC, 0xE2, 0x3A, 0x95, 0xD5, 0x90, 0xE1, 0xE9,
0x3F, 0xF0, 0x1A, 0xAF,
];
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
assert_eq!(
shared_secret.verify(&[0xBB], &signature),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
shared_secret.verify(&message, &[0x12; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_decapsulate_symmetric() {
let mut env = TestEnv::default();
let pin_protocol1 = PinProtocol::new(env.rng());
let pin_protocol2 = PinProtocol::new(env.rng());
for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] {
let shared_secret1 = pin_protocol1
.decapsulate(pin_protocol2.get_public_key(), protocol)
.unwrap();
let shared_secret2 = pin_protocol2
.decapsulate(pin_protocol1.get_public_key(), protocol)
.unwrap();
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret1.encrypt(env.rng(), &plaintext).unwrap();
assert_eq!(plaintext, shared_secret2.decrypt(&ciphertext).unwrap());
}
}
#[test]
fn test_verify_pin_uv_auth_token_v1() {
let token = [0x91; PIN_TOKEN_LENGTH];
let message = [0xAA];
let signature = [
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
0x49, 0x68,
];
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V1),
Ok(())
);
assert_eq!(
verify_pin_uv_auth_token(
&[0x12; PIN_TOKEN_LENGTH],
&message,
&signature,
PinUvAuthProtocol::V1
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V1),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_verify_pin_uv_auth_token_v2() {
let token = [0x91; PIN_TOKEN_LENGTH];
let message = [0xAA];
let signature = [
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
0x49, 0x68, 0x94, 0x90, 0x20, 0x53, 0x0F, 0xA3, 0xD2, 0x7A, 0x9F, 0xFD, 0xFA, 0x62,
0x36, 0x93, 0xF7, 0x84,
];
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V2),
Ok(())
);
assert_eq!(
verify_pin_uv_auth_token(
&[0x12; PIN_TOKEN_LENGTH],
&message,
&signature,
PinUvAuthProtocol::V2
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V2),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

View File

@@ -1,688 +0,0 @@
// Copyright 2019-2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::data_formats::{
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
};
use alloc::string::String;
use alloc::vec::Vec;
use sk_cbor as cbor;
use sk_cbor::{
cbor_array_vec, cbor_bool, cbor_int, cbor_map_collection, cbor_map_options, cbor_text,
};
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
pub enum ResponseData {
AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse),
AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse),
AuthenticatorGetNextAssertion(AuthenticatorGetAssertionResponse),
AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
AuthenticatorReset,
AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>),
AuthenticatorSelection,
AuthenticatorLargeBlobs(Option<AuthenticatorLargeBlobsResponse>),
AuthenticatorConfig,
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse),
AuthenticatorVendorUpgrade,
AuthenticatorVendorUpgradeInfo(AuthenticatorVendorUpgradeInfoResponse),
}
impl From<ResponseData> for Option<cbor::Value> {
fn from(response: ResponseData) -> Self {
match response {
ResponseData::AuthenticatorMakeCredential(data) => Some(data.into()),
ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()),
ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()),
ResponseData::AuthenticatorGetInfo(data) => Some(data.into()),
ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorReset => None,
ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorConfig => None,
ResponseData::AuthenticatorVendorConfigure(data) => Some(data.into()),
ResponseData::AuthenticatorVendorUpgrade => None,
ResponseData::AuthenticatorVendorUpgradeInfo(data) => Some(data.into()),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorMakeCredentialResponse {
pub fmt: String,
pub auth_data: Vec<u8>,
pub att_stmt: PackedAttestationStatement,
pub ep_att: Option<bool>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorMakeCredentialResponse> for cbor::Value {
fn from(make_credential_response: AuthenticatorMakeCredentialResponse) -> Self {
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
ep_att,
large_blob_key,
} = make_credential_response;
cbor_map_options! {
0x01 => fmt,
0x02 => auth_data,
0x03 => att_stmt,
0x04 => ep_att,
0x05 => large_blob_key,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorGetAssertionResponse {
pub credential: Option<PublicKeyCredentialDescriptor>,
pub auth_data: Vec<u8>,
pub signature: Vec<u8>,
pub user: Option<PublicKeyCredentialUserEntity>,
pub number_of_credentials: Option<u64>,
// 0x06: userSelected missing as we don't support displays.
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
fn from(get_assertion_response: AuthenticatorGetAssertionResponse) -> Self {
let AuthenticatorGetAssertionResponse {
credential,
auth_data,
signature,
user,
number_of_credentials,
large_blob_key,
} = get_assertion_response;
cbor_map_options! {
0x01 => credential,
0x02 => auth_data,
0x03 => signature,
0x04 => user,
0x05 => number_of_credentials,
0x07 => large_blob_key,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorGetInfoResponse {
pub versions: Vec<String>,
pub extensions: Option<Vec<String>>,
pub aaguid: [u8; 16],
pub options: Option<Vec<(String, bool)>>,
pub max_msg_size: Option<u64>,
pub pin_protocols: Option<Vec<u64>>,
pub max_credential_count_in_list: Option<u64>,
pub max_credential_id_length: Option<u64>,
pub transports: Option<Vec<AuthenticatorTransport>>,
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
pub max_serialized_large_blob_array: Option<u64>,
pub force_pin_change: Option<bool>,
pub min_pin_length: u8,
pub firmware_version: Option<u64>,
pub max_cred_blob_length: Option<u64>,
pub max_rp_ids_for_set_min_pin_length: Option<u64>,
// Missing response fields as they are only relevant for internal UV:
// - 0x11: preferredPlatformUvAttempts
// - 0x12: uvModality
// Add them when your hardware supports any kind of user verification within
// the boundary of the device, e.g. fingerprint or built-in keyboard.
pub certifications: Option<Vec<(String, i64)>>,
pub remaining_discoverable_credentials: Option<u64>,
// - 0x15: vendorPrototypeConfigCommands missing as we don't support it.
}
impl From<AuthenticatorGetInfoResponse> for cbor::Value {
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
let AuthenticatorGetInfoResponse {
versions,
extensions,
aaguid,
options,
max_msg_size,
pin_protocols,
max_credential_count_in_list,
max_credential_id_length,
transports,
algorithms,
max_serialized_large_blob_array,
force_pin_change,
min_pin_length,
firmware_version,
max_cred_blob_length,
max_rp_ids_for_set_min_pin_length,
certifications,
remaining_discoverable_credentials,
} = get_info_response;
let options_cbor: Option<cbor::Value> = options.map(|options| {
let options_map: Vec<(_, _)> = options
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_bool!(value)))
.collect();
cbor_map_collection!(options_map)
});
let certifications_cbor: Option<cbor::Value> = certifications.map(|certifications| {
let certifications_map: Vec<(_, _)> = certifications
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_int!(value)))
.collect();
cbor_map_collection!(certifications_map)
});
cbor_map_options! {
0x01 => cbor_array_vec!(versions),
0x02 => extensions.map(|vec| cbor_array_vec!(vec)),
0x03 => &aaguid,
0x04 => options_cbor,
0x05 => max_msg_size,
0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)),
0x07 => max_credential_count_in_list,
0x08 => max_credential_id_length,
0x09 => transports.map(|vec| cbor_array_vec!(vec)),
0x0A => algorithms.map(|vec| cbor_array_vec!(vec)),
0x0B => max_serialized_large_blob_array,
0x0C => force_pin_change,
0x0D => min_pin_length as u64,
0x0E => firmware_version,
0x0F => max_cred_blob_length,
0x10 => max_rp_ids_for_set_min_pin_length,
0x13 => certifications_cbor,
0x14 => remaining_discoverable_credentials,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorClientPinResponse {
pub key_agreement: Option<CoseKey>,
pub pin_uv_auth_token: Option<Vec<u8>>,
pub retries: Option<u64>,
pub power_cycle_state: Option<bool>,
// - 0x05: uvRetries missing as we don't support internal UV.
}
impl From<AuthenticatorClientPinResponse> for cbor::Value {
fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self {
let AuthenticatorClientPinResponse {
key_agreement,
pin_uv_auth_token,
retries,
power_cycle_state,
} = client_pin_response;
cbor_map_options! {
0x01 => key_agreement.map(cbor::Value::from),
0x02 => pin_uv_auth_token,
0x03 => retries,
0x04 => power_cycle_state,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorLargeBlobsResponse {
pub config: Vec<u8>,
}
impl From<AuthenticatorLargeBlobsResponse> for cbor::Value {
fn from(platform_large_blobs_response: AuthenticatorLargeBlobsResponse) -> Self {
let AuthenticatorLargeBlobsResponse { config } = platform_large_blobs_response;
cbor_map_options! {
0x01 => config,
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct AuthenticatorCredentialManagementResponse {
pub existing_resident_credentials_count: Option<u64>,
pub max_possible_remaining_resident_credentials_count: Option<u64>,
pub rp: Option<PublicKeyCredentialRpEntity>,
pub rp_id_hash: Option<Vec<u8>>,
pub total_rps: Option<u64>,
pub user: Option<PublicKeyCredentialUserEntity>,
pub credential_id: Option<PublicKeyCredentialDescriptor>,
pub public_key: Option<CoseKey>,
pub total_credentials: Option<u64>,
pub cred_protect: Option<CredentialProtectionPolicy>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorCredentialManagementResponse> for cbor::Value {
fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self {
let AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count,
max_possible_remaining_resident_credentials_count,
rp,
rp_id_hash,
total_rps,
user,
credential_id,
public_key,
total_credentials,
cred_protect,
large_blob_key,
} = cred_management_response;
cbor_map_options! {
0x01 => existing_resident_credentials_count,
0x02 => max_possible_remaining_resident_credentials_count,
0x03 => rp,
0x04 => rp_id_hash,
0x05 => total_rps,
0x06 => user,
0x07 => credential_id,
0x08 => public_key.map(cbor::Value::from),
0x09 => total_credentials,
0x0A => cred_protect,
0x0B => large_blob_key,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorVendorConfigureResponse {
pub cert_programmed: bool,
pub pkey_programmed: bool,
}
impl From<AuthenticatorVendorConfigureResponse> for cbor::Value {
fn from(vendor_response: AuthenticatorVendorConfigureResponse) -> Self {
let AuthenticatorVendorConfigureResponse {
cert_programmed,
pkey_programmed,
} = vendor_response;
cbor_map_options! {
0x01 => cert_programmed,
0x02 => pkey_programmed,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorVendorUpgradeInfoResponse {
pub info: u32,
}
impl From<AuthenticatorVendorUpgradeInfoResponse> for cbor::Value {
fn from(vendor_upgrade_info_response: AuthenticatorVendorUpgradeInfoResponse) -> Self {
let AuthenticatorVendorUpgradeInfoResponse { info } = vendor_upgrade_info_response;
cbor_map_options! {
0x01 => info as u64,
}
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType};
use super::super::ES256_CRED_PARAM;
use super::*;
use crate::env::test::TestEnv;
use cbor::{cbor_array, cbor_bytes, cbor_map};
#[test]
fn test_make_credential_into_cbor() {
let certificate = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]];
let att_stmt = PackedAttestationStatement {
alg: 1,
sig: vec![0x55, 0x55, 0x55, 0x55],
x5c: Some(vec![vec![0x5C, 0x5C, 0x5C, 0x5C]]),
ecdaa_key_id: Some(vec![0xEC, 0xDA, 0x1D]),
};
let cbor_packed_attestation_statement = cbor_map! {
"alg" => 1,
"sig" => vec![0x55, 0x55, 0x55, 0x55],
"x5c" => cbor_array![certificate],
"ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D],
};
let make_credential_response = AuthenticatorMakeCredentialResponse {
fmt: "packed".to_string(),
auth_data: vec![0xAD],
att_stmt,
ep_att: Some(true),
large_blob_key: Some(vec![0x1B]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorMakeCredential(make_credential_response).into();
let expected_cbor = cbor_map_options! {
0x01 => "packed",
0x02 => vec![0xAD],
0x03 => cbor_packed_attestation_statement,
0x04 => true,
0x05 => vec![0x1B],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_assertion_into_cbor() {
let pub_key_cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
transports: Some(vec![AuthenticatorTransport::Usb]),
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0x1D, 0x1D, 0x1D, 0x1D],
user_name: Some("foo".to_string()),
user_display_name: Some("bar".to_string()),
user_icon: Some("example.com/foo/icon.png".to_string()),
};
let get_assertion_response = AuthenticatorGetAssertionResponse {
credential: Some(pub_key_cred_descriptor),
auth_data: vec![0xAD],
signature: vec![0x51],
user: Some(user),
number_of_credentials: Some(2),
large_blob_key: Some(vec![0x1B]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetAssertion(get_assertion_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_map! {
"id" => vec![0x2D, 0x2D, 0x2D, 0x2D],
"type" => "public-key",
"transports" => cbor_array!["usb"],
},
0x02 => vec![0xAD],
0x03 => vec![0x51],
0x04 => cbor_map! {
"id" => vec![0x1D, 0x1D, 0x1D, 0x1D],
"icon" => "example.com/foo/icon.png".to_string(),
"name" => "foo".to_string(),
"displayName" => "bar".to_string(),
},
0x05 => 2,
0x07 => vec![0x1B],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_info_into_cbor() {
let versions = vec!["FIDO_2_0".to_string()];
let get_info_response = AuthenticatorGetInfoResponse {
versions: versions.clone(),
extensions: None,
aaguid: [0x00; 16],
options: None,
max_msg_size: None,
pin_protocols: None,
max_credential_count_in_list: None,
max_credential_id_length: None,
transports: None,
algorithms: None,
max_serialized_large_blob_array: None,
force_pin_change: None,
min_pin_length: 4,
firmware_version: None,
max_cred_blob_length: None,
max_rp_ids_for_set_min_pin_length: None,
certifications: None,
remaining_discoverable_credentials: None,
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![versions],
0x03 => vec![0x00; 16],
0x0D => 4,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_info_optionals_into_cbor() {
let get_info_response = AuthenticatorGetInfoResponse {
versions: vec!["FIDO_2_0".to_string()],
extensions: Some(vec!["extension".to_string()]),
aaguid: [0x00; 16],
options: Some(vec![(String::from("rk"), true)]),
max_msg_size: Some(1024),
pin_protocols: Some(vec![1]),
max_credential_count_in_list: Some(20),
max_credential_id_length: Some(256),
transports: Some(vec![AuthenticatorTransport::Usb]),
algorithms: Some(vec![ES256_CRED_PARAM]),
max_serialized_large_blob_array: Some(1024),
force_pin_change: Some(false),
min_pin_length: 4,
firmware_version: Some(0),
max_cred_blob_length: Some(1024),
max_rp_ids_for_set_min_pin_length: Some(8),
certifications: Some(vec![(String::from("example-cert"), 1)]),
remaining_discoverable_credentials: Some(150),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_array!["FIDO_2_0"],
0x02 => cbor_array!["extension"],
0x03 => vec![0x00; 16],
0x04 => cbor_map! {"rk" => true},
0x05 => 1024,
0x06 => cbor_array![1],
0x07 => 20,
0x08 => 256,
0x09 => cbor_array!["usb"],
0x0A => cbor_array![ES256_CRED_PARAM],
0x0B => 1024,
0x0C => false,
0x0D => 4,
0x0E => 0,
0x0F => 1024,
0x10 => 8,
0x13 => cbor_map! {"example-cert" => 1},
0x14 => 150,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_client_pin_into_cbor() {
let mut env = TestEnv::default();
let sk = crypto::ecdh::SecKey::gensk(env.rng());
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let client_pin_response = AuthenticatorClientPinResponse {
key_agreement: Some(cose_key.clone()),
pin_uv_auth_token: Some(vec![70]),
retries: Some(8),
power_cycle_state: Some(false),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor::Value::from(cose_key),
0x02 => vec![70],
0x03 => 8,
0x04 => false,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_client_pin_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorClientPin(None).into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_reset_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorReset.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_used_credential_management_into_cbor() {
let cred_management_response = AuthenticatorCredentialManagementResponse::default();
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_credential_management_optionals_into_cbor() {
let mut env = TestEnv::default();
let sk = crypto::ecdh::SecKey::gensk(env.rng());
let rp = PublicKeyCredentialRpEntity {
rp_id: String::from("example.com"),
rp_name: None,
rp_icon: None,
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0xFA, 0xB1, 0xA2],
user_name: None,
user_display_name: None,
user_icon: None,
};
let cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None,
};
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let cred_management_response = AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(100),
max_possible_remaining_resident_credentials_count: Some(96),
rp: Some(rp.clone()),
rp_id_hash: Some(vec![0x1D; 32]),
total_rps: Some(3),
user: Some(user.clone()),
credential_id: Some(cred_descriptor.clone()),
public_key: Some(cose_key.clone()),
total_credentials: Some(2),
cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
large_blob_key: Some(vec![0xBB; 64]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => 100,
0x02 => 96,
0x03 => rp,
0x04 => vec![0x1D; 32],
0x05 => 3,
0x06 => user,
0x07 => cred_descriptor,
0x08 => cbor::Value::from(cose_key),
0x09 => 2,
0x0A => 0x01,
0x0B => vec![0xBB; 64],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_credential_management_into_cbor() {
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(None).into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_selection_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_large_blobs_into_cbor() {
let large_blobs_response = AuthenticatorLargeBlobsResponse { config: vec![0xC0] };
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorLargeBlobs(Some(large_blobs_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => vec![0xC0],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_large_blobs_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorLargeBlobs(None).into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_config_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_vendor_response_into_cbor() {
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: false,
})
.into();
assert_eq!(
response_cbor,
Some(cbor_map_options! {
0x01 => true,
0x02 => false,
})
);
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse {
cert_programmed: false,
pkey_programmed: true,
})
.into();
assert_eq!(
response_cbor,
Some(cbor_map_options! {
0x01 => false,
0x02 => true,
})
);
}
#[test]
fn test_vendor_upgrade_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorVendorUpgrade.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_vendor_upgrade_info_into_cbor() {
let vendor_upgrade_info_response =
AuthenticatorVendorUpgradeInfoResponse { info: 0x00060000 };
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorVendorUpgradeInfo(vendor_upgrade_info_response).into();
let expected_cbor = cbor_map! {
0x01 => 0x00060000,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
}

View File

@@ -1,113 +0,0 @@
// Copyright 2019-2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::user_presence::UserPresenceError;
use crate::api::{attestation_store, key_store};
// CTAP specification (version 20190130) section 6.3
// For now, only the CTAP2 codes are here, the CTAP1 are not included.
#[allow(non_camel_case_types)]
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Ctap2StatusCode {
CTAP2_OK = 0x00,
CTAP1_ERR_INVALID_COMMAND = 0x01,
CTAP1_ERR_INVALID_PARAMETER = 0x02,
CTAP1_ERR_INVALID_LENGTH = 0x03,
CTAP1_ERR_INVALID_SEQ = 0x04,
CTAP1_ERR_TIMEOUT = 0x05,
CTAP1_ERR_CHANNEL_BUSY = 0x06,
CTAP1_ERR_LOCK_REQUIRED = 0x0A,
CTAP1_ERR_INVALID_CHANNEL = 0x0B,
CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11,
CTAP2_ERR_INVALID_CBOR = 0x12,
CTAP2_ERR_MISSING_PARAMETER = 0x14,
CTAP2_ERR_LIMIT_EXCEEDED = 0x15,
CTAP2_ERR_FP_DATABASE_FULL = 0x17,
CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18,
CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,
CTAP2_ERR_PROCESSING = 0x21,
CTAP2_ERR_INVALID_CREDENTIAL = 0x22,
CTAP2_ERR_USER_ACTION_PENDING = 0x23,
CTAP2_ERR_OPERATION_PENDING = 0x24,
CTAP2_ERR_NO_OPERATIONS = 0x25,
CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26,
CTAP2_ERR_OPERATION_DENIED = 0x27,
CTAP2_ERR_KEY_STORE_FULL = 0x28,
CTAP2_ERR_NO_OPERATION_PENDING = 0x2A,
CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B,
CTAP2_ERR_INVALID_OPTION = 0x2C,
CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D,
CTAP2_ERR_NO_CREDENTIALS = 0x2E,
CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F,
CTAP2_ERR_NOT_ALLOWED = 0x30,
CTAP2_ERR_PIN_INVALID = 0x31,
CTAP2_ERR_PIN_BLOCKED = 0x32,
CTAP2_ERR_PIN_AUTH_INVALID = 0x33,
CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34,
CTAP2_ERR_PIN_NOT_SET = 0x35,
CTAP2_ERR_PUAT_REQUIRED = 0x36,
CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37,
CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38,
CTAP2_ERR_REQUEST_TOO_LARGE = 0x39,
CTAP2_ERR_ACTION_TIMEOUT = 0x3A,
CTAP2_ERR_UP_REQUIRED = 0x3B,
CTAP2_ERR_UV_BLOCKED = 0x3C,
CTAP2_ERR_INTEGRITY_FAILURE = 0x3D,
CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E,
CTAP2_ERR_UV_INVALID = 0x3F,
CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40,
CTAP1_ERR_OTHER = 0x7F,
_CTAP2_ERR_SPEC_LAST = 0xDF,
_CTAP2_ERR_EXTENSION_FIRST = 0xE0,
_CTAP2_ERR_EXTENSION_LAST = 0xEF,
_CTAP2_ERR_VENDOR_FIRST = 0xF0,
/// An internal invariant is broken.
///
/// This type of error is unexpected and the current state is undefined.
CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2,
/// The hardware is malfunctioning.
///
/// It may be possible that some of those errors are actually internal errors.
CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3,
_CTAP2_ERR_VENDOR_LAST = 0xFF,
}
impl From<UserPresenceError> for Ctap2StatusCode {
fn from(user_presence_error: UserPresenceError) -> Self {
match user_presence_error {
UserPresenceError::Timeout => Self::CTAP2_ERR_USER_ACTION_TIMEOUT,
UserPresenceError::Declined => Self::CTAP2_ERR_OPERATION_DENIED,
UserPresenceError::Canceled => Self::CTAP2_ERR_KEEPALIVE_CANCEL,
}
}
}
impl From<key_store::Error> for Ctap2StatusCode {
fn from(_: key_store::Error) -> Self {
Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR
}
}
impl From<attestation_store::Error> for Ctap2StatusCode {
fn from(error: attestation_store::Error) -> Self {
use attestation_store::Error;
match error {
Error::Storage => Self::CTAP2_ERR_VENDOR_HARDWARE_FAILURE,
Error::Internal => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
Error::NoSupport => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,161 +0,0 @@
// Copyright 2019-2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Number of keys that persist the CTAP reset command.
pub const NUM_PERSISTENT_KEYS: usize = 20;
/// Defines a key given its name and value or range of values.
macro_rules! make_key {
($(#[$doc: meta])* $name: ident = $key: literal..$end: literal) => {
$(#[$doc])* pub const $name: core::ops::Range<usize> = $key..$end;
};
($(#[$doc: meta])* $name: ident = $key: literal) => {
$(#[$doc])* pub const $name: usize = $key;
};
}
/// Returns the range of values of a key given its value description.
#[cfg(test)]
macro_rules! make_range {
($key: literal..$end: literal) => {
$key..$end
};
($key: literal) => {
$key..$key + 1
};
}
/// Helper to define keys as a partial partition of a range.
macro_rules! make_partition {
($range: expr,
$(
$(#[$doc: meta])*
$name: ident = $key: literal $(.. $end: literal)?;
)*) => {
$(
make_key!($(#[$doc])* $name = $key $(.. $end)?);
)*
#[cfg(test)]
const KEY_RANGE: core::ops::Range<usize> = $range;
#[cfg(test)]
const ALL_KEYS: &[core::ops::Range<usize>] = &[$(make_range!($key $(.. $end)?)),*];
};
}
make_partition! {
// We reserve 0 and 2048+ for possible migration purposes. We add persistent entries starting
// from 1 and going up. We add non-persistent entries starting from 2047 and going down. This
// way, we don't commit to a fixed number of persistent keys.
1..2048,
// WARNING: Keys should not be deleted but prefixed with `_` to avoid accidentally reusing them.
/// Reserved for the attestation store implementation of the environment.
_RESERVED_ATTESTATION_STORE = 1..3;
/// Used for the AAGUID before, but deprecated.
_AAGUID = 3;
// This is the persistent key limit:
// - When adding a (persistent) key above this message, make sure its value is smaller than
// NUM_PERSISTENT_KEYS.
// - When adding a (non-persistent) key below this message, make sure its value is bigger or
// equal than NUM_PERSISTENT_KEYS.
/// Reserved for future credential-related objects.
///
/// In particular, additional credentials could be added there by reducing the lower bound of
/// the credential range below as well as the upper bound of this range in a similar manner.
_RESERVED_CREDENTIALS = 1000..1700;
/// The credentials.
///
/// Depending on `Customization::max_supported_resident_keys()`, only a prefix of those keys is used.
/// Each board may configure `Customization::max_supported_resident_keys()` depending on the
/// storage size.
CREDENTIALS = 1700..2000;
/// Storage for the serialized large blob array.
///
/// The stored large blob can be too big for one key, so it has to be sharded.
LARGE_BLOB_SHARDS = 2000..2004;
/// If this entry exists and is empty, alwaysUv is enabled.
ALWAYS_UV = 2038;
/// If this entry exists and is empty, enterprise attestation is enabled.
ENTERPRISE_ATTESTATION = 2039;
/// If this entry exists and is empty, the PIN needs to be changed.
FORCE_PIN_CHANGE = 2040;
/// The secret of the CredRandom feature.
CRED_RANDOM_SECRET = 2041;
/// List of RP IDs allowed to read the minimum PIN length.
MIN_PIN_LENGTH_RP_IDS = 2042;
/// The minimum PIN length.
///
/// If the entry is absent, the minimum PIN length is `Customization::default_min_pin_length()`.
MIN_PIN_LENGTH = 2043;
/// The number of PIN retries.
///
/// If the entry is absent, the number of PIN retries is `Customization::max_pin_retries()`.
PIN_RETRIES = 2044;
/// The PIN hash and length.
///
/// If the entry is absent, there is no PIN set. The first byte represents
/// the length, the following are an array with the hash.
PIN_PROPERTIES = 2045;
/// Reserved for the key store implementation of the environment.
_RESERVED_KEY_STORE = 2046;
/// The global signature counter.
///
/// If the entry is absent, the counter is 0.
GLOBAL_SIGNATURE_COUNTER = 2047;
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::env::test::TestEnv;
use crate::env::Env;
#[test]
fn enough_credentials() {
let env = TestEnv::default();
assert!(
env.customization().max_supported_resident_keys()
<= CREDENTIALS.end - CREDENTIALS.start
);
}
#[test]
fn keys_are_disjoint() {
// Check that keys are in the range.
for keys in ALL_KEYS {
assert!(KEY_RANGE.start <= keys.start && keys.end <= KEY_RANGE.end);
}
// Check that keys are assigned at most once, essentially partitioning the range.
for key in KEY_RANGE {
assert!(ALL_KEYS.iter().filter(|keys| keys.contains(&key)).count() <= 1);
}
}
}

View File

@@ -1,275 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::clock::Clock;
use crate::ctap::client_pin::PinPermission;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::env::Env;
use alloc::string::String;
use crypto::sha256::Sha256;
use crypto::Hash256;
/// Timeout for auth tokens.
///
/// This usage time limit is correct for USB, BLE, and internal.
/// NFC only allows 19.8 seconds.
/// TODO(#15) multiplex over transports, add NFC
const INITIAL_USAGE_TIME_LIMIT_MS: usize = 30000;
/// Implements pinUvAuthToken state from section 6.5.2.1.
///
/// The userPresent flag is omitted as the only way to set it to true is
/// built-in user verification. Therefore, we never cache user presence.
///
/// This implementation does not use a rolling timer.
pub struct PinUvAuthTokenState<E: Env> {
// Relies on the fact that all permissions are represented by powers of two.
permissions_set: u8,
permissions_rp_id: Option<String>,
usage_timer: <E::Clock as Clock>::Timer,
user_verified: bool,
in_use: bool,
}
impl<E: Env> PinUvAuthTokenState<E> {
/// Creates a pinUvAuthToken state without permissions.
pub fn new() -> Self {
PinUvAuthTokenState {
permissions_set: 0,
permissions_rp_id: None,
usage_timer: <E::Clock as Clock>::Timer::default(),
user_verified: false,
in_use: false,
}
}
/// Returns whether the pinUvAuthToken is active.
pub fn is_in_use(&self) -> bool {
self.in_use
}
/// Checks if the permission is granted.
pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> {
if permission as u8 & self.permissions_set != 0 {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
/// Checks if there is no associated permissions RPID.
pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> {
if self.permissions_rp_id.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
Ok(())
}
/// Checks if the permissions RPID is associated.
pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
match &self.permissions_rp_id {
Some(p) if rp_id == p => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Checks if the permissions RPID's association matches the hash.
pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> {
match &self.permissions_rp_id {
Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Sets the permissions, represented as bits in a byte.
pub fn set_permissions(&mut self, permissions: u8) {
self.permissions_set = permissions;
}
/// Sets the permissions RPID.
pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option<String>) {
self.permissions_rp_id = permissions_rp_id;
}
/// Sets the default permissions.
///
/// Allows MakeCredential and GetAssertion, without specifying a RP ID.
pub fn set_default_permissions(&mut self) {
self.set_permissions(0x03);
self.set_permissions_rp_id(None);
}
/// Starts the timer for pinUvAuthToken usage.
pub fn begin_using_pin_uv_auth_token(&mut self, env: &mut E) {
self.user_verified = true;
self.usage_timer = env.clock().make_timer(INITIAL_USAGE_TIME_LIMIT_MS);
self.in_use = true;
}
/// Updates the usage timer, and disables the pinUvAuthToken on timeout.
pub fn pin_uv_auth_token_usage_timer_observer(&mut self, env: &mut E) {
if !self.in_use {
return;
}
if env.clock().is_elapsed(&self.usage_timer) {
self.stop_using_pin_uv_auth_token();
}
}
/// Returns whether the user is verified.
pub fn get_user_verified_flag_value(&self) -> bool {
self.in_use && self.user_verified
}
/// Consumes the user verification.
pub fn clear_user_verified_flag(&mut self) {
self.user_verified = false;
}
/// Clears all permissions except Large Blob Write.
pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) {
self.permissions_set &= PinPermission::LargeBlobWrite as u8;
}
/// Resets to the initial state.
pub fn stop_using_pin_uv_auth_token(&mut self) {
self.permissions_rp_id = None;
self.permissions_set = 0;
self.usage_timer = <E::Clock as Clock>::Timer::default();
self.user_verified = false;
self.in_use = false;
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
use enum_iterator::IntoEnumIterator;
#[test]
fn test_observer() {
let mut env = TestEnv::default();
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.is_in_use());
env.clock().advance(100);
token_state.pin_uv_auth_token_usage_timer_observer(&mut env);
assert!(token_state.is_in_use());
env.clock().advance(INITIAL_USAGE_TIME_LIMIT_MS);
token_state.pin_uv_auth_token_usage_timer_observer(&mut env);
assert!(!token_state.is_in_use());
}
#[test]
fn test_stop() {
let mut env = TestEnv::default();
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.is_in_use());
token_state.stop_using_pin_uv_auth_token();
assert!(!token_state.is_in_use());
}
#[test]
fn test_permissions() {
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
token_state.set_permissions(0xFF);
for permission in PinPermission::into_enum_iter() {
assert_eq!(token_state.has_permission(permission), Ok(()));
}
token_state.clear_pin_uv_auth_token_permissions_except_lbw();
assert_eq!(
token_state.has_permission(PinPermission::CredentialManagement),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permission(PinPermission::LargeBlobWrite),
Ok(())
);
token_state.stop_using_pin_uv_auth_token();
for permission in PinPermission::into_enum_iter() {
assert_eq!(
token_state.has_permission(permission),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}
#[test]
fn test_permissions_rp_id_none() {
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
let example_hash = Sha256::hash(b"example.com");
token_state.set_permissions_rp_id(None);
assert_eq!(token_state.has_no_permissions_rp_id(), Ok(()));
assert_eq!(
token_state.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_permissions_rp_id_some() {
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
let example_hash = Sha256::hash(b"example.com");
token_state.set_permissions_rp_id(Some(String::from("example.com")));
assert_eq!(
token_state.has_no_permissions_rp_id(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(()));
assert_eq!(
token_state.has_permissions_rp_id("another.example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Ok(())
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&[0x1D; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
token_state.stop_using_pin_uv_auth_token();
assert_eq!(
token_state.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_user_verified_flag() {
let mut env = TestEnv::default();
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
assert!(!token_state.get_user_verified_flag_value());
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.get_user_verified_flag_value());
token_state.clear_user_verified_flag();
assert!(!token_state.get_user_verified_flag_value());
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.get_user_verified_flag_value());
token_state.stop_using_pin_uv_auth_token();
assert!(!token_state.get_user_verified_flag_value());
}
}

View File

@@ -1,137 +0,0 @@
// Copyright 2019-2021 Google LLC
//
// Licensed under the Apache License, Version 2 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::TOUCH_TIMEOUT_MS;
use crate::api::clock::Clock;
use crate::env::Env;
const U2F_UP_PROMPT_TIMEOUT_MS: usize = 10000;
pub struct U2fUserPresenceState<E: Env> {
/// If user presence was recently requested, its timeout is saved here.
needs_up: <E::Clock as Clock>::Timer,
/// Button touch timeouts, while user presence is requested, are saved here.
has_up: <E::Clock as Clock>::Timer,
}
impl<E: Env> U2fUserPresenceState<E> {
pub fn new() -> U2fUserPresenceState<E> {
U2fUserPresenceState {
needs_up: <E::Clock as Clock>::Timer::default(),
has_up: <E::Clock as Clock>::Timer::default(),
}
}
/// Allows consuming user presence until timeout, if it was needed.
///
/// If user presence was not requested, granting user presence does nothing.
pub fn grant_up(&mut self, env: &mut E) {
if !env.clock().is_elapsed(&self.needs_up) {
self.needs_up = <E::Clock as Clock>::Timer::default();
self.has_up = env.clock().make_timer(TOUCH_TIMEOUT_MS);
}
}
/// Returns whether user presence was granted within the timeout and not yet consumed.
pub fn consume_up(&mut self, env: &mut E) -> bool {
if !env.clock().is_elapsed(&self.has_up) {
self.has_up = <E::Clock as Clock>::Timer::default();
true
} else {
self.needs_up = env.clock().make_timer(U2F_UP_PROMPT_TIMEOUT_MS);
false
}
}
/// Returns whether user presence was requested.
///
/// This function doesn't represent interaction with the environment, and does not change the
/// state, i.e. neither grants nor consumes user presence.
pub fn is_up_needed(&mut self, env: &mut E) -> bool {
!env.clock().is_elapsed(&self.needs_up)
}
}
#[cfg(feature = "with_ctap1")]
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
fn big_positive() -> usize {
1000000
}
fn grant_up_when_needed(env: &mut TestEnv) {
let mut u2f_state = U2fUserPresenceState::new();
assert!(!u2f_state.consume_up(env));
assert!(u2f_state.is_up_needed(env));
u2f_state.grant_up(env);
assert!(u2f_state.consume_up(env));
assert!(!u2f_state.consume_up(env));
}
fn need_up_timeout(env: &mut TestEnv) {
let mut u2f_state = U2fUserPresenceState::new();
assert!(!u2f_state.consume_up(env));
assert!(u2f_state.is_up_needed(env));
env.clock().advance(U2F_UP_PROMPT_TIMEOUT_MS);
// The timeout excludes equality, so it should be over at this instant.
assert!(!u2f_state.is_up_needed(env));
}
fn grant_up_timeout(env: &mut TestEnv) {
let mut u2f_state = U2fUserPresenceState::new();
assert!(!u2f_state.consume_up(env));
assert!(u2f_state.is_up_needed(env));
u2f_state.grant_up(env);
env.clock().advance(TOUCH_TIMEOUT_MS);
// The timeout excludes equality, so it should be over at this instant.
assert!(!u2f_state.consume_up(env));
}
#[test]
fn test_grant_up_timeout() {
let mut env = TestEnv::default();
grant_up_timeout(&mut env);
env.clock().advance(big_positive());
grant_up_timeout(&mut env);
}
#[test]
fn test_need_up_timeout() {
let mut env = TestEnv::default();
need_up_timeout(&mut env);
env.clock().advance(big_positive());
need_up_timeout(&mut env);
}
#[test]
fn test_grant_up_when_needed() {
let mut env = TestEnv::default();
grant_up_when_needed(&mut env);
env.clock().advance(big_positive());
grant_up_when_needed(&mut env);
}
#[test]
fn test_grant_up_without_need() {
let mut env = TestEnv::default();
let mut u2f_state = U2fUserPresenceState::new();
u2f_state.grant_up(&mut env);
assert!(!u2f_state.is_up_needed(&mut env));
assert!(!u2f_state.consume_up(&mut env));
}
}

View File

@@ -1,153 +0,0 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::ctap::hid::{
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
};
use crate::ctap::{Channel, CtapState};
use crate::env::Env;
/// Implements the non-standard command processing for HID.
///
/// Outside of the pure HID commands like INIT, only PING and CBOR commands are allowed.
pub struct VendorHid<E: Env> {
hid: CtapHid<E>,
}
impl<E: Env> Default for VendorHid<E> {
/// Instantiates a HID handler for CTAP1, CTAP2 and Wink.
fn default() -> Self {
let hid = CtapHid::<E>::new(CtapHid::<E>::CAPABILITY_CBOR | CtapHid::<E>::CAPABILITY_NMSG);
VendorHid { hid }
}
}
impl<E: Env> VendorHid<E> {
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
pub fn process_hid_packet(
&mut self,
env: &mut E,
packet: &HidPacket,
ctap_state: &mut CtapState<E>,
) -> HidPacketIterator {
if let Some(message) = self.hid.parse_packet(env, packet) {
let processed_message = self.process_message(env, message, ctap_state);
debug_ctap!(
env,
"Sending message through the second usage page: {:02x?}",
processed_message
);
CtapHid::<E>::split_message(processed_message)
} else {
HidPacketIterator::none()
}
}
/// Processes a message's commands that affect the protocol outside HID.
pub fn process_message(
&mut self,
env: &mut E,
message: Message,
ctap_state: &mut CtapState<E>,
) -> Message {
let cid = message.cid;
match message.cmd {
// There are no custom CTAP1 commands.
CtapHidCommand::Msg => CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd),
// The CTAP2 processing function multiplexes internally.
CtapHidCommand::Cbor => {
let response =
ctap_state.process_command(env, &message.payload, Channel::VendorHid(cid));
Message {
cid,
cmd: CtapHidCommand::Cbor,
payload: response,
}
}
// Call Wink over the main HID.
CtapHidCommand::Wink => CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd),
// All other commands have already been processed, keep them as is.
_ => message,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ctap::hid::ChannelID;
use crate::env::test::TestEnv;
fn new_initialized() -> (VendorHid<TestEnv>, ChannelID) {
let (hid, cid) = CtapHid::new_initialized();
(VendorHid::<TestEnv> { hid }, cid)
}
#[test]
fn test_process_hid_packet() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut vendor_hid, cid) = new_initialized();
let mut ping_packet = [0x00; 64];
ping_packet[..4].copy_from_slice(&cid);
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
let mut response = vendor_hid.process_hid_packet(&mut env, &ping_packet, &mut ctap_state);
assert_eq!(response.next(), Some(ping_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_process_hid_packet_empty() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut vendor_hid, cid) = new_initialized();
let mut cancel_packet = [0x00; 64];
cancel_packet[..4].copy_from_slice(&cid);
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
let mut response = vendor_hid.process_hid_packet(&mut env, &cancel_packet, &mut ctap_state);
assert_eq!(response.next(), None);
}
#[test]
fn test_blocked_commands() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut vendor_hid, cid) = new_initialized();
// Usually longer, but we don't parse them anyway.
let mut msg_packet = [0x00; 64];
msg_packet[..4].copy_from_slice(&cid);
msg_packet[4..7].copy_from_slice(&[0x83, 0x00, 0x00]);
let mut wink_packet = [0x00; 64];
wink_packet[..4].copy_from_slice(&cid);
wink_packet[4..7].copy_from_slice(&[0x88, 0x00, 0x00]);
let mut error_packet = [0x00; 64];
error_packet[..4].copy_from_slice(&cid);
error_packet[4..8].copy_from_slice(&[0xBF, 0x00, 0x01, 0x01]);
let mut response = vendor_hid.process_hid_packet(&mut env, &msg_packet, &mut ctap_state);
assert_eq!(response.next(), Some(error_packet));
assert_eq!(response.next(), None);
let mut response = vendor_hid.process_hid_packet(&mut env, &wink_packet, &mut ctap_state);
assert_eq!(response.next(), Some(error_packet));
assert_eq!(response.next(), None);
}
}

62
src/env/mod.rs vendored
View File

@@ -1,4 +1,4 @@
// Copyright 2022-2023 Google LLC
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,64 +12,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::attestation_store::AttestationStore;
use crate::api::clock::Clock;
use crate::api::connection::HidConnection;
use crate::api::customization::Customization;
use crate::api::firmware_protection::FirmwareProtection;
use crate::api::key_store::KeyStore;
use crate::api::upgrade_storage::UpgradeStorage;
use crate::api::user_presence::UserPresence;
use persistent_store::{Storage, Store};
use rng256::Rng256;
#[cfg(feature = "std")]
pub mod test;
pub mod tock;
/// Describes what CTAP needs to function.
pub trait Env {
type Rng: Rng256;
type UserPresence: UserPresence;
type Storage: Storage;
type KeyStore: KeyStore;
type UpgradeStorage: UpgradeStorage;
type FirmwareProtection: FirmwareProtection;
type Write: core::fmt::Write;
type Customization: Customization;
type HidConnection: HidConnection;
type AttestationStore: AttestationStore;
type Clock: Clock;
fn rng(&mut self) -> &mut Self::Rng;
fn user_presence(&mut self) -> &mut Self::UserPresence;
fn store(&mut self) -> &mut Store<Self::Storage>;
fn key_store(&mut self) -> &mut Self::KeyStore;
fn attestation_store(&mut self) -> &mut Self::AttestationStore;
fn clock(&mut self) -> &mut Self::Clock;
/// Returns the upgrade storage instance.
///
/// Upgrade storage is optional, so implementations may return `None`. However, implementations
/// should either always return `None` or always return `Some`.
fn upgrade_storage(&mut self) -> Option<&mut Self::UpgradeStorage>;
fn firmware_protection(&mut self) -> &mut Self::FirmwareProtection;
/// Creates a write instance for debugging.
///
/// This API doesn't return a reference such that drop may flush. This matches the Tock
/// environment. Non-Tock embedded environments should use the defmt feature (to be implemented
/// using the defmt crate) and ignore this API. Non-embedded environments may either use this
/// API or use the log feature (to be implemented using the log crate).
fn write(&mut self) -> Self::Write;
fn customization(&self) -> &Self::Customization;
/// I/O connection for sending packets implementing CTAP HID protocol.
fn main_hid_connection(&mut self) -> &mut Self::HidConnection;
/// I/O connection for sending packets implementing vendor extensions to CTAP HID protocol.
#[cfg(feature = "vendor_hid")]
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection;
}

View File

@@ -1,195 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::customization::{Customization, CustomizationImpl, AAGUID_LENGTH};
use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode};
use alloc::string::String;
use alloc::vec::Vec;
pub struct TestCustomization {
aaguid: &'static [u8; AAGUID_LENGTH],
allows_pin_protocol_v1: bool,
default_cred_protect: Option<CredentialProtectionPolicy>,
default_min_pin_length: u8,
default_min_pin_length_rp_ids: Vec<String>,
enforce_always_uv: bool,
enterprise_attestation_mode: Option<EnterpriseAttestationMode>,
enterprise_rp_id_list: Vec<String>,
max_msg_size: usize,
max_pin_retries: u8,
use_batch_attestation: bool,
use_signature_counter: bool,
max_cred_blob_length: usize,
max_credential_count_in_list: Option<usize>,
max_large_blob_array_size: usize,
max_rp_ids_length: usize,
max_supported_resident_keys: usize,
}
impl TestCustomization {
pub fn set_allows_pin_protocol_v1(&mut self, is_allowed: bool) {
self.allows_pin_protocol_v1 = is_allowed;
}
pub fn setup_enterprise_attestation(
&mut self,
mode: Option<EnterpriseAttestationMode>,
rp_id_list: Option<Vec<String>>,
) {
self.enterprise_attestation_mode = mode;
if let Some(rp_id_list) = rp_id_list {
self.enterprise_rp_id_list = rp_id_list;
}
}
}
impl Customization for TestCustomization {
fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH] {
self.aaguid
}
fn allows_pin_protocol_v1(&self) -> bool {
self.allows_pin_protocol_v1
}
fn default_cred_protect(&self) -> Option<CredentialProtectionPolicy> {
self.default_cred_protect
}
fn default_min_pin_length(&self) -> u8 {
self.default_min_pin_length
}
fn default_min_pin_length_rp_ids(&self) -> Vec<String> {
self.default_min_pin_length_rp_ids.clone()
}
fn enforce_always_uv(&self) -> bool {
self.enforce_always_uv
}
fn enterprise_attestation_mode(&self) -> Option<EnterpriseAttestationMode> {
self.enterprise_attestation_mode
}
fn enterprise_rp_id_list(&self) -> Vec<String> {
self.enterprise_rp_id_list.clone()
}
fn is_enterprise_rp_id(&self, rp_id: &str) -> bool {
self.enterprise_rp_id_list.iter().any(|id| id == rp_id)
}
fn max_msg_size(&self) -> usize {
self.max_msg_size
}
fn max_pin_retries(&self) -> u8 {
self.max_pin_retries
}
fn use_batch_attestation(&self) -> bool {
self.use_batch_attestation
}
fn use_signature_counter(&self) -> bool {
self.use_signature_counter
}
fn max_cred_blob_length(&self) -> usize {
self.max_cred_blob_length
}
fn max_credential_count_in_list(&self) -> Option<usize> {
self.max_credential_count_in_list
}
fn max_large_blob_array_size(&self) -> usize {
self.max_large_blob_array_size
}
fn max_rp_ids_length(&self) -> usize {
self.max_rp_ids_length
}
fn max_supported_resident_keys(&self) -> usize {
self.max_supported_resident_keys
}
}
impl From<CustomizationImpl> for TestCustomization {
fn from(c: CustomizationImpl) -> Self {
let CustomizationImpl {
aaguid,
allows_pin_protocol_v1,
default_cred_protect,
default_min_pin_length,
default_min_pin_length_rp_ids,
enforce_always_uv,
enterprise_attestation_mode,
enterprise_rp_id_list,
max_msg_size,
max_pin_retries,
use_batch_attestation,
use_signature_counter,
max_cred_blob_length,
max_credential_count_in_list,
max_large_blob_array_size,
max_rp_ids_length,
max_supported_resident_keys,
} = c;
let default_min_pin_length_rp_ids = default_min_pin_length_rp_ids
.iter()
.map(|s| String::from(*s))
.collect::<Vec<_>>();
let enterprise_rp_id_list = enterprise_rp_id_list
.iter()
.map(|s| String::from(*s))
.collect::<Vec<_>>();
Self {
aaguid,
allows_pin_protocol_v1,
default_cred_protect,
default_min_pin_length,
default_min_pin_length_rp_ids,
enforce_always_uv,
enterprise_attestation_mode,
enterprise_rp_id_list,
max_msg_size,
max_pin_retries,
use_batch_attestation,
use_signature_counter,
max_cred_blob_length,
max_credential_count_in_list,
max_large_blob_array_size,
max_rp_ids_length,
max_supported_resident_keys,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::{is_valid, DEFAULT_CUSTOMIZATION};
#[test]
fn test_invariants() {
let customization = TestCustomization::from(DEFAULT_CUSTOMIZATION.clone());
assert!(is_valid(&customization));
}
}

286
src/env/test/mod.rs vendored
View File

@@ -1,286 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use self::upgrade_storage::BufferUpgradeStorage;
use crate::api::attestation_store::AttestationStore;
use crate::api::clock::Clock;
use crate::api::connection::{HidConnection, SendOrRecvResult, SendOrRecvStatus};
use crate::api::customization::DEFAULT_CUSTOMIZATION;
use crate::api::firmware_protection::FirmwareProtection;
use crate::api::user_presence::{UserPresence, UserPresenceResult};
use crate::api::{attestation_store, key_store};
use crate::env::Env;
use customization::TestCustomization;
use persistent_store::{BufferOptions, BufferStorage, Store};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rng256::Rng256;
pub mod customization;
mod upgrade_storage;
pub struct TestEnv {
rng: TestRng256,
user_presence: TestUserPresence,
store: Store<BufferStorage>,
upgrade_storage: Option<BufferUpgradeStorage>,
customization: TestCustomization,
clock: TestClock,
}
pub struct TestRng256 {
rng: StdRng,
}
impl TestRng256 {
pub fn seed_from_u64(&mut self, state: u64) {
self.rng = StdRng::seed_from_u64(state);
}
}
impl Rng256 for TestRng256 {
fn gen_uniform_u8x32(&mut self) -> [u8; 32] {
let mut result = [Default::default(); 32];
self.rng.fill(&mut result);
result
}
}
#[derive(Debug, Default, PartialEq)]
pub struct TestTimer {
end_ms: usize,
}
#[derive(Debug, Default)]
pub struct TestClock {
/// The current time, as advanced, in milliseconds.
now_ms: usize,
}
impl TestClock {
pub fn advance(&mut self, milliseconds: usize) {
self.now_ms += milliseconds;
}
}
impl Clock for TestClock {
type Timer = TestTimer;
fn make_timer(&mut self, milliseconds: usize) -> Self::Timer {
TestTimer {
end_ms: self.now_ms + milliseconds,
}
}
fn is_elapsed(&mut self, timer: &Self::Timer) -> bool {
self.now_ms >= timer.end_ms
}
#[cfg(feature = "debug_ctap")]
fn timestamp_us(&mut self) -> usize {
// Unused, but let's implement something because it's easy.
self.now_ms * 1000
}
}
pub struct TestUserPresence {
check: Box<dyn Fn() -> UserPresenceResult>,
}
pub struct TestWrite;
impl core::fmt::Write for TestWrite {
fn write_str(&mut self, _: &str) -> core::fmt::Result {
Ok(())
}
}
fn new_storage() -> BufferStorage {
// Use the Nordic configuration.
const PAGE_SIZE: usize = 0x1000;
const NUM_PAGES: usize = 20;
let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice();
let options = BufferOptions {
word_size: 4,
page_size: PAGE_SIZE,
max_word_writes: 2,
max_page_erases: 10000,
strict_mode: true,
};
BufferStorage::new(store, options)
}
impl HidConnection for TestEnv {
fn send_and_maybe_recv(&mut self, _buf: &mut [u8; 64], _timeout_ms: usize) -> SendOrRecvResult {
// TODO: Implement I/O from canned requests/responses for integration testing.
Ok(SendOrRecvStatus::Sent)
}
}
impl Default for TestEnv {
fn default() -> Self {
let rng = TestRng256 {
rng: StdRng::seed_from_u64(0),
};
let user_presence = TestUserPresence {
check: Box::new(|| Ok(())),
};
let storage = new_storage();
let store = Store::new(storage).ok().unwrap();
let upgrade_storage = Some(BufferUpgradeStorage::new().unwrap());
let customization = DEFAULT_CUSTOMIZATION.into();
let clock = TestClock::default();
TestEnv {
rng,
user_presence,
store,
upgrade_storage,
customization,
clock,
}
}
}
impl TestEnv {
pub fn disable_upgrade_storage(&mut self) {
self.upgrade_storage = None;
}
pub fn customization_mut(&mut self) -> &mut TestCustomization {
&mut self.customization
}
pub fn rng(&mut self) -> &mut TestRng256 {
&mut self.rng
}
}
impl TestUserPresence {
pub fn set(&mut self, check: impl Fn() -> UserPresenceResult + 'static) {
self.check = Box::new(check);
}
}
impl UserPresence for TestUserPresence {
fn check_init(&mut self) {}
fn wait_with_timeout(&mut self, _timeout_ms: usize) -> UserPresenceResult {
(self.check)()
}
fn check_complete(&mut self) {}
}
impl FirmwareProtection for TestEnv {
fn lock(&mut self) -> bool {
true
}
}
impl key_store::Helper for TestEnv {}
impl AttestationStore for TestEnv {
fn get(
&mut self,
_id: &attestation_store::Id,
) -> Result<Option<attestation_store::Attestation>, attestation_store::Error> {
attestation_store::helper_get(self)
}
fn set(
&mut self,
_id: &attestation_store::Id,
attestation: Option<&attestation_store::Attestation>,
) -> Result<(), attestation_store::Error> {
attestation_store::helper_set(self, attestation)
}
}
impl Env for TestEnv {
type Rng = TestRng256;
type UserPresence = TestUserPresence;
type Storage = BufferStorage;
type KeyStore = Self;
type AttestationStore = Self;
type Clock = TestClock;
type UpgradeStorage = BufferUpgradeStorage;
type FirmwareProtection = Self;
type Write = TestWrite;
type Customization = TestCustomization;
type HidConnection = Self;
fn rng(&mut self) -> &mut Self::Rng {
&mut self.rng
}
fn user_presence(&mut self) -> &mut Self::UserPresence {
&mut self.user_presence
}
fn store(&mut self) -> &mut Store<Self::Storage> {
&mut self.store
}
fn key_store(&mut self) -> &mut Self {
self
}
fn attestation_store(&mut self) -> &mut Self {
self
}
fn clock(&mut self) -> &mut Self::Clock {
&mut self.clock
}
fn upgrade_storage(&mut self) -> Option<&mut Self::UpgradeStorage> {
self.upgrade_storage.as_mut()
}
fn firmware_protection(&mut self) -> &mut Self::FirmwareProtection {
self
}
fn write(&mut self) -> Self::Write {
TestWrite
}
fn customization(&self) -> &Self::Customization {
&self.customization
}
fn main_hid_connection(&mut self) -> &mut Self::HidConnection {
self
}
#[cfg(feature = "vendor_hid")]
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection {
self
}
}
#[cfg(test)]
#[allow(clippy::module_inception)]
mod test {
use super::*;
#[test]
fn test_clock() {
let mut clock = TestClock::default();
let timer = clock.make_timer(3);
assert!(!clock.is_elapsed(&timer));
clock.advance(2);
assert!(!clock.is_elapsed(&timer));
clock.advance(1);
assert!(clock.is_elapsed(&timer));
}
}

View File

@@ -1,117 +0,0 @@
// Copyright 2021-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::upgrade_storage::helper::ModRange;
use crate::api::upgrade_storage::UpgradeStorage;
use alloc::boxed::Box;
use persistent_store::{StorageError, StorageResult};
const PARTITION_LENGTH: usize = 0x41000;
const METADATA_LENGTH: usize = 0x1000;
pub struct BufferUpgradeStorage {
/// Content of the partition storage.
partition: Box<[u8]>,
}
impl BufferUpgradeStorage {
pub fn new() -> StorageResult<BufferUpgradeStorage> {
Ok(BufferUpgradeStorage {
partition: vec![0xff; PARTITION_LENGTH].into_boxed_slice(),
})
}
#[cfg(test)]
fn read_partition(&self, offset: usize, length: usize) -> StorageResult<&[u8]> {
if length == 0 {
return Err(StorageError::OutOfBounds);
}
let partition_range = ModRange::new(0, self.partition.len());
if partition_range.contains_range(&ModRange::new(offset, length)) {
Ok(&self.partition[offset..][..length])
} else {
Err(StorageError::OutOfBounds)
}
}
}
impl UpgradeStorage for BufferUpgradeStorage {
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
if offset == 0 && data.len() != METADATA_LENGTH {
return Err(StorageError::OutOfBounds);
}
if data.is_empty() {
return Err(StorageError::OutOfBounds);
}
let partition_range = ModRange::new(0, self.partition.len());
if partition_range.contains_range(&ModRange::new(offset, data.len())) {
self.partition[offset..][..data.len()].copy_from_slice(&data);
Ok(())
} else {
Err(StorageError::OutOfBounds)
}
}
fn bundle_identifier(&self) -> u32 {
0x60000
}
fn running_firmware_version(&self) -> u64 {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_write_bundle() {
let mut storage = BufferUpgradeStorage::new().unwrap();
assert_eq!(storage.read_partition(0, 2).unwrap(), &[0xFF, 0xFF]);
assert!(storage.write_bundle(1, vec![0x88, 0x88]).is_ok());
assert_eq!(storage.read_partition(0, 2).unwrap(), &[0xFF, 0x88]);
assert_eq!(
storage.write_bundle(PARTITION_LENGTH - 1, vec![0x88, 0x88],),
Err(StorageError::OutOfBounds)
);
assert_eq!(
storage.read_partition(PARTITION_LENGTH - 2, 2).unwrap(),
&[0xFF, 0xFF]
);
assert_eq!(
storage.read_partition(PARTITION_LENGTH - 1, 2),
Err(StorageError::OutOfBounds)
);
assert_eq!(
storage.write_bundle(4, vec![]),
Err(StorageError::OutOfBounds)
);
assert_eq!(
storage.write_bundle(PARTITION_LENGTH + 4, vec![]),
Err(StorageError::OutOfBounds)
);
assert_eq!(storage.read_partition(4, 0), Err(StorageError::OutOfBounds));
assert_eq!(
storage.read_partition(PARTITION_LENGTH + 4, 0),
Err(StorageError::OutOfBounds)
);
}
#[test]
fn partition_slice() {
let storage = BufferUpgradeStorage::new().unwrap();
assert_eq!(storage.bundle_identifier(), 0x60000);
}
}

View File

@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::clock::Clock;
use libtock_drivers::timer::{get_clock_frequency, get_ticks};
use opensk::api::clock::Clock;
/// 56-bits timestamp (valid for 70k+ years)
#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]

29
src/env/tock/mod.rs vendored
View File

@@ -13,15 +13,6 @@
// limitations under the License.
pub use self::storage::{TockStorage, TockUpgradeStorage};
use crate::api::attestation_store::AttestationStore;
use crate::api::connection::{
HidConnection, SendOrRecvError, SendOrRecvResult, SendOrRecvStatus, UsbEndpoint,
};
use crate::api::customization::{CustomizationImpl, AAGUID_LENGTH, DEFAULT_CUSTOMIZATION};
use crate::api::firmware_protection::FirmwareProtection;
use crate::api::user_presence::{UserPresence, UserPresenceError, UserPresenceResult};
use crate::api::{attestation_store, key_store};
use crate::env::Env;
use clock::TockClock;
use core::cell::Cell;
use core::convert::TryFrom;
@@ -32,6 +23,15 @@ use libtock_drivers::console::Console;
use libtock_drivers::result::{FlexUnwrap, TockError};
use libtock_drivers::timer::Duration;
use libtock_drivers::{crp, led, rng, timer, usb_ctap_hid};
use opensk::api::attestation_store::AttestationStore;
use opensk::api::connection::{
HidConnection, SendOrRecvError, SendOrRecvResult, SendOrRecvStatus, UsbEndpoint,
};
use opensk::api::customization::{CustomizationImpl, AAGUID_LENGTH, DEFAULT_CUSTOMIZATION};
use opensk::api::firmware_protection::FirmwareProtection;
use opensk::api::user_presence::{UserPresence, UserPresenceError, UserPresenceResult};
use opensk::api::{attestation_store, key_store};
use opensk::env::Env;
use persistent_store::{StorageResult, Store};
use rng256::Rng256;
@@ -348,3 +348,14 @@ pub fn switch_off_leds() {
led::get(l).flex_unwrap().off().flex_unwrap();
}
}
#[cfg(test)]
mod test {
use super::*;
use opensk::api::customization::is_valid;
#[test]
fn test_invariants() {
assert!(is_valid(&TOCK_CUSTOMIZATION));
}
}

View File

@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::upgrade_storage::helper::{find_slice, is_aligned, ModRange, Partition};
use crate::api::upgrade_storage::UpgradeStorage;
use alloc::borrow::Cow;
use alloc::vec::Vec;
use arrayref::array_ref;
@@ -22,6 +20,8 @@ use core::cell::Cell;
use crypto::sha256::Sha256;
use crypto::{ecdsa, Hash256};
use libtock_core::{callback, syscalls};
use opensk::api::upgrade_storage::helper::{find_slice, is_aligned, ModRange, Partition};
use opensk::api::upgrade_storage::UpgradeStorage;
use persistent_store::{Storage, StorageError, StorageIndex, StorageResult};
const DRIVER_NUMBER: usize = 0x50003;
@@ -470,8 +470,8 @@ fn verify_signature(
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
use crate::env::Env;
use opensk::env::test::TestEnv;
use opensk::env::Env;
#[test]
fn test_check_metadata() {

View File

@@ -15,109 +15,5 @@
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
#[macro_use]
extern crate arrayref;
use crate::ctap::hid::{HidPacket, HidPacketIterator};
use crate::ctap::main_hid::MainHid;
#[cfg(feature = "vendor_hid")]
use crate::ctap::vendor_hid::VendorHid;
use crate::ctap::CtapState;
pub use crate::ctap::Transport;
use crate::env::Env;
// Those macros should eventually be split into trace, debug, info, warn, and error macros when
// adding either the defmt or log feature and crate dependency.
#[cfg(feature = "debug_ctap")]
macro_rules! debug_ctap {
($env: expr, $($rest:tt)*) => {{
use core::fmt::Write;
writeln!($env.write(), $($rest)*).unwrap();
}};
}
#[cfg(not(feature = "debug_ctap"))]
macro_rules! debug_ctap {
($env: expr, $($rest:tt)*) => {
// To avoid unused variable warnings.
let _ = $env;
};
}
pub mod api;
// TODO(kaczmarczyck): Refactor this so that ctap module isn't public.
pub mod ctap;
pub mod env;
#[cfg(feature = "std")]
pub mod test_helpers;
/// CTAP implementation parameterized by its environment.
pub struct Ctap<E: Env> {
env: E,
state: CtapState<E>,
hid: MainHid<E>,
#[cfg(feature = "vendor_hid")]
vendor_hid: VendorHid<E>,
}
impl<E: Env> Ctap<E> {
/// Instantiates a CTAP implementation given its environment.
// This should only take the environment, but it temporarily takes the boot time until the
// clock is part of the environment.
pub fn new(mut env: E) -> Self {
let state = CtapState::<E>::new(&mut env);
let hid = MainHid::default();
#[cfg(feature = "vendor_hid")]
let vendor_hid = VendorHid::default();
Ctap {
env,
state,
hid,
#[cfg(feature = "vendor_hid")]
vendor_hid,
}
}
pub fn state(&mut self) -> &mut CtapState<E> {
&mut self.state
}
pub fn hid(&mut self) -> &mut MainHid<E> {
&mut self.hid
}
pub fn env(&mut self) -> &mut E {
&mut self.env
}
pub fn process_hid_packet(
&mut self,
packet: &HidPacket,
transport: Transport,
) -> HidPacketIterator {
match transport {
Transport::MainHid => {
self.hid
.process_hid_packet(&mut self.env, packet, &mut self.state)
}
#[cfg(feature = "vendor_hid")]
Transport::VendorHid => {
self.vendor_hid
.process_hid_packet(&mut self.env, packet, &mut self.state)
}
}
}
pub fn should_wink(&mut self) -> bool {
self.hid.should_wink(&mut self.env)
}
#[cfg(feature = "with_ctap1")]
pub fn u2f_grant_user_presence(&mut self) {
self.state.u2f_grant_user_presence(&mut self.env)
}
#[cfg(feature = "with_ctap1")]
pub fn u2f_needs_user_presence(&mut self) -> bool {
self.state.u2f_needs_user_presence(&mut self.env)
}
}

View File

@@ -26,15 +26,9 @@ use core::cell::Cell;
use core::convert::TryFrom;
#[cfg(feature = "debug_ctap")]
use core::fmt::Write;
use ctap2::api::clock::Clock;
use ctap2::api::connection::{HidConnection, SendOrRecvStatus, UsbEndpoint};
use ctap2::ctap::hid::HidPacketIterator;
use ctap2::ctap::KEEPALIVE_DELAY_MS;
#[cfg(feature = "with_ctap1")]
use ctap2::env::tock::blink_leds;
use ctap2::env::tock::{switch_off_leds, wink_leds, TockEnv};
use ctap2::env::Env;
use ctap2::Transport;
#[cfg(feature = "with_ctap1")]
use libtock_drivers::buttons::{self, ButtonState};
#[cfg(feature = "debug_ctap")]
@@ -42,6 +36,12 @@ use libtock_drivers::console::Console;
use libtock_drivers::result::FlexUnwrap;
use libtock_drivers::timer::Duration;
use libtock_drivers::usb_ctap_hid;
use opensk::api::clock::Clock;
use opensk::api::connection::{HidConnection, SendOrRecvStatus, UsbEndpoint};
use opensk::ctap::hid::HidPacketIterator;
use opensk::ctap::KEEPALIVE_DELAY_MS;
use opensk::env::Env;
use opensk::Transport;
libtock_core::stack_size! {0x4000}
@@ -115,7 +115,7 @@ fn main() {
}
let env = TockEnv::default();
let mut ctap = ctap2::Ctap::new(env);
let mut ctap = opensk::Ctap::new(env);
let mut led_counter = 0;
let mut led_blink_timer = <<TockEnv as Env>::Clock as Clock>::Timer::default();

View File

@@ -1,61 +0,0 @@
// Copyright 2022-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::ctap::command::{
AuthenticatorAttestationMaterial, AuthenticatorConfigParameters,
AuthenticatorVendorConfigureParameters, Command, ATTESTATION_PRIVATE_KEY_LENGTH,
};
use crate::ctap::data_formats::ConfigSubCommand;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::{Channel, CtapState};
use crate::env::Env;
// In tests where we define a dummy user-presence check that immediately returns, the channel
// ID is irrelevant, so we pass this (dummy but valid) value.
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
#[cfg(feature = "vendor_hid")]
const VENDOR_CHANNEL: Channel = Channel::VendorHid([0x12, 0x34, 0x56, 0x78]);
pub fn enable_enterprise_attestation<E: Env>(
state: &mut CtapState<E>,
env: &mut E,
) -> Result<AuthenticatorAttestationMaterial, Ctap2StatusCode> {
let dummy_key = [0x41; ATTESTATION_PRIVATE_KEY_LENGTH];
let dummy_cert = vec![0xdd; 20];
let attestation_material = AuthenticatorAttestationMaterial {
certificate: dummy_cert,
private_key: dummy_key,
};
let configure_params = AuthenticatorVendorConfigureParameters {
lockdown: false,
attestation_material: Some(attestation_material.clone()),
};
#[cfg(feature = "vendor_hid")]
let vendor_channel = VENDOR_CHANNEL;
#[cfg(not(feature = "vendor_hid"))]
let vendor_channel = DUMMY_CHANNEL;
let vendor_command = Command::AuthenticatorVendorConfigure(configure_params);
state.process_parsed_command(env, vendor_command, vendor_channel)?;
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_command = Command::AuthenticatorConfig(config_params);
state.process_parsed_command(env, config_command, DUMMY_CHANNEL)?;
Ok(attestation_material)
}