Files
OpenSK/src/env/tock/commands.rs
kaczmarczyck 645c1ba3a7 Vendor Command + HID fix (#618)
* Fixes CBOR message passing through Vendor HID

I did all my tests on hardware with this fix, and now I'm surprised that
it didn't end up on develop. So should have been part of a former PR.

* vendor channel test

* forward vendor HID correctly for upgrades

* fixes cargo fmt

* removes script and updates documentation to match
2023-04-26 14:59:22 +02:00

709 lines
22 KiB
Rust

// 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::secret::Secret;
use opensk::ctap::status_code::Ctap2StatusCode;
use opensk::ctap::{cbor_read, cbor_write, Channel};
use opensk::env::{Env, Sha};
use sk_cbor as cbor;
use sk_cbor::{cbor_map_options, destructure_cbor_map};
const VENDOR_COMMAND_CONFIGURE: u8 = 0x40;
const VENDOR_COMMAND_UPGRADE: u8 = 0x42;
const VENDOR_COMMAND_UPGRADE_INFO: u8 = 0x43;
pub fn process_vendor_command(
env: &mut TockEnv,
bytes: &[u8],
channel: Channel,
) -> Option<Vec<u8>> {
#[cfg(feature = "vendor_hid")]
if matches!(channel, Channel::MainHid(_)) {
return None;
}
process_cbor(env, bytes, channel).unwrap_or_else(|e| Some(vec![e as u8]))
}
fn process_cbor(
env: &mut TockEnv,
bytes: &[u8],
channel: Channel,
) -> Result<Option<Vec<u8>>, Ctap2StatusCode> {
match bytes[0] {
VENDOR_COMMAND_CONFIGURE => {
let decoded_cbor = cbor_read(&bytes[1..])?;
let params = VendorConfigureParameters::try_from(decoded_cbor)?;
let response = process_vendor_configure(env, params, channel)?;
Ok(Some(encode_cbor(response.into())))
}
VENDOR_COMMAND_UPGRADE => {
let decoded_cbor = cbor_read(&bytes[1..])?;
let params = VendorUpgradeParameters::try_from(decoded_cbor)?;
process_vendor_upgrade(env, params)?;
Ok(Some(vec![Ctap2StatusCode::CTAP2_OK as u8]))
}
VENDOR_COMMAND_UPGRADE_INFO => {
let response = process_vendor_upgrade_info(env)?;
Ok(Some(encode_cbor(response.into())))
}
_ => Ok(None),
}
}
fn encode_cbor(value: cbor::Value) -> Vec<u8> {
let mut response_vec = vec![Ctap2StatusCode::CTAP2_OK as u8];
if cbor_write(value, &mut response_vec).is_err() {
vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]
} else {
response_vec
}
}
fn process_vendor_configure(
env: &mut TockEnv,
params: VendorConfigureParameters,
// Unused in std only
_channel: Channel,
) -> Result<VendorConfigureResponse, Ctap2StatusCode> {
if params.attestation_material.is_some() || params.lockdown {
// This is removed in std so we don't need too many mocks in TockEnv.
#[cfg(not(feature = "std"))]
check_user_presence(env, _channel)?;
}
// This command is for U2F support and we use the batch attestation there.
let attestation_id = attestation_store::Id::Batch;
// Sanity checks
let current_attestation = env.attestation_store().get(&attestation_id)?;
let response = match params.attestation_material {
None => VendorConfigureResponse {
cert_programmed: current_attestation.is_some(),
pkey_programmed: current_attestation.is_some(),
},
Some(data) => {
// We don't overwrite the attestation if it's already set. We don't return any error
// to not leak information.
if current_attestation.is_none() {
let attestation = Attestation {
private_key: Secret::from_exposed_secret(data.private_key),
certificate: data.certificate,
};
env.attestation_store()
.set(&attestation_id, Some(&attestation))?;
}
VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
}
};
if params.lockdown {
// To avoid bricking the authenticator, we only allow lockdown
// to happen if both values are programmed or if both U2F/CTAP1 and
// batch attestation are disabled.
#[cfg(feature = "with_ctap1")]
let need_certificate = true;
#[cfg(not(feature = "with_ctap1"))]
let need_certificate = env.customization().use_batch_attestation();
if (need_certificate && !(response.pkey_programmed && response.cert_programmed))
|| !env.lock_firmware_protection()
{
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
Ok(response)
}
fn process_vendor_upgrade(
env: &mut TockEnv,
params: VendorUpgradeParameters,
) -> Result<(), Ctap2StatusCode> {
let VendorUpgradeParameters { offset, data, hash } = params;
let calculated_hash = Sha::<TockEnv>::digest(&data);
if hash != calculated_hash {
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
}
env.upgrade_storage()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?
.write_bundle(offset, data)
.map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
fn process_vendor_upgrade_info(
env: &mut TockEnv,
) -> Result<VendorUpgradeInfoResponse, Ctap2StatusCode> {
let upgrade_locations = env
.upgrade_storage()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?;
Ok(VendorUpgradeInfoResponse {
info: upgrade_locations.bundle_identifier(),
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttestationMaterial {
pub certificate: Vec<u8>,
pub private_key: [u8; EC_FIELD_SIZE],
}
impl TryFrom<cbor::Value> for AttestationMaterial {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => certificate,
0x02 => private_key,
} = extract_map(cbor_value)?;
}
let certificate = extract_byte_string(ok_or_missing(certificate)?)?;
let private_key = extract_byte_string(ok_or_missing(private_key)?)?;
if private_key.len() != EC_FIELD_SIZE {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let private_key = array_ref!(private_key, 0, EC_FIELD_SIZE);
Ok(AttestationMaterial {
certificate,
private_key: *private_key,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorConfigureParameters {
pub lockdown: bool,
pub attestation_material: Option<AttestationMaterial>,
}
impl TryFrom<cbor::Value> for VendorConfigureParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => lockdown,
0x02 => attestation_material,
} = extract_map(cbor_value)?;
}
let lockdown = lockdown.map_or(Ok(false), extract_bool)?;
let attestation_material = attestation_material
.map(AttestationMaterial::try_from)
.transpose()?;
Ok(VendorConfigureParameters {
lockdown,
attestation_material,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorUpgradeParameters {
pub offset: usize,
pub data: Vec<u8>,
pub hash: [u8; 32],
}
impl TryFrom<cbor::Value> for VendorUpgradeParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => offset,
0x02 => data,
0x03 => hash,
} = extract_map(cbor_value)?;
}
let offset = extract_unsigned(ok_or_missing(offset)?)? as usize;
let data = extract_byte_string(ok_or_missing(data)?)?;
let hash = <[u8; 32]>::try_from(extract_byte_string(ok_or_missing(hash)?)?)
.map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
Ok(VendorUpgradeParameters { offset, data, hash })
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorConfigureResponse {
pub cert_programmed: bool,
pub pkey_programmed: bool,
}
impl From<VendorConfigureResponse> for cbor::Value {
fn from(vendor_response: VendorConfigureResponse) -> Self {
let VendorConfigureResponse {
cert_programmed,
pkey_programmed,
} = vendor_response;
cbor_map_options! {
0x01 => cert_programmed,
0x02 => pkey_programmed,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorUpgradeInfoResponse {
pub info: u32,
}
impl From<VendorUpgradeInfoResponse> for cbor::Value {
fn from(vendor_upgrade_info_response: VendorUpgradeInfoResponse) -> Self {
let VendorUpgradeInfoResponse { info } = vendor_upgrade_info_response;
cbor_map_options! {
0x01 => info as u64,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use cbor::cbor_map;
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
#[cfg(feature = "vendor_hid")]
const VENDOR_CHANNEL: Channel = Channel::VendorHid([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]
#[cfg(feature = "vendor_hid")]
fn test_process_command_valid_vendor_hid() {
let mut env = TockEnv::default();
let cbor_bytes = vec![VENDOR_COMMAND_UPGRADE_INFO];
assert!(process_cbor(&mut env, &cbor_bytes, VENDOR_CHANNEL)
.unwrap()
.is_some());
assert!(process_vendor_command(&mut env, &cbor_bytes, VENDOR_CHANNEL).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: Secret::from_exposed_secret(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: Secret::from_exposed_secret(dummy_key),
certificate: dummy_cert.to_vec(),
}))
);
// Now try to lock the device
let response = process_vendor_configure(
&mut env,
VendorConfigureParameters {
lockdown: true,
attestation_material: None,
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
})
);
}
#[test]
fn test_vendor_upgrade() {
// The test partition storage has size 0x40000.
// The test metadata storage has size 0x1000.
// The test identifier matches partition B.
let mut env = TockEnv::default();
const METADATA_LEN: usize = 0x1000;
let metadata = vec![0xFF; METADATA_LEN];
let metadata_hash = Sha::<TockEnv>::digest(&metadata);
let data = vec![0xFF; 0x1000];
let hash = Sha::<TockEnv>::digest(&data);
// Write to partition.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0x20000,
data: data.clone(),
hash,
},
);
assert_eq!(response, Ok(()));
// TockEnv doesn't check the metadata, test its parser in your Env.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0,
data: metadata.clone(),
hash: metadata_hash,
},
);
assert_eq!(response, Ok(()));
// TockEnv doesn't check the metadata, test its parser in your Env.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: METADATA_LEN,
data: data.clone(),
hash,
},
);
assert_eq!(response, Ok(()));
// Write metadata of a wrong size.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0,
data: metadata[..METADATA_LEN - 1].to_vec(),
hash: metadata_hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE));
// Write outside of the partition.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0x41000,
data: data.clone(),
hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER));
// Write a bad hash.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0x20000,
data,
hash: [0xEE; 32],
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE));
}
#[test]
fn test_vendor_upgrade_no_second_partition() {
let mut env = TockEnv::default();
env.disable_upgrade_storage();
let data = vec![0xFF; 0x1000];
let hash = Sha::<TockEnv>::digest(&data);
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0,
data,
hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND));
}
#[test]
fn test_vendor_upgrade_info() {
let mut env = TockEnv::default();
let bundle_identifier = env.upgrade_storage().unwrap().bundle_identifier();
let upgrade_info_reponse = process_vendor_upgrade_info(&mut env);
assert_eq!(
upgrade_info_reponse,
Ok(VendorUpgradeInfoResponse {
info: bundle_identifier,
})
);
}
#[test]
fn test_vendor_response_into_cbor() {
let response_cbor: cbor::Value = VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: false,
}
.into();
assert_eq!(
response_cbor,
cbor_map_options! {
0x01 => true,
0x02 => false,
}
);
let response_cbor: cbor::Value = VendorConfigureResponse {
cert_programmed: false,
pkey_programmed: true,
}
.into();
assert_eq!(
response_cbor,
cbor_map_options! {
0x01 => false,
0x02 => true,
}
);
}
#[test]
fn test_vendor_upgrade_info_into_cbor() {
let vendor_upgrade_info_response = VendorUpgradeInfoResponse { info: 0x00060000 };
let response_cbor: cbor::Value = vendor_upgrade_info_response.into();
let expected_cbor = cbor_map! {
0x01 => 0x00060000,
};
assert_eq!(response_cbor, expected_cbor);
}
}