Moves vendor commands into TockEnv (#614)

This move changes the Env trait: It removes all functionality that is
used only in vendor commands (`FirmwareProtection`, `UpgradeStorage`)
and adds a function to call when parsing CBOR commands.

The abstraction necessary to test these commands is instead realized
through compile flags. The mock upgrade storage is active when compiled
for std for example.
This commit is contained in:
kaczmarczyck
2023-04-17 00:17:37 +02:00
committed by GitHub
parent a1d6ed0223
commit 3091b5a29d
23 changed files with 1164 additions and 1047 deletions

View File

@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::api::crypto::EC_FIELD_SIZE;
use crate::env::Env;
use alloc::vec::Vec;
use persistent_store::{StoreError, StoreUpdate};
use crate::env::Env;
/// Identifies an attestation.
#[derive(Clone, PartialEq, Eq)]
pub enum Id {
@@ -27,7 +27,7 @@ pub enum Id {
#[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))]
pub struct Attestation {
/// ECDSA private key (big-endian).
pub private_key: [u8; 32],
pub private_key: [u8; EC_FIELD_SIZE],
pub certificate: Vec<u8>,
}
@@ -65,11 +65,11 @@ pub fn helper_get(env: &mut impl Env) -> Result<Option<Attestation>, Error> {
(None, None) => return Ok(None),
_ => return Err(Error::Internal),
};
if private_key.len() != 32 {
if private_key.len() != EC_FIELD_SIZE {
return Err(Error::Internal);
}
Ok(Some(Attestation {
private_key: *array_ref![private_key, 0, 32],
private_key: *array_ref![private_key, 0, EC_FIELD_SIZE],
certificate,
}))
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::{EC_FIELD_SIZE, EC_SIGNATURE_SIZE};
use super::{EC_FIELD_SIZE, EC_SIGNATURE_SIZE, HASH_SIZE};
use crate::api::rng::Rng;
use alloc::vec::Vec;
@@ -58,6 +58,11 @@ pub trait PublicKey: Sized {
/// For hashing, SHA256 is used implicitly.
fn verify(&self, message: &[u8], signature: &Self::Signature) -> bool;
/// Verifies if the signature matches the hash of the message.
///
/// Prehash is the SHA256 of the signed message.
fn verify_prehash(&self, prehash: &[u8; HASH_SIZE], signature: &Self::Signature) -> bool;
/// Writes the public key coordinates into the passed in parameters.
fn to_coordinates(&self, x: &mut [u8; EC_FIELD_SIZE], y: &mut [u8; EC_FIELD_SIZE]);
}
@@ -67,6 +72,10 @@ pub trait Signature: Sized {
/// Creates a signature from its affine coordinates, represented as concatenated bytes.
fn from_slice(bytes: &[u8; EC_SIGNATURE_SIZE]) -> Option<Self>;
/// Writes the signature bytes into the passed in parameter.
#[cfg(feature = "std")]
fn to_slice(&self, bytes: &mut [u8; EC_SIGNATURE_SIZE]);
/// Encodes the signatures as ASN1 DER.
fn to_der(&self) -> Vec<u8>;
}

View File

@@ -70,7 +70,7 @@ mod test {
use super::software_crypto::*;
use super::*;
use crate::api::crypto::ecdh::{PublicKey as _, SecretKey as _, SharedSecret};
use crate::api::crypto::ecdsa::{PublicKey as _, SecretKey as _};
use crate::api::crypto::ecdsa::{PublicKey as _, SecretKey as _, Signature};
use crate::env::test::TestEnv;
use crate::env::Env;
use core::convert::TryFrom;
@@ -114,7 +114,18 @@ mod test {
}
#[test]
fn test_ecdsa_secret_key_from_to_bytes() {
fn test_sign_verify_hash() {
let mut env = TestEnv::default();
let private_key = SoftwareEcdsaSecretKey::random(env.rng());
let public_key = private_key.public_key();
let message = [0x12, 0x34, 0x56, 0x78];
let signature = private_key.sign(&message);
let message_hash = SoftwareSha256::digest(&message);
assert!(public_key.verify_prehash(&message_hash, &signature));
}
#[test]
fn test_ecdsa_secret_key_from_to_slice() {
let mut env = TestEnv::default();
let first_key = SoftwareEcdsaSecretKey::random(env.rng());
let mut key_bytes = [0; EC_FIELD_SIZE];
@@ -125,6 +136,20 @@ mod test {
assert_eq!(key_bytes, new_bytes);
}
#[test]
fn test_ecdsa_signature_from_to_slice() {
let mut env = TestEnv::default();
let private_key = SoftwareEcdsaSecretKey::random(env.rng());
let message = [0x12, 0x34, 0x56, 0x78];
let signature = private_key.sign(&message);
let mut signature_bytes = [0; EC_SIGNATURE_SIZE];
signature.to_slice(&mut signature_bytes);
let new_signature = SoftwareEcdsaSignature::from_slice(&signature_bytes).unwrap();
let mut new_bytes = [0; EC_SIGNATURE_SIZE];
new_signature.to_slice(&mut new_bytes);
assert_eq!(signature_bytes, new_bytes);
}
#[test]
fn test_sha256_hash_matches() {
let data = [0x55; 16];

View File

@@ -36,6 +36,7 @@ use aes::cipher::{
use core::convert::TryFrom;
use hmac::Mac;
use p256::ecdh::EphemeralSecret;
use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p256::ecdsa::signature::{SignatureEncoding, Signer, Verifier};
use p256::ecdsa::{SigningKey, VerifyingKey};
use p256::elliptic_curve::sec1::ToEncodedPoint;
@@ -174,6 +175,12 @@ impl ecdsa::PublicKey for SoftwareEcdsaPublicKey {
.is_ok()
}
fn verify_prehash(&self, prehash: &[u8; HASH_SIZE], signature: &Self::Signature) -> bool {
self.verifying_key
.verify_prehash(prehash, &signature.signature)
.is_ok()
}
fn to_coordinates(&self, x: &mut [u8; EC_FIELD_SIZE], y: &mut [u8; EC_FIELD_SIZE]) {
let point = self.verifying_key.to_encoded_point(false);
x.copy_from_slice(point.x().unwrap());
@@ -198,6 +205,11 @@ impl ecdsa::Signature for SoftwareEcdsaSignature {
Some(SoftwareEcdsaSignature { signature })
}
#[cfg(feature = "std")]
fn to_slice(&self, bytes: &mut [u8; EC_SIGNATURE_SIZE]) {
bytes.copy_from_slice(&self.signature.to_bytes());
}
fn to_der(&self) -> Vec<u8> {
self.signature.to_der().to_vec()
}

View File

@@ -145,6 +145,11 @@ impl ecdsa::PublicKey for SoftwareEcdsaPublicKey {
.verify_vartime::<crypto::sha256::Sha256>(message, &signature.signature)
}
fn verify_prehash(&self, prehash: &[u8; HASH_SIZE], signature: &Self::Signature) -> bool {
self.pub_key
.verify_hash_vartime(prehash, &signature.signature)
}
fn to_coordinates(&self, x: &mut [u8; EC_FIELD_SIZE], y: &mut [u8; EC_FIELD_SIZE]) {
self.pub_key.to_coordinates(x, y);
}
@@ -159,6 +164,11 @@ impl ecdsa::Signature for SoftwareEcdsaSignature {
crypto::ecdsa::Signature::from_bytes(bytes).map(|s| SoftwareEcdsaSignature { signature: s })
}
#[cfg(feature = "std")]
fn to_slice(&self, bytes: &mut [u8; EC_SIGNATURE_SIZE]) {
self.signature.to_bytes(bytes);
}
fn to_der(&self) -> Vec<u8> {
self.signature.to_asn1_der()
}

View File

@@ -25,5 +25,4 @@ pub mod customization;
pub mod firmware_protection;
pub mod key_store;
pub mod rng;
pub mod upgrade_storage;
pub mod user_presence;

View File

@@ -1,381 +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)
}
}
#[derive(Default)]
pub struct Partition {
ranges: Vec<ModRange>,
}
impl Partition {
/// 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::default();
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::default();
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::default();
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 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

@@ -14,9 +14,9 @@
use super::cbor_read;
use super::data_formats::{
extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string,
extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams,
CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned,
ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, CoseKey,
CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions,
PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams,
@@ -26,7 +26,6 @@ use alloc::string::String;
use alloc::vec::Vec;
#[cfg(feature = "fuzz")]
use arbitrary::Arbitrary;
use arrayref::array_ref;
use core::convert::TryFrom;
use sk_cbor as cbor;
use sk_cbor::destructure_cbor_map;
@@ -34,9 +33,6 @@ use sk_cbor::destructure_cbor_map;
// This constant is a consequence of the structure of messages.
const MIN_LARGE_BLOB_LEN: usize = 17;
/// Expected length of the passed in attestation key bytes during configuration.
pub const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32;
// CTAP specification (version 20190130) section 6.1
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
@@ -51,10 +47,6 @@ pub enum Command {
AuthenticatorSelection,
AuthenticatorLargeBlobs(AuthenticatorLargeBlobsParameters),
AuthenticatorConfig(AuthenticatorConfigParameters),
// Vendor specific commands
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters),
AuthenticatorVendorUpgrade(AuthenticatorVendorUpgradeParameters),
AuthenticatorVendorUpgradeInfo,
}
impl Command {
@@ -71,13 +63,10 @@ impl Command {
const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C;
const AUTHENTICATOR_CONFIG: u8 = 0x0D;
const _AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40;
const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40;
// This commands is the same as AUTHENTICATOR_CREDENTIAL_MANAGEMENT but is duplicated as a
// vendor command for legacy and compatibility reasons. See
// https://github.com/Yubico/libfido2/issues/628 for more information.
const AUTHENTICATOR_VENDOR_CREDENTIAL_MANAGEMENT: u8 = 0x41;
const AUTHENTICATOR_VENDOR_UPGRADE: u8 = 0x42;
const AUTHENTICATOR_VENDOR_UPGRADE_INFO: u8 = 0x43;
const _AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF;
pub fn deserialize(bytes: &[u8]) -> Result<Command, Ctap2StatusCode> {
@@ -141,22 +130,6 @@ impl Command {
AuthenticatorConfigParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_VENDOR_CONFIGURE => {
let decoded_cbor = cbor_read(&bytes[1..])?;
Ok(Command::AuthenticatorVendorConfigure(
AuthenticatorVendorConfigureParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_VENDOR_UPGRADE => {
let decoded_cbor = cbor_read(&bytes[1..])?;
Ok(Command::AuthenticatorVendorUpgrade(
AuthenticatorVendorUpgradeParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_VENDOR_UPGRADE_INFO => {
// Parameters are ignored.
Ok(Command::AuthenticatorVendorUpgradeInfo)
}
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND),
}
}
@@ -498,35 +471,6 @@ impl TryFrom<cbor::Value> for AuthenticatorConfigParameters {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthenticatorAttestationMaterial {
pub certificate: Vec<u8>,
pub private_key: [u8; ATTESTATION_PRIVATE_KEY_LENGTH],
}
impl TryFrom<cbor::Value> for AuthenticatorAttestationMaterial {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => certificate,
0x02 => private_key,
} = extract_map(cbor_value)?;
}
let certificate = extract_byte_string(ok_or_missing(certificate)?)?;
let private_key = extract_byte_string(ok_or_missing(private_key)?)?;
if private_key.len() != ATTESTATION_PRIVATE_KEY_LENGTH {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let private_key = array_ref!(private_key, 0, ATTESTATION_PRIVATE_KEY_LENGTH);
Ok(AuthenticatorAttestationMaterial {
certificate,
private_key: *private_key,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorCredentialManagementParameters {
pub sub_command: CredentialManagementSubCommand,
@@ -566,59 +510,6 @@ impl TryFrom<cbor::Value> for AuthenticatorCredentialManagementParameters {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorVendorConfigureParameters {
pub lockdown: bool,
pub attestation_material: Option<AuthenticatorAttestationMaterial>,
}
impl TryFrom<cbor::Value> for AuthenticatorVendorConfigureParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => lockdown,
0x02 => attestation_material,
} = extract_map(cbor_value)?;
}
let lockdown = lockdown.map_or(Ok(false), extract_bool)?;
let attestation_material = attestation_material
.map(AuthenticatorAttestationMaterial::try_from)
.transpose()?;
Ok(AuthenticatorVendorConfigureParameters {
lockdown,
attestation_material,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorVendorUpgradeParameters {
pub offset: usize,
pub data: Vec<u8>,
pub hash: [u8; 32],
}
impl TryFrom<cbor::Value> for AuthenticatorVendorUpgradeParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => offset,
0x02 => data,
0x03 => hash,
} = extract_map(cbor_value)?;
}
let offset = extract_unsigned(ok_or_missing(offset)?)? as usize;
let data = extract_byte_string(ok_or_missing(data)?)?;
let hash = <[u8; 32]>::try_from(extract_byte_string(ok_or_missing(hash)?)?)
.map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
Ok(AuthenticatorVendorUpgradeParameters { offset, data, hash })
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::{
@@ -1002,154 +893,4 @@ mod test {
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
#[test]
fn test_vendor_configure() {
// Incomplete command
let mut cbor_bytes = vec![Command::AUTHENTICATOR_VENDOR_CONFIGURE];
let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR));
cbor_bytes.extend(&[0xA1, 0x01, 0xF5]);
let command = Command::deserialize(&cbor_bytes);
assert_eq!(
command,
Ok(Command::AuthenticatorVendorConfigure(
AuthenticatorVendorConfigureParameters {
lockdown: true,
attestation_material: None
}
))
);
let dummy_cert = [0xddu8; 20];
let dummy_pkey = [0x41u8; ATTESTATION_PRIVATE_KEY_LENGTH];
// Attestation key is too short.
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x01 => dummy_cert,
0x02 => dummy_pkey[..ATTESTATION_PRIVATE_KEY_LENGTH - 1]
}
};
assert_eq!(
AuthenticatorVendorConfigureParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// Missing private key
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x01 => dummy_cert
}
};
assert_eq!(
AuthenticatorVendorConfigureParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Missing certificate
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x02 => dummy_pkey
}
};
assert_eq!(
AuthenticatorVendorConfigureParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Valid
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x01 => dummy_cert,
0x02 => dummy_pkey
},
};
assert_eq!(
AuthenticatorVendorConfigureParameters::try_from(cbor_value),
Ok(AuthenticatorVendorConfigureParameters {
lockdown: false,
attestation_material: Some(AuthenticatorAttestationMaterial {
certificate: dummy_cert.to_vec(),
private_key: dummy_pkey
}),
})
);
}
#[test]
fn test_vendor_upgrade() {
// Incomplete command
let cbor_bytes = vec![Command::AUTHENTICATOR_VENDOR_UPGRADE];
let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR));
// Missing offset
let cbor_value = cbor_map! {
0x02 => [0xFF; 0x100],
0x03 => [0x44; 32],
};
assert_eq!(
AuthenticatorVendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Missing data
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x03 => [0x44; 32],
};
assert_eq!(
AuthenticatorVendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Invalid hash size
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x02 => [0xFF; 0x100],
0x03 => [0x44; 33],
};
assert_eq!(
AuthenticatorVendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// Missing hash
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x02 => [0xFF; 0x100],
};
assert_eq!(
AuthenticatorVendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Valid
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x02 => [0xFF; 0x100],
0x03 => [0x44; 32],
};
assert_eq!(
AuthenticatorVendorUpgradeParameters::try_from(cbor_value),
Ok(AuthenticatorVendorUpgradeParameters {
offset: 0x1000,
data: vec![0xFF; 0x100],
hash: [0x44; 32],
})
);
}
#[test]
fn test_deserialize_vendor_upgrade_info() {
let cbor_bytes = [Command::AUTHENTICATOR_VENDOR_UPGRADE_INFO];
let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Ok(Command::AuthenticatorVendorUpgradeInfo));
}
}

View File

@@ -1163,14 +1163,14 @@ impl From<CredentialManagementSubCommandParameters> for cbor::Value {
}
}
pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result<u64, Ctap2StatusCode> {
pub fn extract_unsigned(cbor_value: cbor::Value) -> Result<u64, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Unsigned(unsigned) => Ok(unsigned),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub(super) fn extract_integer(cbor_value: cbor::Value) -> Result<i64, Ctap2StatusCode> {
pub fn extract_integer(cbor_value: cbor::Value) -> Result<i64, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Unsigned(unsigned) => {
if unsigned <= core::i64::MAX as u64 {
@@ -1191,21 +1191,21 @@ pub fn extract_byte_string(cbor_value: cbor::Value) -> Result<Vec<u8>, Ctap2Stat
}
}
pub(super) fn extract_text_string(cbor_value: cbor::Value) -> Result<String, Ctap2StatusCode> {
pub fn extract_text_string(cbor_value: cbor::Value) -> Result<String, Ctap2StatusCode> {
match cbor_value {
cbor::Value::TextString(text_string) => Ok(text_string),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub(super) fn extract_array(cbor_value: cbor::Value) -> Result<Vec<cbor::Value>, Ctap2StatusCode> {
pub fn extract_array(cbor_value: cbor::Value) -> Result<Vec<cbor::Value>, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Array(array) => Ok(array),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub(super) fn extract_map(
pub fn extract_map(
cbor_value: cbor::Value,
) -> Result<Vec<(cbor::Value, cbor::Value)>, Ctap2StatusCode> {
match cbor_value {
@@ -1214,7 +1214,7 @@ pub(super) fn extract_map(
}
}
pub(super) fn extract_bool(cbor_value: cbor::Value) -> Result<bool, Ctap2StatusCode> {
pub fn extract_bool(cbor_value: cbor::Value) -> Result<bool, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Simple(cbor::SimpleValue::FalseValue) => Ok(false),
cbor::Value::Simple(cbor::SimpleValue::TrueValue) => Ok(true),
@@ -1222,7 +1222,7 @@ pub(super) fn extract_bool(cbor_value: cbor::Value) -> Result<bool, Ctap2StatusC
}
}
pub(super) fn ok_or_missing<T>(value_option: Option<T>) -> Result<T, Ctap2StatusCode> {
pub fn ok_or_missing<T>(value_option: Option<T>) -> Result<T, Ctap2StatusCode> {
value_option.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
}

View File

@@ -37,8 +37,7 @@ pub mod vendor_hid;
use self::client_pin::{ClientPin, PinPermission};
use self::command::{
AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters,
AuthenticatorVendorConfigureParameters, AuthenticatorVendorUpgradeParameters, Command,
AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, Command,
};
use self::config_command::process_config;
use self::credential_id::{
@@ -56,8 +55,7 @@ use self::hid::{ChannelID, CtapHid, CtapHidCommand, KeepaliveStatus, ProcessedPa
use self::large_blobs::LargeBlobs;
use self::response::{
AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse,
AuthenticatorMakeCredentialResponse, AuthenticatorVendorConfigureResponse,
AuthenticatorVendorUpgradeInfoResponse, ResponseData,
AuthenticatorMakeCredentialResponse, ResponseData,
};
use self::status_code::Ctap2StatusCode;
#[cfg(feature = "with_ctap1")]
@@ -69,9 +67,7 @@ use crate::api::crypto::ecdsa::{SecretKey as _, Signature};
use crate::api::crypto::hkdf256::Hkdf256;
use crate::api::crypto::sha256::Sha256;
use crate::api::customization::Customization;
use crate::api::firmware_protection::FirmwareProtection;
use crate::api::rng::Rng;
use crate::api::upgrade_storage::UpgradeStorage;
use crate::api::user_presence::{UserPresence, UserPresenceError};
use crate::env::{EcdsaSk, Env, Hkdf, Sha};
use alloc::boxed::Box;
@@ -183,7 +179,7 @@ pub fn cbor_read(encoded_cbor: &[u8]) -> Result<cbor::Value, Ctap2StatusCode> {
.map_err(|_e| Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)
}
fn cbor_write(value: cbor::Value, encoded_cbor: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
pub fn cbor_write(value: cbor::Value, encoded_cbor: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
cbor::writer::write_nested(value, encoded_cbor, Some(MAX_CBOR_NESTING_DEPTH))
.map_err(|_e| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
}
@@ -283,7 +279,7 @@ fn send_keepalive_up_needed<E: Env>(
/// Blocks for user presence.
///
/// Returns an error in case of timeout, user declining presence request, or keepalive error.
fn check_user_presence<E: Env>(env: &mut E, channel: Channel) -> Result<(), Ctap2StatusCode> {
pub fn check_user_presence<E: Env>(env: &mut E, channel: Channel) -> Result<(), Ctap2StatusCode> {
env.user_presence().check_init();
// The timeout is N times the keepalive delay.
@@ -385,7 +381,7 @@ impl<E: Env> StatefulPermission<E> {
}
/// Clears all state if communication is coming from a different channel.
pub fn clear_old_channels(&mut self, channel: &Channel) {
pub fn clear_old_channels(&mut self, channel: Channel) {
// There are different possible choices for incoming traffic on a different channel:
// A) Always reset state (our choice).
// B) Only reset state if the new command is stateful.
@@ -396,7 +392,7 @@ impl<E: Env> StatefulPermission<E> {
// However, interleaving (stateless) commands could delete credentials or change the PIN,
// which could invalidate our access. Some read-only commands should be okay to run,
// but (A) is the safest and easiest solution.
if let Some(c) = &self.channel {
if let Some(c) = self.channel {
if c != channel {
self.clear();
}
@@ -525,12 +521,28 @@ impl<E: Env> CtapState<E> {
Ok(!storage::has_always_uv(env)?)
}
fn clear_other_channels(&mut self, channel: Channel) {
// Correct behavior between CTAP1 and CTAP2 isn't defined yet. Just a guess.
#[cfg(feature = "with_ctap1")]
{
// We create a block statement to wrap this assignment expression, because attributes
// (like #[cfg]) are not supported on expressions.
self.u2f_up_state = U2fUserPresenceState::new();
}
self.stateful_command_permission.clear_old_channels(channel);
}
pub fn process_command(
&mut self,
env: &mut E,
command_cbor: &[u8],
channel: Channel,
) -> Vec<u8> {
if let Some(response) = env.process_vendor_command(command_cbor, channel) {
self.clear_other_channels(channel);
self.stateful_command_permission.clear();
return response;
}
let cmd = Command::deserialize(command_cbor);
debug_ctap!(env, "Received command: {:#?}", cmd);
let response = cmd.and_then(|command| self.process_parsed_command(env, command, channel));
@@ -562,15 +574,7 @@ impl<E: Env> CtapState<E> {
// The auth token timeouts are checked once here, to make error codes consistent. If your
// auth token hasn't timed out now, you can fully use it for this command.
self.client_pin.update_timeouts(env);
// Correct behavior between CTAP1 and CTAP2 isn't defined yet. Just a guess.
#[cfg(feature = "with_ctap1")]
{
// We create a block statement to wrap this assignment expression, because attributes
// (like #[cfg]) are not supported on expressions.
self.u2f_up_state = U2fUserPresenceState::new();
}
self.stateful_command_permission
.clear_old_channels(&channel);
self.clear_other_channels(channel);
match (&command, self.stateful_command_permission.get_command(env)) {
(Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_)))
| (Command::AuthenticatorReset, Ok(StatefulCommand::Reset))
@@ -591,10 +595,10 @@ impl<E: Env> CtapState<E> {
) => (),
(_, _) => self.stateful_command_permission.clear(),
}
match &channel {
match channel {
Channel::MainHid(_) => self.process_fido_command(env, command, channel),
#[cfg(feature = "vendor_hid")]
Channel::VendorHid(_) => self.process_vendor_command(env, command, channel),
Channel::VendorHid(_) => self.process_vendor_command(env, command),
}
}
@@ -630,25 +634,16 @@ impl<E: Env> CtapState<E> {
Command::AuthenticatorConfig(params) => {
process_config(env, &mut self.client_pin, params)
}
#[cfg(feature = "vendor_hid")]
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND),
#[cfg(not(feature = "vendor_hid"))]
_ => self.process_vendor_command(env, command, channel),
}
}
#[cfg(feature = "vendor_hid")]
fn process_vendor_command(
&mut self,
env: &mut E,
command: Command,
channel: Channel,
) -> Result<ResponseData, Ctap2StatusCode> {
match command {
Command::AuthenticatorVendorConfigure(params) => {
self.process_vendor_configure(env, params, channel)
}
Command::AuthenticatorVendorUpgrade(params) => self.process_vendor_upgrade(env, params),
Command::AuthenticatorVendorUpgradeInfo => self.process_vendor_upgrade_info(env),
Command::AuthenticatorGetInfo => self.process_get_info(env),
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND),
}
@@ -1277,7 +1272,7 @@ impl<E: Env> CtapState<E> {
),
force_pin_change: Some(storage::has_force_pin_change(env)?),
min_pin_length: storage::min_pin_length(env)?,
firmware_version: env.upgrade_storage().map(|u| u.running_firmware_version()),
firmware_version: env.firmware_version(),
max_cred_blob_length: Some(env.customization().max_cred_blob_length() as u64),
max_rp_ids_for_set_min_pin_length: Some(
env.customization().max_rp_ids_length() as u64
@@ -1323,88 +1318,6 @@ impl<E: Env> CtapState<E> {
Ok(ResponseData::AuthenticatorSelection)
}
fn process_vendor_configure(
&mut self,
env: &mut E,
params: AuthenticatorVendorConfigureParameters,
channel: Channel,
) -> Result<ResponseData, Ctap2StatusCode> {
if params.attestation_material.is_some() || params.lockdown {
check_user_presence(env, channel)?;
}
// This command is for U2F support and we use the batch attestation there.
let attestation_id = attestation_store::Id::Batch;
// Sanity checks
let current_attestation = env.attestation_store().get(&attestation_id)?;
let response = match params.attestation_material {
None => AuthenticatorVendorConfigureResponse {
cert_programmed: current_attestation.is_some(),
pkey_programmed: current_attestation.is_some(),
},
Some(data) => {
// We don't overwrite the attestation if it's already set. We don't return any error
// to not leak information.
if current_attestation.is_none() {
let attestation = Attestation {
private_key: data.private_key,
certificate: data.certificate,
};
env.attestation_store()
.set(&attestation_id, Some(&attestation))?;
}
AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
}
};
if params.lockdown {
// To avoid bricking the authenticator, we only allow lockdown
// to happen if both values are programmed or if both U2F/CTAP1 and
// batch attestation are disabled.
#[cfg(feature = "with_ctap1")]
let need_certificate = true;
#[cfg(not(feature = "with_ctap1"))]
let need_certificate = env.customization().use_batch_attestation();
if (need_certificate && !(response.pkey_programmed && response.cert_programmed))
|| !env.firmware_protection().lock()
{
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
Ok(ResponseData::AuthenticatorVendorConfigure(response))
}
fn process_vendor_upgrade(
&mut self,
env: &mut E,
params: AuthenticatorVendorUpgradeParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorVendorUpgradeParameters { offset, data, hash } = params;
let calculated_hash = Sha::<E>::digest(&data);
if hash != calculated_hash {
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
}
env.upgrade_storage()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?
.write_bundle(offset, data)
.map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
Ok(ResponseData::AuthenticatorVendorUpgrade)
}
fn process_vendor_upgrade_info(&self, env: &mut E) -> Result<ResponseData, Ctap2StatusCode> {
let upgrade_locations = env
.upgrade_storage()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?;
Ok(ResponseData::AuthenticatorVendorUpgradeInfo(
AuthenticatorVendorUpgradeInfoResponse {
info: upgrade_locations.bundle_identifier(),
},
))
}
pub fn generate_auth_data(
&self,
env: &mut E,
@@ -1440,8 +1353,7 @@ impl<E: Env> CtapState<E> {
mod test {
use super::client_pin::PIN_TOKEN_LENGTH;
use super::command::{
AuthenticatorAttestationMaterial, AuthenticatorClientPinParameters,
AuthenticatorCredentialManagementParameters, ATTESTATION_PRIVATE_KEY_LENGTH,
AuthenticatorClientPinParameters, AuthenticatorCredentialManagementParameters,
};
use super::credential_id::CBOR_CREDENTIAL_ID_SIZE;
use super::data_formats::{
@@ -3218,228 +3130,6 @@ mod test {
}
}
#[test]
fn test_vendor_configure() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
// Nothing should be configured at the beginning
let response = ctap_state.process_vendor_configure(
&mut env,
AuthenticatorVendorConfigureParameters {
lockdown: false,
attestation_material: None,
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(ResponseData::AuthenticatorVendorConfigure(
AuthenticatorVendorConfigureResponse {
cert_programmed: false,
pkey_programmed: false,
}
))
);
// Inject dummy values
let dummy_key = [0x41u8; ATTESTATION_PRIVATE_KEY_LENGTH];
let dummy_cert = [0xddu8; 20];
let response = ctap_state.process_vendor_configure(
&mut env,
AuthenticatorVendorConfigureParameters {
lockdown: false,
attestation_material: Some(AuthenticatorAttestationMaterial {
certificate: dummy_cert.to_vec(),
private_key: dummy_key,
}),
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(ResponseData::AuthenticatorVendorConfigure(
AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
))
);
assert_eq!(
env.attestation_store().get(&attestation_store::Id::Batch),
Ok(Some(Attestation {
private_key: dummy_key,
certificate: dummy_cert.to_vec(),
}))
);
// Try to inject other dummy values and check that initial values are retained.
let other_dummy_key = [0x44u8; ATTESTATION_PRIVATE_KEY_LENGTH];
let response = ctap_state.process_vendor_configure(
&mut env,
AuthenticatorVendorConfigureParameters {
lockdown: false,
attestation_material: Some(AuthenticatorAttestationMaterial {
certificate: dummy_cert.to_vec(),
private_key: other_dummy_key,
}),
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(ResponseData::AuthenticatorVendorConfigure(
AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
))
);
assert_eq!(
env.attestation_store().get(&attestation_store::Id::Batch),
Ok(Some(Attestation {
private_key: dummy_key,
certificate: dummy_cert.to_vec(),
}))
);
// Now try to lock the device
let response = ctap_state.process_vendor_configure(
&mut env,
AuthenticatorVendorConfigureParameters {
lockdown: true,
attestation_material: None,
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(ResponseData::AuthenticatorVendorConfigure(
AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
))
);
}
#[test]
fn test_vendor_upgrade() {
// The test partition storage has size 0x40000.
// The test metadata storage has size 0x1000.
// The test identifier matches partition B.
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
const METADATA_LEN: usize = 0x1000;
let metadata = vec![0xFF; METADATA_LEN];
let metadata_hash = Sha::<TestEnv>::digest(&metadata);
let data = vec![0xFF; 0x1000];
let hash = Sha::<TestEnv>::digest(&data);
// Write to partition.
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: 0x20000,
data: data.clone(),
hash,
},
);
assert_eq!(response, Ok(ResponseData::AuthenticatorVendorUpgrade));
// TestEnv doesn't check the metadata, test its parser in your Env.
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: 0,
data: metadata.clone(),
hash: metadata_hash,
},
);
assert_eq!(response, Ok(ResponseData::AuthenticatorVendorUpgrade));
// TestEnv doesn't check the metadata, test its parser in your Env.
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: METADATA_LEN,
data: data.clone(),
hash,
},
);
assert_eq!(response, Ok(ResponseData::AuthenticatorVendorUpgrade));
// Write metadata of a wrong size.
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: 0,
data: metadata[..METADATA_LEN - 1].to_vec(),
hash: metadata_hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE));
// Write outside of the partition.
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: 0x41000,
data: data.clone(),
hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER));
// Write a bad hash.
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: 0x20000,
data,
hash: [0xEE; 32],
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE));
}
#[test]
fn test_vendor_upgrade_no_second_partition() {
let mut env = TestEnv::default();
env.disable_upgrade_storage();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let data = vec![0xFF; 0x1000];
let hash = Sha::<TestEnv>::digest(&data);
let response = ctap_state.process_vendor_upgrade(
&mut env,
AuthenticatorVendorUpgradeParameters {
offset: 0,
data,
hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND));
}
#[test]
fn test_vendor_upgrade_info() {
let mut env = TestEnv::default();
let ctap_state = CtapState::<TestEnv>::new(&mut env);
let bundle_identifier = env.upgrade_storage().unwrap().bundle_identifier();
let upgrade_info_reponse = ctap_state.process_vendor_upgrade_info(&mut env);
assert_eq!(
upgrade_info_reponse,
Ok(ResponseData::AuthenticatorVendorUpgradeInfo(
AuthenticatorVendorUpgradeInfoResponse {
info: bundle_identifier,
}
))
);
}
#[test]
fn test_permission_timeout() {
let mut env = TestEnv::default();
@@ -3734,18 +3424,12 @@ mod test {
let response = ctap_state.process_parsed_command(
&mut env,
Command::AuthenticatorVendorUpgradeInfo,
Command::AuthenticatorGetInfo,
VENDOR_CHANNEL,
);
assert!(matches!(
response,
Ok(ResponseData::AuthenticatorVendorUpgradeInfo(_))
Ok(ResponseData::AuthenticatorGetInfo(_))
));
let response = ctap_state.process_parsed_command(
&mut env,
Command::AuthenticatorVendorUpgradeInfo,
DUMMY_CHANNEL,
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND));
}
}

View File

@@ -37,9 +37,6 @@ pub enum ResponseData {
AuthenticatorSelection,
AuthenticatorLargeBlobs(Option<AuthenticatorLargeBlobsResponse>),
AuthenticatorConfig,
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse),
AuthenticatorVendorUpgrade,
AuthenticatorVendorUpgradeInfo(AuthenticatorVendorUpgradeInfoResponse),
}
impl From<ResponseData> for Option<cbor::Value> {
@@ -55,9 +52,6 @@ impl From<ResponseData> for Option<cbor::Value> {
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()),
}
}
}
@@ -303,41 +297,6 @@ impl From<AuthenticatorCredentialManagementResponse> for cbor::Value {
}
}
#[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};
@@ -631,52 +590,4 @@ mod test {
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

@@ -620,7 +620,6 @@ fn serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2
mod test {
use super::*;
use crate::api::attestation_store::{self, Attestation, AttestationStore};
use crate::ctap::command::ATTESTATION_PRIVATE_KEY_LENGTH;
use crate::ctap::crypto_wrapper::PrivateKey;
use crate::ctap::data_formats::{
CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType,
@@ -937,7 +936,7 @@ mod test {
// Make sure the persistent keys are initialized to dummy values.
let dummy_attestation = Attestation {
private_key: [0x41; ATTESTATION_PRIVATE_KEY_LENGTH],
private_key: [0x41; 32],
certificate: vec![0xdd; 20],
};
env.attestation_store()
@@ -1084,7 +1083,7 @@ mod test {
let mut env = TestEnv::default();
let dummy_attestation = Attestation {
private_key: [0x41; ATTESTATION_PRIVATE_KEY_LENGTH],
private_key: [0x41; 32],
certificate: vec![0xdd; 20],
};
env.attestation_store()

View File

@@ -19,11 +19,11 @@ use crate::api::crypto::ecdh::Ecdh;
use crate::api::crypto::ecdsa::Ecdsa;
use crate::api::crypto::Crypto;
use crate::api::customization::Customization;
use crate::api::firmware_protection::FirmwareProtection;
use crate::api::key_store::KeyStore;
use crate::api::rng::Rng;
use crate::api::upgrade_storage::UpgradeStorage;
use crate::api::user_presence::UserPresence;
use crate::ctap::Channel;
use alloc::vec::Vec;
use persistent_store::{Storage, Store};
#[cfg(feature = "std")]
@@ -34,6 +34,7 @@ pub type EcdhSk<E> = <<<E as Env>::Crypto as Crypto>::Ecdh as Ecdh>::SecretKey;
pub type EcdhPk<E> = <<<E as Env>::Crypto as Crypto>::Ecdh as Ecdh>::PublicKey;
pub type EcdsaSk<E> = <<<E as Env>::Crypto as Crypto>::Ecdsa as Ecdsa>::SecretKey;
pub type EcdsaPk<E> = <<<E as Env>::Crypto as Crypto>::Ecdsa as Ecdsa>::PublicKey;
pub type EcdsaSignature<E> = <<<E as Env>::Crypto as Crypto>::Ecdsa as Ecdsa>::Signature;
pub type Sha<E> = <<E as Env>::Crypto as Crypto>::Sha256;
pub type Hmac<E> = <<E as Env>::Crypto as Crypto>::Hmac256;
pub type Hkdf<E> = <<E as Env>::Crypto as Crypto>::Hkdf256;
@@ -44,8 +45,6 @@ pub trait Env {
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;
@@ -60,14 +59,6 @@ pub trait Env {
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
@@ -84,4 +75,22 @@ pub trait Env {
/// 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;
/// Option to return a firmware version that is shown as device info.
fn firmware_version(&self) -> Option<u64> {
None
}
/// Option to process a CBOR command before standard parsing.
///
/// Responses are sent on the same channel they were received. Return `None` to continue
/// regular parsing. Parsing vendor commands may still have side effects, like dropping state
/// from other commands.
///
/// For standard commands, the format for bytes is one byte of CTAP2 command and a CBOR encoded
/// map. To be able to reliably detect your payload, consider starting your messages with a
/// vendor reserved command byte.
fn process_vendor_command(&mut self, _bytes: &[u8], _channel: Channel) -> Option<Vec<u8>> {
None
}
}

View File

@@ -12,13 +12,11 @@
// 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::crypto::software_crypto::SoftwareCrypto;
use crate::api::customization::DEFAULT_CUSTOMIZATION;
use crate::api::firmware_protection::FirmwareProtection;
use crate::api::rng::Rng;
use crate::api::user_presence::{UserPresence, UserPresenceResult};
use crate::api::{attestation_store, key_store};
@@ -29,13 +27,11 @@ use rand::rngs::StdRng;
use rand::SeedableRng;
pub mod customization;
mod upgrade_storage;
pub struct TestEnv {
rng: TestRng,
user_presence: TestUserPresence,
store: Store<BufferStorage>,
upgrade_storage: Option<BufferUpgradeStorage>,
customization: TestCustomization,
clock: TestClock,
}
@@ -123,14 +119,12 @@ impl Default for TestEnv {
};
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,
}
@@ -138,10 +132,6 @@ impl Default for TestEnv {
}
impl TestEnv {
pub fn disable_upgrade_storage(&mut self) {
self.upgrade_storage = None;
}
pub fn customization_mut(&mut self) -> &mut TestCustomization {
&mut self.customization
}
@@ -165,12 +155,6 @@ impl UserPresence for TestUserPresence {
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 {
@@ -197,8 +181,6 @@ impl Env for TestEnv {
type KeyStore = Self;
type AttestationStore = Self;
type Clock = TestClock;
type UpgradeStorage = BufferUpgradeStorage;
type FirmwareProtection = Self;
type Write = TestWrite;
type Customization = TestCustomization;
type HidConnection = Self;
@@ -228,14 +210,6 @@ impl Env for TestEnv {
&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
}
@@ -252,6 +226,10 @@ impl Env for TestEnv {
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection {
self
}
fn firmware_version(&self) -> Option<u64> {
Some(0)
}
}
#[cfg(test)]

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,10 +12,8 @@
// 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::api::attestation_store::{self, AttestationStore};
use crate::ctap::command::{AuthenticatorConfigParameters, Command};
use crate::ctap::data_formats::ConfigSubCommand;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::{Channel, CtapState};
@@ -24,29 +22,17 @@ 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,
) -> Result<attestation_store::Attestation, Ctap2StatusCode> {
let attestation = attestation_store::Attestation {
private_key: [0x41; 32],
certificate: vec![0xdd; 20],
};
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)?;
env.attestation_store()
.set(&attestation_store::Id::Batch, Some(&attestation))?;
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
@@ -57,5 +43,5 @@ pub fn enable_enterprise_attestation<E: Env>(
let config_command = Command::AuthenticatorConfig(config_params);
state.process_parsed_command(env, config_command, DUMMY_CHANNEL)?;
Ok(attestation_material)
Ok(attestation)
}