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:
@@ -28,7 +28,7 @@ following:
|
||||
* `nrfutil` (can be installed using `pip3 install nrfutil`) if you want to flash
|
||||
a device with DFU
|
||||
* `uuid-runtime` if you are missing the `uuidgen` command.
|
||||
* `llvm` if you want to use the upgradability feature.
|
||||
* `llvm` and `gcc-arm-none-eabi` if you want to use the upgradability feature.
|
||||
|
||||
The proprietary software to use the default programmer can be found on the
|
||||
[Segger website](https://www.segger.com/downloads/jlink). Please follow their
|
||||
|
||||
@@ -21,7 +21,7 @@ use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use alloc::{format, vec};
|
||||
use core::fmt::Write;
|
||||
use ctap2::env::tock::{take_storage, TockStorage};
|
||||
use ctap2::env::tock::{take_storage, Storage};
|
||||
use libtock_drivers::console::Console;
|
||||
use libtock_drivers::timer::{self, Duration, Timer, Timestamp};
|
||||
use persistent_store::Store;
|
||||
@@ -39,7 +39,7 @@ fn measure<T>(timer: &Timer, operation: impl FnOnce() -> T) -> (T, Duration<f64>
|
||||
(result, after - before)
|
||||
}
|
||||
|
||||
fn boot_store(mut storage: TockStorage, erase: bool) -> Store<TockStorage> {
|
||||
fn boot_store(mut storage: Storage, erase: bool) -> Store<Storage> {
|
||||
use persistent_store::Storage;
|
||||
let num_pages = storage.num_pages();
|
||||
if erase {
|
||||
@@ -55,7 +55,7 @@ struct StorageConfig {
|
||||
num_pages: usize,
|
||||
}
|
||||
|
||||
fn storage_config(storage: &TockStorage) -> StorageConfig {
|
||||
fn storage_config(storage: &Storage) -> StorageConfig {
|
||||
use persistent_store::Storage;
|
||||
StorageConfig {
|
||||
num_pages: storage.num_pages(),
|
||||
@@ -73,12 +73,12 @@ struct Stat {
|
||||
}
|
||||
|
||||
fn compute_latency(
|
||||
storage: TockStorage,
|
||||
storage: Storage,
|
||||
timer: &Timer,
|
||||
num_pages: usize,
|
||||
key_increment: usize,
|
||||
word_length: usize,
|
||||
) -> (TockStorage, Stat) {
|
||||
) -> (Storage, Stat) {
|
||||
let mut stat = Stat {
|
||||
key_increment,
|
||||
entry_length: word_length,
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
33
libraries/opensk/src/env/mod.rs
vendored
33
libraries/opensk/src/env/mod.rs
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
30
libraries/opensk/src/env/test/mod.rs
vendored
30
libraries/opensk/src/env/test/mod.rs
vendored
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
// 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 super::storage_helper::ModRange;
|
||||
use alloc::boxed::Box;
|
||||
use persistent_store::{StorageError, StorageResult};
|
||||
|
||||
@@ -44,10 +43,8 @@ impl BufferUpgradeStorage {
|
||||
Err(StorageError::OutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeStorage for BufferUpgradeStorage {
|
||||
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
|
||||
pub fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
|
||||
if offset == 0 && data.len() != METADATA_LENGTH {
|
||||
return Err(StorageError::OutOfBounds);
|
||||
}
|
||||
@@ -63,11 +60,11 @@ impl UpgradeStorage for BufferUpgradeStorage {
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_identifier(&self) -> u32 {
|
||||
pub fn bundle_identifier(&self) -> u32 {
|
||||
0x60000
|
||||
}
|
||||
|
||||
fn running_firmware_version(&self) -> u64 {
|
||||
pub fn running_firmware_version(&self) -> u64 {
|
||||
0
|
||||
}
|
||||
}
|
||||
694
src/env/tock/commands.rs
vendored
Normal file
694
src/env/tock/commands.rs
vendored
Normal file
@@ -0,0 +1,694 @@
|
||||
// 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::TockEnv;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use arrayref::array_ref;
|
||||
use core::convert::TryFrom;
|
||||
use opensk::api::attestation_store::{self, Attestation, AttestationStore};
|
||||
use opensk::api::crypto::sha256::Sha256;
|
||||
use opensk::api::crypto::EC_FIELD_SIZE;
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
use opensk::api::customization::Customization;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use opensk::ctap::check_user_presence;
|
||||
use opensk::ctap::data_formats::{
|
||||
extract_bool, extract_byte_string, extract_map, extract_unsigned, ok_or_missing,
|
||||
};
|
||||
use opensk::ctap::status_code::Ctap2StatusCode;
|
||||
use opensk::ctap::{cbor_read, cbor_write, Channel};
|
||||
use opensk::env::{Env, Sha};
|
||||
use sk_cbor as cbor;
|
||||
use sk_cbor::{cbor_map_options, destructure_cbor_map};
|
||||
|
||||
const VENDOR_COMMAND_CONFIGURE: u8 = 0x40;
|
||||
const VENDOR_COMMAND_UPGRADE: u8 = 0x42;
|
||||
const VENDOR_COMMAND_UPGRADE_INFO: u8 = 0x43;
|
||||
|
||||
pub fn process_vendor_command(
|
||||
env: &mut TockEnv,
|
||||
bytes: &[u8],
|
||||
channel: Channel,
|
||||
) -> Option<Vec<u8>> {
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
if matches!(channel, Channel::VendorHid(_)) {
|
||||
return None;
|
||||
}
|
||||
process_cbor(env, bytes, channel).unwrap_or_else(|e| Some(vec![e as u8]))
|
||||
}
|
||||
|
||||
fn process_cbor(
|
||||
env: &mut TockEnv,
|
||||
bytes: &[u8],
|
||||
channel: Channel,
|
||||
) -> Result<Option<Vec<u8>>, Ctap2StatusCode> {
|
||||
match bytes[0] {
|
||||
VENDOR_COMMAND_CONFIGURE => {
|
||||
let decoded_cbor = cbor_read(&bytes[1..])?;
|
||||
let params = VendorConfigureParameters::try_from(decoded_cbor)?;
|
||||
let response = process_vendor_configure(env, params, channel)?;
|
||||
Ok(Some(encode_cbor(response.into())))
|
||||
}
|
||||
VENDOR_COMMAND_UPGRADE => {
|
||||
let decoded_cbor = cbor_read(&bytes[1..])?;
|
||||
let params = VendorUpgradeParameters::try_from(decoded_cbor)?;
|
||||
process_vendor_upgrade(env, params)?;
|
||||
Ok(Some(vec![Ctap2StatusCode::CTAP2_OK as u8]))
|
||||
}
|
||||
VENDOR_COMMAND_UPGRADE_INFO => {
|
||||
let response = process_vendor_upgrade_info(env)?;
|
||||
Ok(Some(encode_cbor(response.into())))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_cbor(value: cbor::Value) -> Vec<u8> {
|
||||
let mut response_vec = vec![Ctap2StatusCode::CTAP2_OK as u8];
|
||||
if cbor_write(value, &mut response_vec).is_err() {
|
||||
vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]
|
||||
} else {
|
||||
response_vec
|
||||
}
|
||||
}
|
||||
|
||||
fn process_vendor_configure(
|
||||
env: &mut TockEnv,
|
||||
params: VendorConfigureParameters,
|
||||
// Unused in std only
|
||||
_channel: Channel,
|
||||
) -> Result<VendorConfigureResponse, Ctap2StatusCode> {
|
||||
if params.attestation_material.is_some() || params.lockdown {
|
||||
// This is removed in std so we don't need too many mocks in TockEnv.
|
||||
#[cfg(not(feature = "std"))]
|
||||
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 => VendorConfigureResponse {
|
||||
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))?;
|
||||
}
|
||||
VendorConfigureResponse {
|
||||
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.lock_firmware_protection()
|
||||
{
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn process_vendor_upgrade(
|
||||
env: &mut TockEnv,
|
||||
params: VendorUpgradeParameters,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
let VendorUpgradeParameters { offset, data, hash } = params;
|
||||
let calculated_hash = Sha::<TockEnv>::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)
|
||||
}
|
||||
|
||||
fn process_vendor_upgrade_info(
|
||||
env: &mut TockEnv,
|
||||
) -> Result<VendorUpgradeInfoResponse, Ctap2StatusCode> {
|
||||
let upgrade_locations = env
|
||||
.upgrade_storage()
|
||||
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?;
|
||||
Ok(VendorUpgradeInfoResponse {
|
||||
info: upgrade_locations.bundle_identifier(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AttestationMaterial {
|
||||
pub certificate: Vec<u8>,
|
||||
pub private_key: [u8; EC_FIELD_SIZE],
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for AttestationMaterial {
|
||||
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() != EC_FIELD_SIZE {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
let private_key = array_ref!(private_key, 0, EC_FIELD_SIZE);
|
||||
Ok(AttestationMaterial {
|
||||
certificate,
|
||||
private_key: *private_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct VendorConfigureParameters {
|
||||
pub lockdown: bool,
|
||||
pub attestation_material: Option<AttestationMaterial>,
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for VendorConfigureParameters {
|
||||
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(AttestationMaterial::try_from)
|
||||
.transpose()?;
|
||||
Ok(VendorConfigureParameters {
|
||||
lockdown,
|
||||
attestation_material,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct VendorUpgradeParameters {
|
||||
pub offset: usize,
|
||||
pub data: Vec<u8>,
|
||||
pub hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for VendorUpgradeParameters {
|
||||
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(VendorUpgradeParameters { offset, data, hash })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct VendorConfigureResponse {
|
||||
pub cert_programmed: bool,
|
||||
pub pkey_programmed: bool,
|
||||
}
|
||||
|
||||
impl From<VendorConfigureResponse> for cbor::Value {
|
||||
fn from(vendor_response: VendorConfigureResponse) -> Self {
|
||||
let VendorConfigureResponse {
|
||||
cert_programmed,
|
||||
pkey_programmed,
|
||||
} = vendor_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => cert_programmed,
|
||||
0x02 => pkey_programmed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct VendorUpgradeInfoResponse {
|
||||
pub info: u32,
|
||||
}
|
||||
|
||||
impl From<VendorUpgradeInfoResponse> for cbor::Value {
|
||||
fn from(vendor_upgrade_info_response: VendorUpgradeInfoResponse) -> Self {
|
||||
let VendorUpgradeInfoResponse { info } = vendor_upgrade_info_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => info as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use cbor::cbor_map;
|
||||
|
||||
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
|
||||
|
||||
#[test]
|
||||
fn test_process_cbor_unrelated_input() {
|
||||
let mut env = TockEnv::default();
|
||||
let cbor_bytes = vec![0x01];
|
||||
assert_eq!(process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL), Ok(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_cbor_invalid_input() {
|
||||
let mut env = TockEnv::default();
|
||||
let cbor_bytes = vec![VENDOR_COMMAND_CONFIGURE];
|
||||
assert_eq!(
|
||||
process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_cbor_valid_input() {
|
||||
let mut env = TockEnv::default();
|
||||
let cbor_bytes = vec![VENDOR_COMMAND_UPGRADE_INFO];
|
||||
assert!(process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_configure_parameters() {
|
||||
let dummy_cert = [0xddu8; 20];
|
||||
let dummy_pkey = [0x41u8; EC_FIELD_SIZE];
|
||||
|
||||
// Attestation key is too short.
|
||||
let cbor_value = cbor_map! {
|
||||
0x01 => false,
|
||||
0x02 => cbor_map! {
|
||||
0x01 => dummy_cert,
|
||||
0x02 => dummy_pkey[..EC_FIELD_SIZE - 1]
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
VendorConfigureParameters::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!(
|
||||
VendorConfigureParameters::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!(
|
||||
VendorConfigureParameters::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!(
|
||||
VendorConfigureParameters::try_from(cbor_value),
|
||||
Ok(VendorConfigureParameters {
|
||||
lockdown: false,
|
||||
attestation_material: Some(AttestationMaterial {
|
||||
certificate: dummy_cert.to_vec(),
|
||||
private_key: dummy_pkey
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_upgrade_parameters() {
|
||||
// Missing offset
|
||||
let cbor_value = cbor_map! {
|
||||
0x02 => [0xFF; 0x100],
|
||||
0x03 => [0x44; 32],
|
||||
};
|
||||
assert_eq!(
|
||||
VendorUpgradeParameters::try_from(cbor_value),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
|
||||
);
|
||||
|
||||
// Missing data
|
||||
let cbor_value = cbor_map! {
|
||||
0x01 => 0x1000,
|
||||
0x03 => [0x44; 32],
|
||||
};
|
||||
assert_eq!(
|
||||
VendorUpgradeParameters::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!(
|
||||
VendorUpgradeParameters::try_from(cbor_value),
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
|
||||
);
|
||||
|
||||
// Missing hash
|
||||
let cbor_value = cbor_map! {
|
||||
0x01 => 0x1000,
|
||||
0x02 => [0xFF; 0x100],
|
||||
};
|
||||
assert_eq!(
|
||||
VendorUpgradeParameters::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!(
|
||||
VendorUpgradeParameters::try_from(cbor_value),
|
||||
Ok(VendorUpgradeParameters {
|
||||
offset: 0x1000,
|
||||
data: vec![0xFF; 0x100],
|
||||
hash: [0x44; 32],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_vendor_upgrade_info() {
|
||||
let mut env = TockEnv::default();
|
||||
let cbor_bytes = [VENDOR_COMMAND_UPGRADE_INFO];
|
||||
assert!(process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_configure() {
|
||||
let mut env = TockEnv::default();
|
||||
|
||||
// Nothing should be configured at the beginning
|
||||
let response = process_vendor_configure(
|
||||
&mut env,
|
||||
VendorConfigureParameters {
|
||||
lockdown: false,
|
||||
attestation_material: None,
|
||||
},
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
Ok(VendorConfigureResponse {
|
||||
cert_programmed: false,
|
||||
pkey_programmed: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Inject dummy values
|
||||
let dummy_key = [0x41u8; EC_FIELD_SIZE];
|
||||
let dummy_cert = [0xddu8; 20];
|
||||
let response = process_vendor_configure(
|
||||
&mut env,
|
||||
VendorConfigureParameters {
|
||||
lockdown: false,
|
||||
attestation_material: Some(AttestationMaterial {
|
||||
certificate: dummy_cert.to_vec(),
|
||||
private_key: dummy_key,
|
||||
}),
|
||||
},
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
Ok(VendorConfigureResponse {
|
||||
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; EC_FIELD_SIZE];
|
||||
let response = process_vendor_configure(
|
||||
&mut env,
|
||||
VendorConfigureParameters {
|
||||
lockdown: false,
|
||||
attestation_material: Some(AttestationMaterial {
|
||||
certificate: dummy_cert.to_vec(),
|
||||
private_key: other_dummy_key,
|
||||
}),
|
||||
},
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
Ok(VendorConfigureResponse {
|
||||
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 = process_vendor_configure(
|
||||
&mut env,
|
||||
VendorConfigureParameters {
|
||||
lockdown: true,
|
||||
attestation_material: None,
|
||||
},
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
Ok(VendorConfigureResponse {
|
||||
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 = TockEnv::default();
|
||||
|
||||
const METADATA_LEN: usize = 0x1000;
|
||||
let metadata = vec![0xFF; METADATA_LEN];
|
||||
let metadata_hash = Sha::<TockEnv>::digest(&metadata);
|
||||
let data = vec![0xFF; 0x1000];
|
||||
let hash = Sha::<TockEnv>::digest(&data);
|
||||
|
||||
// Write to partition.
|
||||
let response = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
offset: 0x20000,
|
||||
data: data.clone(),
|
||||
hash,
|
||||
},
|
||||
);
|
||||
assert_eq!(response, Ok(()));
|
||||
|
||||
// TockEnv doesn't check the metadata, test its parser in your Env.
|
||||
let response = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
offset: 0,
|
||||
data: metadata.clone(),
|
||||
hash: metadata_hash,
|
||||
},
|
||||
);
|
||||
assert_eq!(response, Ok(()));
|
||||
|
||||
// TockEnv doesn't check the metadata, test its parser in your Env.
|
||||
let response = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
offset: METADATA_LEN,
|
||||
data: data.clone(),
|
||||
hash,
|
||||
},
|
||||
);
|
||||
assert_eq!(response, Ok(()));
|
||||
|
||||
// Write metadata of a wrong size.
|
||||
let response = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
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 = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
offset: 0x41000,
|
||||
data: data.clone(),
|
||||
hash,
|
||||
},
|
||||
);
|
||||
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER));
|
||||
|
||||
// Write a bad hash.
|
||||
let response = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
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 = TockEnv::default();
|
||||
env.disable_upgrade_storage();
|
||||
|
||||
let data = vec![0xFF; 0x1000];
|
||||
let hash = Sha::<TockEnv>::digest(&data);
|
||||
let response = process_vendor_upgrade(
|
||||
&mut env,
|
||||
VendorUpgradeParameters {
|
||||
offset: 0,
|
||||
data,
|
||||
hash,
|
||||
},
|
||||
);
|
||||
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_upgrade_info() {
|
||||
let mut env = TockEnv::default();
|
||||
let bundle_identifier = env.upgrade_storage().unwrap().bundle_identifier();
|
||||
|
||||
let upgrade_info_reponse = process_vendor_upgrade_info(&mut env);
|
||||
assert_eq!(
|
||||
upgrade_info_reponse,
|
||||
Ok(VendorUpgradeInfoResponse {
|
||||
info: bundle_identifier,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_response_into_cbor() {
|
||||
let response_cbor: cbor::Value = VendorConfigureResponse {
|
||||
cert_programmed: true,
|
||||
pkey_programmed: false,
|
||||
}
|
||||
.into();
|
||||
assert_eq!(
|
||||
response_cbor,
|
||||
cbor_map_options! {
|
||||
0x01 => true,
|
||||
0x02 => false,
|
||||
}
|
||||
);
|
||||
let response_cbor: cbor::Value = VendorConfigureResponse {
|
||||
cert_programmed: false,
|
||||
pkey_programmed: true,
|
||||
}
|
||||
.into();
|
||||
assert_eq!(
|
||||
response_cbor,
|
||||
cbor_map_options! {
|
||||
0x01 => false,
|
||||
0x02 => true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_upgrade_info_into_cbor() {
|
||||
let vendor_upgrade_info_response = VendorUpgradeInfoResponse { info: 0x00060000 };
|
||||
let response_cbor: cbor::Value = vendor_upgrade_info_response.into();
|
||||
let expected_cbor = cbor_map! {
|
||||
0x01 => 0x00060000,
|
||||
};
|
||||
assert_eq!(response_cbor, expected_cbor);
|
||||
}
|
||||
}
|
||||
121
src/env/tock/mod.rs
vendored
121
src/env/tock/mod.rs
vendored
@@ -12,33 +12,54 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub use self::storage::{TockStorage, TockUpgradeStorage};
|
||||
use alloc::vec::Vec;
|
||||
use clock::TockClock;
|
||||
use core::cell::Cell;
|
||||
use core::convert::TryFrom;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use libtock_core::result::{CommandError, EALREADY};
|
||||
use libtock_drivers::buttons::{self, ButtonState};
|
||||
use libtock_drivers::console::Console;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use libtock_drivers::crp;
|
||||
use libtock_drivers::result::{FlexUnwrap, TockError};
|
||||
use libtock_drivers::timer::Duration;
|
||||
use libtock_drivers::{crp, led, rng, timer, usb_ctap_hid};
|
||||
use libtock_drivers::{led, rng, timer, usb_ctap_hid};
|
||||
use opensk::api::attestation_store::AttestationStore;
|
||||
use opensk::api::connection::{
|
||||
HidConnection, SendOrRecvError, SendOrRecvResult, SendOrRecvStatus, UsbEndpoint,
|
||||
};
|
||||
use opensk::api::crypto::software_crypto::SoftwareCrypto;
|
||||
use opensk::api::customization::{CustomizationImpl, AAGUID_LENGTH, DEFAULT_CUSTOMIZATION};
|
||||
use opensk::api::firmware_protection::FirmwareProtection;
|
||||
use opensk::api::rng::Rng;
|
||||
use opensk::api::user_presence::{UserPresence, UserPresenceError, UserPresenceResult};
|
||||
use opensk::api::{attestation_store, key_store};
|
||||
use opensk::ctap::Channel;
|
||||
use opensk::env::Env;
|
||||
#[cfg(feature = "std")]
|
||||
use persistent_store::{BufferOptions, BufferStorage};
|
||||
use persistent_store::{StorageResult, Store};
|
||||
use rand_core::{impls, CryptoRng, Error, RngCore};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod buffer_upgrade_storage;
|
||||
mod clock;
|
||||
mod commands;
|
||||
#[cfg(not(feature = "std"))]
|
||||
mod storage;
|
||||
mod storage_helper;
|
||||
mod upgrade_helper;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub type Storage = storage::TockStorage;
|
||||
#[cfg(feature = "std")]
|
||||
pub type Storage = BufferStorage;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
type UpgradeStorage = storage::TockUpgradeStorage;
|
||||
#[cfg(feature = "std")]
|
||||
type UpgradeStorage = buffer_upgrade_storage::BufferUpgradeStorage;
|
||||
|
||||
pub const AAGUID: &[u8; AAGUID_LENGTH] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin"));
|
||||
@@ -97,8 +118,8 @@ impl HidConnection for TockHidConnection {
|
||||
|
||||
pub struct TockEnv {
|
||||
rng: TockRng,
|
||||
store: Store<TockStorage>,
|
||||
upgrade_storage: Option<TockUpgradeStorage>,
|
||||
store: Store<Storage>,
|
||||
upgrade_storage: Option<UpgradeStorage>,
|
||||
main_connection: TockHidConnection,
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
vendor_connection: TockHidConnection,
|
||||
@@ -116,7 +137,7 @@ impl Default for TockEnv {
|
||||
// We rely on `take_storage` to ensure that this function is called only once.
|
||||
let storage = take_storage().unwrap();
|
||||
let store = Store::new(storage).ok().unwrap();
|
||||
let upgrade_storage = TockUpgradeStorage::new().ok();
|
||||
let upgrade_storage = UpgradeStorage::new().ok();
|
||||
TockEnv {
|
||||
rng: TockRng {},
|
||||
store,
|
||||
@@ -134,16 +155,65 @@ impl Default for TockEnv {
|
||||
}
|
||||
}
|
||||
|
||||
impl TockEnv {
|
||||
/// 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`.
|
||||
pub fn upgrade_storage(&mut self) -> Option<&mut UpgradeStorage> {
|
||||
self.upgrade_storage.as_mut()
|
||||
}
|
||||
|
||||
pub fn disable_upgrade_storage(&mut self) {
|
||||
self.upgrade_storage = None;
|
||||
}
|
||||
|
||||
pub fn lock_firmware_protection(&mut self) -> bool {
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
matches!(
|
||||
crp::set_protection(crp::ProtectionLevel::FullyLocked),
|
||||
Ok(())
|
||||
| Err(TockError::Command(CommandError {
|
||||
return_code: EALREADY,
|
||||
..
|
||||
}))
|
||||
)
|
||||
}
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unique storage instance.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - If called a second time.
|
||||
pub fn take_storage() -> StorageResult<TockStorage> {
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub fn take_storage() -> StorageResult<Storage> {
|
||||
// Make sure the storage was not already taken.
|
||||
static TAKEN: AtomicBool = AtomicBool::new(false);
|
||||
assert!(!TAKEN.fetch_or(true, Ordering::SeqCst));
|
||||
TockStorage::new()
|
||||
Storage::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn take_storage() -> StorageResult<Storage> {
|
||||
// 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,
|
||||
};
|
||||
Ok(BufferStorage::new(store, options))
|
||||
}
|
||||
|
||||
impl UserPresence for TockEnv {
|
||||
@@ -216,19 +286,6 @@ impl UserPresence for TockEnv {
|
||||
}
|
||||
}
|
||||
|
||||
impl FirmwareProtection for TockEnv {
|
||||
fn lock(&mut self) -> bool {
|
||||
matches!(
|
||||
crp::set_protection(crp::ProtectionLevel::FullyLocked),
|
||||
Ok(())
|
||||
| Err(TockError::Command(CommandError {
|
||||
return_code: EALREADY,
|
||||
..
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl key_store::Helper for TockEnv {}
|
||||
|
||||
impl AttestationStore for TockEnv {
|
||||
@@ -257,12 +314,10 @@ impl AttestationStore for TockEnv {
|
||||
impl Env for TockEnv {
|
||||
type Rng = TockRng;
|
||||
type UserPresence = Self;
|
||||
type Storage = TockStorage;
|
||||
type Storage = Storage;
|
||||
type KeyStore = Self;
|
||||
type AttestationStore = Self;
|
||||
type Clock = TockClock;
|
||||
type UpgradeStorage = TockUpgradeStorage;
|
||||
type FirmwareProtection = Self;
|
||||
type Write = Console;
|
||||
type Customization = CustomizationImpl;
|
||||
type HidConnection = TockHidConnection;
|
||||
@@ -292,14 +347,6 @@ impl Env for TockEnv {
|
||||
&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 {
|
||||
Console::new()
|
||||
}
|
||||
@@ -316,6 +363,16 @@ impl Env for TockEnv {
|
||||
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection {
|
||||
&mut self.vendor_connection
|
||||
}
|
||||
|
||||
fn process_vendor_command(&mut self, bytes: &[u8], channel: Channel) -> Option<Vec<u8>> {
|
||||
commands::process_vendor_command(self, bytes, channel)
|
||||
}
|
||||
|
||||
fn firmware_version(&self) -> Option<u64> {
|
||||
self.upgrade_storage
|
||||
.as_ref()
|
||||
.map(|u| u.running_firmware_version())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blink_leds(pattern_seed: usize) {
|
||||
|
||||
198
src/env/tock/storage.rs
vendored
198
src/env/tock/storage.rs
vendored
@@ -12,20 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::storage_helper::{find_slice, is_aligned, ModRange, Partition};
|
||||
use super::upgrade_helper::{
|
||||
check_metadata, parse_metadata_hash, parse_metadata_version, METADATA_SIGN_OFFSET,
|
||||
};
|
||||
use super::TockEnv;
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::vec::Vec;
|
||||
use arrayref::array_ref;
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
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 opensk::api::crypto::sha256::Sha256;
|
||||
use opensk::env::Sha;
|
||||
use persistent_store::{Storage, StorageError, StorageIndex, StorageResult};
|
||||
|
||||
const DRIVER_NUMBER: usize = 0x50003;
|
||||
const METADATA_SIGN_OFFSET: usize = 0x800;
|
||||
|
||||
const UPGRADE_PUBLIC_KEY: &[u8; 65] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_upgrade_pubkey.bin"));
|
||||
@@ -337,7 +337,7 @@ impl TockUpgradeStorage {
|
||||
/// Checks if the metadata's hash matches the partition's content.
|
||||
fn check_partition_hash(&self, metadata: &[u8]) -> StorageResult<()> {
|
||||
let start_address = self.metadata.start() + METADATA_SIGN_OFFSET;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut hasher = Sha::<TockEnv>::new();
|
||||
for range in self.partition.ranges_from(start_address) {
|
||||
let partition_slice = unsafe { read_slice(range.start(), range.length()) };
|
||||
// The hash implementation handles this in chunks, so no memory issues.
|
||||
@@ -349,10 +349,8 @@ impl TockUpgradeStorage {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeStorage for TockUpgradeStorage {
|
||||
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
|
||||
pub fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
|
||||
if data.is_empty() {
|
||||
return Err(StorageError::OutOfBounds);
|
||||
}
|
||||
@@ -363,7 +361,7 @@ impl UpgradeStorage for TockUpgradeStorage {
|
||||
let write_range = ModRange::new(address, data.len());
|
||||
if self.contains_metadata(&write_range)? {
|
||||
let new_metadata = &data[self.metadata.start() - address..][..self.metadata.length()];
|
||||
check_metadata(self, UPGRADE_PUBLIC_KEY, new_metadata)?;
|
||||
check_metadata::<TockEnv>(self, UPGRADE_PUBLIC_KEY, new_metadata)?;
|
||||
}
|
||||
|
||||
// Erases all pages that have their first byte in the write range.
|
||||
@@ -384,11 +382,11 @@ impl UpgradeStorage for TockUpgradeStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundle_identifier(&self) -> u32 {
|
||||
pub fn bundle_identifier(&self) -> u32 {
|
||||
self.identifier
|
||||
}
|
||||
|
||||
fn running_firmware_version(&self) -> u64 {
|
||||
pub fn running_firmware_version(&self) -> u64 {
|
||||
let running_metadata = unsafe {
|
||||
read_slice(
|
||||
self.running_metadata.start(),
|
||||
@@ -398,175 +396,3 @@ impl UpgradeStorage for TockUpgradeStorage {
|
||||
parse_metadata_version(running_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the metadata of an upgrade, and checks its correctness.
|
||||
///
|
||||
/// The metadata is a page starting with:
|
||||
/// - 32 B upgrade hash (SHA256)
|
||||
/// - 64 B signature,
|
||||
/// that are not signed over. The second part is included in the signature with
|
||||
/// - 8 B version and
|
||||
/// - 4 B partition address in little endian encoding
|
||||
/// written at METADATA_SIGN_OFFSET.
|
||||
///
|
||||
/// Checks signature correctness against the hash, and whether the partition offset matches.
|
||||
/// Whether the hash matches the partition content is not tested here!
|
||||
fn check_metadata(
|
||||
upgrade_locations: &impl UpgradeStorage,
|
||||
public_key_bytes: &[u8],
|
||||
metadata: &[u8],
|
||||
) -> StorageResult<()> {
|
||||
const METADATA_LEN: usize = 0x1000;
|
||||
if metadata.len() != METADATA_LEN {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
|
||||
let version = parse_metadata_version(metadata);
|
||||
if version < upgrade_locations.running_firmware_version() {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
|
||||
let metadata_address = LittleEndian::read_u32(&metadata[METADATA_SIGN_OFFSET + 8..][..4]);
|
||||
if metadata_address != upgrade_locations.bundle_identifier() {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
|
||||
verify_signature(
|
||||
array_ref!(metadata, 32, 64),
|
||||
public_key_bytes,
|
||||
parse_metadata_hash(metadata),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses the metadata, returns the hash.
|
||||
fn parse_metadata_hash(data: &[u8]) -> &[u8; 32] {
|
||||
array_ref!(data, 0, 32)
|
||||
}
|
||||
|
||||
/// Parses the metadata, returns the firmware version.
|
||||
fn parse_metadata_version(data: &[u8]) -> u64 {
|
||||
LittleEndian::read_u64(&data[METADATA_SIGN_OFFSET..][..8])
|
||||
}
|
||||
|
||||
/// Verifies the signature over the given hash.
|
||||
///
|
||||
/// The public key is COSE encoded, and the hash is a SHA256.
|
||||
fn verify_signature(
|
||||
signature_bytes: &[u8; 64],
|
||||
public_key_bytes: &[u8],
|
||||
signed_hash: &[u8; 32],
|
||||
) -> StorageResult<()> {
|
||||
let signature =
|
||||
ecdsa::Signature::from_bytes(signature_bytes).ok_or(StorageError::CustomError)?;
|
||||
let public_key = ecdsa::PubKey::from_bytes_uncompressed(public_key_bytes)
|
||||
.ok_or(StorageError::CustomError)?;
|
||||
if !public_key.verify_hash_vartime(signed_hash, &signature) {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use opensk::env::test::TestEnv;
|
||||
use opensk::env::Env;
|
||||
|
||||
#[test]
|
||||
fn test_check_metadata() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = crypto::ecdsa::SecKey::gensk(env.rng());
|
||||
let upgrade_locations = env.upgrade_storage().unwrap();
|
||||
|
||||
const METADATA_LEN: usize = 0x1000;
|
||||
const METADATA_SIGN_OFFSET: usize = 0x800;
|
||||
let mut metadata = vec![0xFF; METADATA_LEN];
|
||||
LittleEndian::write_u32(&mut metadata[METADATA_SIGN_OFFSET + 8..][..4], 0x60000);
|
||||
|
||||
let mut signed_over_data = metadata[METADATA_SIGN_OFFSET..].to_vec();
|
||||
signed_over_data.extend(&[0xFF; 0x20000]);
|
||||
let signed_hash = Sha256::hash(&signed_over_data);
|
||||
|
||||
metadata[..32].copy_from_slice(&signed_hash);
|
||||
let signature = private_key.sign_rfc6979::<Sha256>(&signed_over_data);
|
||||
let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH];
|
||||
signature.to_bytes(&mut signature_bytes);
|
||||
metadata[32..96].copy_from_slice(&signature_bytes);
|
||||
|
||||
let public_key = private_key.genpk();
|
||||
let mut public_key_bytes = [0; 65];
|
||||
public_key.to_bytes_uncompressed(&mut public_key_bytes);
|
||||
|
||||
assert_eq!(
|
||||
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Manipulating the partition address fails.
|
||||
metadata[METADATA_SIGN_OFFSET + 8] = 0x88;
|
||||
assert_eq!(
|
||||
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
metadata[METADATA_SIGN_OFFSET + 8] = 0x00;
|
||||
// Wrong metadata length fails.
|
||||
assert_eq!(
|
||||
check_metadata(
|
||||
upgrade_locations,
|
||||
&public_key_bytes,
|
||||
&metadata[..METADATA_LEN - 1]
|
||||
),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
// Manipulating the hash fails.
|
||||
metadata[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
metadata[0] ^= 0x01;
|
||||
// Manipulating the signature fails.
|
||||
metadata[32] ^= 0x01;
|
||||
assert_eq!(
|
||||
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_signature() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = crypto::ecdsa::SecKey::gensk(env.rng());
|
||||
let message = [0x44; 64];
|
||||
let signed_hash = Sha256::hash(&message);
|
||||
let signature = private_key.sign_rfc6979::<Sha256>(&message);
|
||||
|
||||
let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH];
|
||||
signature.to_bytes(&mut signature_bytes);
|
||||
|
||||
let public_key = private_key.genpk();
|
||||
let mut public_key_bytes = [0; 65];
|
||||
public_key.to_bytes_uncompressed(&mut public_key_bytes);
|
||||
|
||||
assert_eq!(
|
||||
verify_signature(&signature_bytes, &public_key_bytes, &signed_hash),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
verify_signature(&signature_bytes, &public_key_bytes, &[0x55; 32]),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
public_key_bytes[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify_signature(&signature_bytes, &public_key_bytes, &signed_hash),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
public_key_bytes[0] ^= 0x01;
|
||||
signature_bytes[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify_signature(&signature_bytes, &public_key_bytes, &signed_hash),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
219
src/env/tock/upgrade_helper.rs
vendored
Normal file
219
src/env/tock/upgrade_helper.rs
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
// 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))]
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use crate::env::tock::buffer_upgrade_storage::BufferUpgradeStorage;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use crate::env::tock::storage::TockUpgradeStorage;
|
||||
use arrayref::array_ref;
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use opensk::api::crypto::ecdsa::{PublicKey as _, Signature as _};
|
||||
use opensk::env::{EcdsaPk, EcdsaSignature, Env};
|
||||
use persistent_store::{StorageError, StorageResult};
|
||||
|
||||
pub const METADATA_SIGN_OFFSET: usize = 0x800;
|
||||
|
||||
/// Parses the metadata of an upgrade, and checks its correctness.
|
||||
///
|
||||
/// The metadata is a page starting with:
|
||||
/// - 32 B upgrade hash (SHA256)
|
||||
/// - 64 B signature,
|
||||
/// that are not signed over. The second part is included in the signature with
|
||||
/// - 8 B version and
|
||||
/// - 4 B partition address in little endian encoding
|
||||
/// written at METADATA_SIGN_OFFSET.
|
||||
///
|
||||
/// Checks signature correctness against the hash, and whether the partition offset matches.
|
||||
/// Whether the hash matches the partition content is not tested here!
|
||||
pub fn check_metadata<E: Env>(
|
||||
#[cfg(not(feature = "std"))] upgrade_locations: &TockUpgradeStorage,
|
||||
#[cfg(feature = "std")] upgrade_locations: &BufferUpgradeStorage,
|
||||
public_key_bytes: &[u8],
|
||||
metadata: &[u8],
|
||||
) -> StorageResult<()> {
|
||||
const METADATA_LEN: usize = 0x1000;
|
||||
if metadata.len() != METADATA_LEN {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
|
||||
let version = parse_metadata_version(metadata);
|
||||
if version < upgrade_locations.running_firmware_version() {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
|
||||
let metadata_address = LittleEndian::read_u32(&metadata[METADATA_SIGN_OFFSET + 8..][..4]);
|
||||
if metadata_address != upgrade_locations.bundle_identifier() {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
|
||||
verify_signature::<E>(
|
||||
array_ref!(metadata, 32, 64),
|
||||
public_key_bytes,
|
||||
parse_metadata_hash(metadata),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses the metadata, returns the hash.
|
||||
pub fn parse_metadata_hash(data: &[u8]) -> &[u8; 32] {
|
||||
array_ref!(data, 0, 32)
|
||||
}
|
||||
|
||||
/// Parses the metadata, returns the firmware version.
|
||||
pub fn parse_metadata_version(data: &[u8]) -> u64 {
|
||||
LittleEndian::read_u64(&data[METADATA_SIGN_OFFSET..][..8])
|
||||
}
|
||||
|
||||
/// Verifies the signature over the given hash.
|
||||
///
|
||||
/// The public key is COSE encoded, and the hash is a SHA256.
|
||||
fn verify_signature<E: Env>(
|
||||
signature_bytes: &[u8; 64],
|
||||
public_key_bytes: &[u8],
|
||||
signed_hash: &[u8; 32],
|
||||
) -> StorageResult<()> {
|
||||
let signature =
|
||||
EcdsaSignature::<E>::from_slice(signature_bytes).ok_or(StorageError::CustomError)?;
|
||||
if public_key_bytes.len() != 65 || public_key_bytes[0] != 0x04 {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
let x = array_ref!(public_key_bytes, 1, 32);
|
||||
let y = array_ref!(public_key_bytes, 33, 32);
|
||||
let public_key = EcdsaPk::<E>::from_coordinates(x, y).ok_or(StorageError::CustomError)?;
|
||||
if !public_key.verify_prehash(signed_hash, &signature) {
|
||||
return Err(StorageError::CustomError);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use arrayref::mut_array_refs;
|
||||
use opensk::api::crypto::ecdsa::SecretKey as _;
|
||||
use opensk::api::crypto::sha256::Sha256;
|
||||
use opensk::api::crypto::{EC_FIELD_SIZE, EC_SIGNATURE_SIZE};
|
||||
use opensk::env::test::TestEnv;
|
||||
use opensk::env::{EcdsaSk, Sha};
|
||||
|
||||
fn to_uncompressed(public_key: &EcdsaPk<TestEnv>) -> [u8; 1 + 2 * EC_FIELD_SIZE] {
|
||||
// Formatting according to:
|
||||
// https://tools.ietf.org/id/draft-jivsov-ecc-compact-05.html#overview
|
||||
const B0_BYTE_MARKER: u8 = 0x04;
|
||||
let mut representation = [0; 1 + 2 * EC_FIELD_SIZE];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (marker, x, y) = mut_array_refs![&mut representation, 1, EC_FIELD_SIZE, EC_FIELD_SIZE];
|
||||
marker[0] = B0_BYTE_MARKER;
|
||||
public_key.to_coordinates(x, y);
|
||||
representation
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_metadata() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = EcdsaSk::<TestEnv>::random(env.rng());
|
||||
let upgrade_locations = BufferUpgradeStorage::new().unwrap();
|
||||
|
||||
const METADATA_LEN: usize = 0x1000;
|
||||
const METADATA_SIGN_OFFSET: usize = 0x800;
|
||||
let mut metadata = vec![0xFF; METADATA_LEN];
|
||||
LittleEndian::write_u32(&mut metadata[METADATA_SIGN_OFFSET + 8..][..4], 0x60000);
|
||||
|
||||
let mut signed_over_data = metadata[METADATA_SIGN_OFFSET..].to_vec();
|
||||
signed_over_data.extend(&[0xFF; 0x20000]);
|
||||
let signed_hash = Sha::<TestEnv>::digest(&signed_over_data);
|
||||
|
||||
metadata[..32].copy_from_slice(&signed_hash);
|
||||
let signature = private_key.sign(&signed_over_data);
|
||||
let mut signature_bytes = [0; EC_SIGNATURE_SIZE];
|
||||
signature.to_slice(&mut signature_bytes);
|
||||
metadata[32..96].copy_from_slice(&signature_bytes);
|
||||
|
||||
let public_key = private_key.public_key();
|
||||
let public_key_bytes = to_uncompressed(&public_key);
|
||||
|
||||
assert_eq!(
|
||||
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Manipulating the partition address fails.
|
||||
metadata[METADATA_SIGN_OFFSET + 8] = 0x88;
|
||||
assert_eq!(
|
||||
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
metadata[METADATA_SIGN_OFFSET + 8] = 0x00;
|
||||
// Wrong metadata length fails.
|
||||
assert_eq!(
|
||||
check_metadata::<TestEnv>(
|
||||
&upgrade_locations,
|
||||
&public_key_bytes,
|
||||
&metadata[..METADATA_LEN - 1]
|
||||
),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
// Manipulating the hash fails.
|
||||
metadata[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
metadata[0] ^= 0x01;
|
||||
// Manipulating the signature fails.
|
||||
metadata[32] ^= 0x01;
|
||||
assert_eq!(
|
||||
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_signature() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = EcdsaSk::<TestEnv>::random(env.rng());
|
||||
let message = [0x44; 64];
|
||||
let signed_hash = Sha::<TestEnv>::digest(&message);
|
||||
let signature = private_key.sign(&message);
|
||||
|
||||
let mut signature_bytes = [0; EC_SIGNATURE_SIZE];
|
||||
signature.to_slice(&mut signature_bytes);
|
||||
|
||||
let public_key = private_key.public_key();
|
||||
let mut public_key_bytes = to_uncompressed(&public_key);
|
||||
|
||||
assert_eq!(
|
||||
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &signed_hash),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &[0x55; 32]),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
public_key_bytes[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &signed_hash),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
public_key_bytes[0] ^= 0x01;
|
||||
signature_bytes[0] ^= 0x01;
|
||||
assert_eq!(
|
||||
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &signed_hash),
|
||||
Err(StorageError::CustomError)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user