diff --git a/docs/install.md b/docs/install.md index f326f59..5303ee2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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 diff --git a/examples/store_latency.rs b/examples/store_latency.rs index a9d714d..06c978e 100644 --- a/examples/store_latency.rs +++ b/examples/store_latency.rs @@ -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(timer: &Timer, operation: impl FnOnce() -> T) -> (T, Duration (result, after - before) } -fn boot_store(mut storage: TockStorage, erase: bool) -> Store { +fn boot_store(mut storage: Storage, erase: bool) -> Store { 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, diff --git a/libraries/opensk/src/api/attestation_store.rs b/libraries/opensk/src/api/attestation_store.rs index 8d9d5f2..3f80183 100644 --- a/libraries/opensk/src/api/attestation_store.rs +++ b/libraries/opensk/src/api/attestation_store.rs @@ -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, } @@ -65,11 +65,11 @@ pub fn helper_get(env: &mut impl Env) -> Result, 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, })) } diff --git a/libraries/opensk/src/api/crypto/ecdsa.rs b/libraries/opensk/src/api/crypto/ecdsa.rs index ea3b588..1dad533 100644 --- a/libraries/opensk/src/api/crypto/ecdsa.rs +++ b/libraries/opensk/src/api/crypto/ecdsa.rs @@ -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; + /// 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; } diff --git a/libraries/opensk/src/api/crypto/mod.rs b/libraries/opensk/src/api/crypto/mod.rs index fc4c6f7..669a6ed 100644 --- a/libraries/opensk/src/api/crypto/mod.rs +++ b/libraries/opensk/src/api/crypto/mod.rs @@ -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]; diff --git a/libraries/opensk/src/api/crypto/rust_crypto.rs b/libraries/opensk/src/api/crypto/rust_crypto.rs index 401db05..3bd5aef 100644 --- a/libraries/opensk/src/api/crypto/rust_crypto.rs +++ b/libraries/opensk/src/api/crypto/rust_crypto.rs @@ -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 { self.signature.to_der().to_vec() } diff --git a/libraries/opensk/src/api/crypto/software_crypto.rs b/libraries/opensk/src/api/crypto/software_crypto.rs index e11072e..74e8cec 100644 --- a/libraries/opensk/src/api/crypto/software_crypto.rs +++ b/libraries/opensk/src/api/crypto/software_crypto.rs @@ -145,6 +145,11 @@ impl ecdsa::PublicKey for SoftwareEcdsaPublicKey { .verify_vartime::(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 { self.signature.to_asn1_der() } diff --git a/libraries/opensk/src/api/mod.rs b/libraries/opensk/src/api/mod.rs index a15c0ec..41e733d 100644 --- a/libraries/opensk/src/api/mod.rs +++ b/libraries/opensk/src/api/mod.rs @@ -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; diff --git a/libraries/opensk/src/api/upgrade_storage/mod.rs b/libraries/opensk/src/api/upgrade_storage/mod.rs deleted file mode 100644 index ed54d29..0000000 --- a/libraries/opensk/src/api/upgrade_storage/mod.rs +++ /dev/null @@ -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) -> 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; -} diff --git a/libraries/opensk/src/ctap/command.rs b/libraries/opensk/src/ctap/command.rs index 6e3c567..b3a0e9d 100644 --- a/libraries/opensk/src/ctap/command.rs +++ b/libraries/opensk/src/ctap/command.rs @@ -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 { @@ -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 for AuthenticatorConfigParameters { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct AuthenticatorAttestationMaterial { - pub certificate: Vec, - pub private_key: [u8; ATTESTATION_PRIVATE_KEY_LENGTH], -} - -impl TryFrom for AuthenticatorAttestationMaterial { - type Error = Ctap2StatusCode; - - fn try_from(cbor_value: cbor::Value) -> Result { - 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 for AuthenticatorCredentialManagementParameters { } } -#[derive(Debug, PartialEq, Eq)] -pub struct AuthenticatorVendorConfigureParameters { - pub lockdown: bool, - pub attestation_material: Option, -} - -impl TryFrom for AuthenticatorVendorConfigureParameters { - type Error = Ctap2StatusCode; - - fn try_from(cbor_value: cbor::Value) -> Result { - 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, - pub hash: [u8; 32], -} - -impl TryFrom for AuthenticatorVendorUpgradeParameters { - type Error = Ctap2StatusCode; - - fn try_from(cbor_value: cbor::Value) -> Result { - 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)); - } } diff --git a/libraries/opensk/src/ctap/data_formats.rs b/libraries/opensk/src/ctap/data_formats.rs index 4beb396..3021449 100644 --- a/libraries/opensk/src/ctap/data_formats.rs +++ b/libraries/opensk/src/ctap/data_formats.rs @@ -1163,14 +1163,14 @@ impl From for cbor::Value { } } -pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { +pub fn extract_unsigned(cbor_value: cbor::Value) -> Result { 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 { +pub fn extract_integer(cbor_value: cbor::Value) -> Result { 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, Ctap2Stat } } -pub(super) fn extract_text_string(cbor_value: cbor::Value) -> Result { +pub fn extract_text_string(cbor_value: cbor::Value) -> Result { 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, Ctap2StatusCode> { +pub fn extract_array(cbor_value: cbor::Value) -> Result, 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, Ctap2StatusCode> { match cbor_value { @@ -1214,7 +1214,7 @@ pub(super) fn extract_map( } } -pub(super) fn extract_bool(cbor_value: cbor::Value) -> Result { +pub fn extract_bool(cbor_value: cbor::Value) -> Result { 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(value_option: Option) -> Result { +pub fn ok_or_missing(value_option: Option) -> Result { value_option.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) } diff --git a/libraries/opensk/src/ctap/mod.rs b/libraries/opensk/src/ctap/mod.rs index 1cbf160..49d1636 100644 --- a/libraries/opensk/src/ctap/mod.rs +++ b/libraries/opensk/src/ctap/mod.rs @@ -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 { .map_err(|_e| Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR) } -fn cbor_write(value: cbor::Value, encoded_cbor: &mut Vec) -> Result<(), Ctap2StatusCode> { +pub fn cbor_write(value: cbor::Value, encoded_cbor: &mut Vec) -> 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( /// Blocks for user presence. /// /// Returns an error in case of timeout, user declining presence request, or keepalive error. -fn check_user_presence(env: &mut E, channel: Channel) -> Result<(), Ctap2StatusCode> { +pub fn check_user_presence(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 StatefulPermission { } /// 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 StatefulPermission { // 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 CtapState { 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 { + 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 CtapState { // 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 CtapState { ) => (), (_, _) => 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 CtapState { 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 { 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 CtapState { ), 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 CtapState { Ok(ResponseData::AuthenticatorSelection) } - fn process_vendor_configure( - &mut self, - env: &mut E, - params: AuthenticatorVendorConfigureParameters, - channel: Channel, - ) -> Result { - 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 { - let AuthenticatorVendorUpgradeParameters { offset, data, hash } = params; - let calculated_hash = Sha::::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 { - 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 CtapState { 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::::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::::new(&mut env); - - const METADATA_LEN: usize = 0x1000; - let metadata = vec![0xFF; METADATA_LEN]; - let metadata_hash = Sha::::digest(&metadata); - let data = vec![0xFF; 0x1000]; - let hash = Sha::::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::::new(&mut env); - - let data = vec![0xFF; 0x1000]; - let hash = Sha::::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::::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)); } } diff --git a/libraries/opensk/src/ctap/response.rs b/libraries/opensk/src/ctap/response.rs index 865b374..ef7e225 100644 --- a/libraries/opensk/src/ctap/response.rs +++ b/libraries/opensk/src/ctap/response.rs @@ -37,9 +37,6 @@ pub enum ResponseData { AuthenticatorSelection, AuthenticatorLargeBlobs(Option), AuthenticatorConfig, - AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse), - AuthenticatorVendorUpgrade, - AuthenticatorVendorUpgradeInfo(AuthenticatorVendorUpgradeInfoResponse), } impl From for Option { @@ -55,9 +52,6 @@ impl From for Option { 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 for cbor::Value { } } -#[derive(Debug, PartialEq, Eq)] -pub struct AuthenticatorVendorConfigureResponse { - pub cert_programmed: bool, - pub pkey_programmed: bool, -} - -impl From 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 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 = ResponseData::AuthenticatorConfig.into(); assert_eq!(response_cbor, None); } - - #[test] - fn test_vendor_response_into_cbor() { - let response_cbor: Option = - 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 = - 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 = 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 = - ResponseData::AuthenticatorVendorUpgradeInfo(vendor_upgrade_info_response).into(); - let expected_cbor = cbor_map! { - 0x01 => 0x00060000, - }; - assert_eq!(response_cbor, Some(expected_cbor)); - } } diff --git a/libraries/opensk/src/ctap/storage.rs b/libraries/opensk/src/ctap/storage.rs index ebe9845..507d839 100644 --- a/libraries/opensk/src/ctap/storage.rs +++ b/libraries/opensk/src/ctap/storage.rs @@ -620,7 +620,6 @@ fn serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, 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() diff --git a/libraries/opensk/src/env/mod.rs b/libraries/opensk/src/env/mod.rs index 95bdc7b..01800be 100644 --- a/libraries/opensk/src/env/mod.rs +++ b/libraries/opensk/src/env/mod.rs @@ -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 = <<::Crypto as Crypto>::Ecdh as Ecdh>::SecretKey; pub type EcdhPk = <<::Crypto as Crypto>::Ecdh as Ecdh>::PublicKey; pub type EcdsaSk = <<::Crypto as Crypto>::Ecdsa as Ecdsa>::SecretKey; pub type EcdsaPk = <<::Crypto as Crypto>::Ecdsa as Ecdsa>::PublicKey; +pub type EcdsaSignature = <<::Crypto as Crypto>::Ecdsa as Ecdsa>::Signature; pub type Sha = <::Crypto as Crypto>::Sha256; pub type Hmac = <::Crypto as Crypto>::Hmac256; pub type Hkdf = <::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 { + 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> { + None + } } diff --git a/libraries/opensk/src/env/test/mod.rs b/libraries/opensk/src/env/test/mod.rs index 0c03ea3..81d9ca4 100644 --- a/libraries/opensk/src/env/test/mod.rs +++ b/libraries/opensk/src/env/test/mod.rs @@ -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, - upgrade_storage: Option, 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 { + Some(0) + } } #[cfg(test)] diff --git a/libraries/opensk/src/test_helpers/mod.rs b/libraries/opensk/src/test_helpers/mod.rs index ee65784..799160a 100644 --- a/libraries/opensk/src/test_helpers/mod.rs +++ b/libraries/opensk/src/test_helpers/mod.rs @@ -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( state: &mut CtapState, env: &mut E, -) -> Result { - 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 { + 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( let config_command = Command::AuthenticatorConfig(config_params); state.process_parsed_command(env, config_command, DUMMY_CHANNEL)?; - Ok(attestation_material) + Ok(attestation) } diff --git a/libraries/opensk/src/env/test/upgrade_storage.rs b/src/env/tock/buffer_upgrade_storage.rs similarity index 91% rename from libraries/opensk/src/env/test/upgrade_storage.rs rename to src/env/tock/buffer_upgrade_storage.rs index 9667099..27c9726 100644 --- a/libraries/opensk/src/env/test/upgrade_storage.rs +++ b/src/env/tock/buffer_upgrade_storage.rs @@ -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) -> StorageResult<()> { + pub fn write_bundle(&mut self, offset: usize, data: Vec) -> 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 } } diff --git a/src/env/tock/commands.rs b/src/env/tock/commands.rs new file mode 100644 index 0000000..7bbe668 --- /dev/null +++ b/src/env/tock/commands.rs @@ -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> { + #[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>, 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 { + 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 { + 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::::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 { + 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, + pub private_key: [u8; EC_FIELD_SIZE], +} + +impl TryFrom for AttestationMaterial { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + 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, +} + +impl TryFrom for VendorConfigureParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + 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, + pub hash: [u8; 32], +} + +impl TryFrom for VendorUpgradeParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + 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 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 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::::digest(&metadata); + let data = vec![0xFF; 0x1000]; + let hash = Sha::::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::::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); + } +} diff --git a/src/env/tock/mod.rs b/src/env/tock/mod.rs index fbf0cfc..6027906 100644 --- a/src/env/tock/mod.rs +++ b/src/env/tock/mod.rs @@ -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, - upgrade_storage: Option, + store: Store, + upgrade_storage: Option, 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 { +#[cfg(not(feature = "std"))] +pub fn take_storage() -> StorageResult { // 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 { + // 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> { + commands::process_vendor_command(self, bytes, channel) + } + + fn firmware_version(&self) -> Option { + self.upgrade_storage + .as_ref() + .map(|u| u.running_firmware_version()) + } } pub fn blink_leds(pattern_seed: usize) { diff --git a/src/env/tock/storage.rs b/src/env/tock/storage.rs index aaf5763..e48684d 100644 --- a/src/env/tock/storage.rs +++ b/src/env/tock/storage.rs @@ -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::::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) -> StorageResult<()> { + pub fn write_bundle(&mut self, offset: usize, data: Vec) -> 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::(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::(&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::(&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) - ); - } -} diff --git a/libraries/opensk/src/api/upgrade_storage/helper.rs b/src/env/tock/storage_helper.rs similarity index 100% rename from libraries/opensk/src/api/upgrade_storage/helper.rs rename to src/env/tock/storage_helper.rs diff --git a/src/env/tock/upgrade_helper.rs b/src/env/tock/upgrade_helper.rs new file mode 100644 index 0000000..f9947bc --- /dev/null +++ b/src/env/tock/upgrade_helper.rs @@ -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( + #[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::( + 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( + signature_bytes: &[u8; 64], + public_key_bytes: &[u8], + signed_hash: &[u8; 32], +) -> StorageResult<()> { + let signature = + EcdsaSignature::::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::::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) -> [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::::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::::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::(&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 = EcdsaSk::::random(env.rng()); + let message = [0x44; 64]; + let signed_hash = Sha::::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::(&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) + ); + } +}