pub mod migrate;
use core::num::NonZeroU32;
use ctap_types::{
ctap2::{
config::{DEFAULT_MIN_PIN_LENGTH, MAX_MIN_PIN_LENGTH_RP_IDS, MAX_RP_ID_LENGTH},
AttestationFormatsPreference,
},
sizes::MAX_CREDENTIAL_COUNT_IN_LIST, Error,
String,
};
use littlefs2_core::{path, Path};
use trussed_core::{
mechanisms::P256,
syscall, try_syscall,
types::{KeyId, Location, Mechanism, Message, PathBuf},
CertificateClient, CryptoClient, FilesystemClient,
};
use heapless::{
binary_heap::{BinaryHeap, Max},
Vec,
};
use crate::{
credential::{CredentialIdVersion, FullCredential, KeyEncryptionKey, KeyWrappingKey},
ctap2::{self, pin::PinProtocolState},
Config, Result,
};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CachedCredential {
pub timestamp: u32,
pub path: String<37>,
}
impl PartialOrd for CachedCredential {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CachedCredential {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.timestamp.cmp(&other.timestamp)
}
}
#[derive(Clone, Debug, Default)]
pub struct CredentialCacheGeneric<const N: usize>(BinaryHeap<CachedCredential, Max, N>);
impl<const N: usize> CredentialCacheGeneric<N> {
pub fn push(&mut self, item: CachedCredential) {
if self.0.len() == self.0.capacity() {
self.0.pop();
}
self.0.push(item).map_err(drop).unwrap();
}
pub fn pop(&mut self) -> Option<CachedCredential> {
self.0.pop()
}
pub fn len(&self) -> u32 {
self.0.len() as u32
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn clear(&mut self) {
self.0.clear()
}
}
pub type CredentialCache = CredentialCacheGeneric<MAX_CREDENTIAL_COUNT_IN_LIST>;
#[derive(Debug)]
pub struct State {
pub identity: Identity,
pub persistent: PersistentState,
pub runtime: RuntimeState,
}
impl Default for State {
fn default() -> Self {
Self::new()
}
}
impl State {
pub fn new() -> Self {
let identity = Default::default();
let runtime: RuntimeState = Default::default();
let persistent = Default::default();
Self {
identity,
persistent,
runtime,
}
}
pub fn decrement_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
self.persistent.decrement_retries(trussed)?;
self.runtime.decrement_retries();
Ok(())
}
pub fn reset_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
self.persistent.reset_retries(trussed)?;
self.runtime.reset_retries();
Ok(())
}
pub fn pin_blocked(&self) -> Result<()> {
if self.persistent.pin_blocked() {
return Err(Error::PinBlocked);
}
if self.runtime.pin_blocked() {
return Err(Error::PinAuthBlocked);
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Identity {
attestation_key: Option<KeyId>,
}
pub type Aaguid = [u8; 16];
pub type Certificate = trussed_core::types::Message;
impl Identity {
fn yank_aaguid(&mut self, der: &[u8]) -> Option<[u8; 16]> {
let aaguid_start_sequence = [
0x06u8, 0x0B, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x01, 0x01, 0x04,
0x04, 0x12, 0x04, 0x10,
];
let mut cert_reader = der;
while !cert_reader.is_empty() {
if cert_reader.starts_with(&aaguid_start_sequence) {
info_now!("found aaguid");
break;
}
cert_reader = &cert_reader[1..];
}
if cert_reader.is_empty() {
return None;
}
cert_reader = &cert_reader[aaguid_start_sequence.len()..];
let mut aaguid = [0u8; 16];
aaguid[..16].clone_from_slice(&cert_reader[..16]);
Some(aaguid)
}
pub fn attestation<T: CryptoClient + CertificateClient>(
&mut self,
trussed: &mut T,
) -> (Option<(KeyId, Certificate)>, Aaguid) {
let key = crate::constants::ATTESTATION_KEY_ID;
let attestation_key_exists = syscall!(trussed.exists(Mechanism::P256, key)).exists;
if attestation_key_exists {
let cert =
syscall!(trussed.read_certificate(crate::constants::ATTESTATION_CERT_ID)).der;
let mut aaguid = self.yank_aaguid(cert.as_slice());
if aaguid.is_none() {
aaguid = Some(*b"AAGUID0123456789");
}
(Some((key, cert)), aaguid.unwrap())
} else {
info_now!("attestation key does not exist");
(None, *b"AAGUID0123456789")
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CredentialManagementEnumerateRps {
pub remaining: NonZeroU32,
pub rp_id_hash: [u8; 32],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CredentialManagementEnumerateCredentials {
pub remaining: u32,
pub prev_filename: PathBuf,
}
#[derive(Clone, Debug, Default)]
pub struct ActiveGetAssertionData {
pub rp_id_hash: [u8; 32],
pub client_data_hash: [u8; 32],
pub uv_performed: bool,
pub up_performed: bool,
pub multiple_credentials: bool,
pub extensions: Option<ctap_types::ctap2::get_assertion::ExtensionsInput>,
pub attestation_formats_preference: Option<AttestationFormatsPreference>,
}
#[derive(Debug, Default)]
pub struct RuntimeState {
pin_protocol: Option<PinProtocolState>,
consecutive_pin_mismatches: u8,
cached_credentials: CredentialCache,
pub active_get_assertion: Option<ActiveGetAssertionData>,
pub cached_rp: Option<CredentialManagementEnumerateRps>,
pub cached_rk: Option<CredentialManagementEnumerateCredentials>,
pub large_blobs: ctap2::large_blobs::State,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Default)]
pub struct PersistentState {
#[serde(skip)]
initialised: bool,
credential_id_version: Option<CredentialIdVersion>,
key_encryption_key: Option<KeyId>,
key_wrapping_key: Option<KeyId>,
consecutive_pin_mismatches: u8,
#[serde(with = "serde_bytes")]
pin_hash: Option<[u8; 16]>,
#[serde(default)]
pin_code_point_length: u8,
timestamp: u32,
#[serde(default)]
min_pin_length: u8,
#[serde(default)]
min_pin_length_rp_ids: Vec<String<MAX_RP_ID_LENGTH>, MAX_MIN_PIN_LENGTH_RP_IDS>,
#[serde(default)]
force_pin_change: bool,
#[serde(default)]
always_uv: bool,
}
impl PersistentState {
const RESET_RETRIES: u8 = 8;
const FILENAME: &'static Path = path!("persistent-state.cbor");
fn reset_value(config: &Config) -> Self {
Self {
initialised: false,
key_encryption_key: None,
key_wrapping_key: None,
credential_id_version: config.credential_id_version,
consecutive_pin_mismatches: 0,
pin_hash: None,
pin_code_point_length: 0,
timestamp: 1,
min_pin_length: 0,
min_pin_length_rp_ids: Vec::new(),
force_pin_change: false,
always_uv: false,
}
}
fn load<T: FilesystemClient>(trussed: &mut T) -> Result<Self> {
let result =
try_syscall!(trussed.read_file(Location::Internal, PathBuf::from(Self::FILENAME),))
.map_err(|_| Error::Other);
if result.is_err() {
info!("err loading: {:?}", result.err().unwrap());
return Err(Error::Other);
}
let data = result.unwrap().data;
let state: Self = cbor_smol::cbor_deserialize(&data).map_err(|_err| {
info!("err deser'ing: {_err:?}",);
info!("{}", hex_str!(&data));
Error::Other
})?;
debug!("Loaded state: {state:#?}");
Ok(state)
}
pub fn save<T: FilesystemClient>(&self, trussed: &mut T) -> Result<()> {
let mut data = Message::new();
cbor_smol::cbor_serialize_to(self, &mut data).unwrap();
syscall!(trussed.write_file(
Location::Internal,
PathBuf::from(Self::FILENAME),
data,
None,
));
Ok(())
}
pub fn reset<T: CryptoClient + FilesystemClient>(
&mut self,
trussed: &mut T,
config: &Config,
) -> Result<()> {
if let Some(key) = self.key_encryption_key {
syscall!(trussed.delete(key));
}
if let Some(key) = self.key_wrapping_key {
syscall!(trussed.delete(key));
}
*self = Self::reset_value(config);
self.initialised = true;
self.save(trussed)
}
pub fn load_if_not_initialised<T: FilesystemClient>(
&mut self,
trussed: &mut T,
config: &Config,
) {
if !self.initialised {
match Self::load(trussed) {
Ok(previous_self) => {
info!("loaded previous state!");
*self = previous_self
}
Err(_err) => {
info!("error with previous state! {:?}", _err);
*self = Self::reset_value(config);
}
}
self.initialised = true;
}
}
pub fn signature_counter<T: CryptoClient + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<u32> {
let now = self.timestamp;
if now > 0 {
let increment = syscall!(trussed.random_bytes(1)).bytes[0];
if let Some(timestamp) = self.timestamp.checked_add(u32::from(increment) + 1) {
self.timestamp = timestamp;
} else {
self.timestamp = 0;
}
self.save(trussed)?;
}
Ok(now)
}
pub fn credential_id_version(&self) -> CredentialIdVersion {
self.credential_id_version
.unwrap_or(CredentialIdVersion::V1)
}
pub fn key_encryption_key<T: CryptoClient + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyEncryptionKey> {
match self.key_encryption_key {
Some(key) => Ok(KeyEncryptionKey(key)),
None => self.rotate_key_encryption_key(trussed),
}
}
pub fn rotate_key_encryption_key<T: CryptoClient + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyEncryptionKey> {
if let Some(key) = self.key_encryption_key {
syscall!(trussed.delete(key));
}
let key = self
.credential_id_version()
.generate_key_encryption_key(trussed);
self.key_encryption_key = Some(key.0);
self.save(trussed)?;
Ok(key)
}
pub fn key_wrapping_key<T: CryptoClient + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyWrappingKey> {
match self.key_wrapping_key {
Some(key) => Ok(KeyWrappingKey(key)),
None => self.rotate_key_wrapping_key(trussed),
}
}
pub fn rotate_key_wrapping_key<T: CryptoClient + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyWrappingKey> {
if let Some(key) = self.key_wrapping_key {
syscall!(trussed.delete(key));
}
let key = self
.credential_id_version()
.generate_key_wrapping_key(trussed);
self.key_wrapping_key = Some(key.0);
self.save(trussed)?;
Ok(key)
}
pub fn pin_is_set(&self) -> bool {
self.pin_hash.is_some()
}
pub fn retries(&self) -> u8 {
Self::RESET_RETRIES.saturating_sub(self.consecutive_pin_mismatches)
}
pub fn pin_blocked(&self) -> bool {
self.consecutive_pin_mismatches >= Self::RESET_RETRIES
}
fn reset_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
if self.consecutive_pin_mismatches > 0 {
self.consecutive_pin_mismatches = 0;
self.save(trussed)?;
}
Ok(())
}
fn decrement_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
if self.consecutive_pin_mismatches < Self::RESET_RETRIES {
self.consecutive_pin_mismatches += 1;
self.save(trussed)?;
if self.consecutive_pin_mismatches == 0 {
return Err(Error::PinBlocked);
}
}
Ok(())
}
pub fn pin_hash(&self) -> Option<[u8; 16]> {
self.pin_hash
}
pub fn pin_code_point_length(&self) -> u8 {
self.pin_code_point_length
}
pub fn set_pin_hash<T: FilesystemClient>(
&mut self,
trussed: &mut T,
pin_hash: [u8; 16],
pin_code_point_length: u8,
) -> Result<()> {
if self.pin_hash == Some(pin_hash) {
return Ok(());
}
self.pin_hash = Some(pin_hash);
self.pin_code_point_length = pin_code_point_length;
self.force_pin_change = false;
self.save(trussed)?;
Ok(())
}
pub fn min_pin_length(&self) -> u8 {
core::cmp::max(self.min_pin_length, DEFAULT_MIN_PIN_LENGTH)
}
pub fn set_min_pin_length<T: FilesystemClient>(
&mut self,
trussed: &mut T,
new_value: u8,
) -> Result<()> {
let cur = self.min_pin_length();
if new_value < cur {
return Err(Error::PinPolicyViolation);
}
if new_value == cur {
return Ok(());
}
self.min_pin_length = new_value;
self.save(trussed)?;
Ok(())
}
pub fn min_pin_length_rp_ids(&self) -> &[heapless::String<MAX_RP_ID_LENGTH>] {
&self.min_pin_length_rp_ids
}
pub fn set_min_pin_length_rp_ids<T: FilesystemClient>(
&mut self,
trussed: &mut T,
rp_ids: heapless::Vec<heapless::String<MAX_RP_ID_LENGTH>, MAX_MIN_PIN_LENGTH_RP_IDS>,
) -> Result<()> {
self.min_pin_length_rp_ids = rp_ids;
self.save(trussed)?;
Ok(())
}
pub fn force_pin_change(&self) -> bool {
self.force_pin_change
}
pub fn set_force_pin_change<T: FilesystemClient>(
&mut self,
trussed: &mut T,
value: bool,
) -> Result<()> {
if self.force_pin_change == value {
return Ok(());
}
self.force_pin_change = value;
self.save(trussed)?;
Ok(())
}
pub fn always_uv(&self) -> bool {
self.always_uv
}
pub fn toggle_always_uv<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
self.always_uv = !self.always_uv;
self.save(trussed)
}
}
impl RuntimeState {
const POWERCYCLE_RETRIES: u8 = 3;
fn decrement_retries(&mut self) {
if self.consecutive_pin_mismatches < Self::POWERCYCLE_RETRIES {
self.consecutive_pin_mismatches += 1;
}
}
fn reset_retries(&mut self) {
self.consecutive_pin_mismatches = 0;
}
pub fn pin_blocked(&self) -> bool {
self.consecutive_pin_mismatches >= Self::POWERCYCLE_RETRIES
}
pub fn clear_credential_cache(&mut self) {
self.cached_credentials.clear()
}
pub fn push_credential(&mut self, credential: CachedCredential) {
self.cached_credentials.push(credential);
}
pub fn pop_credential<T: FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Option<FullCredential> {
let cached_credential = self.cached_credentials.pop()?;
let credential_data = syscall!(trussed.read_file(
Location::Internal,
PathBuf::try_from(cached_credential.path.as_str()).unwrap(),
))
.data;
FullCredential::deserialize(&credential_data).ok()
}
pub fn remaining_credentials(&self) -> u32 {
self.cached_credentials.len() as _
}
pub fn pin_protocol<T: P256>(&mut self, trussed: &mut T) -> &mut PinProtocolState {
self.pin_protocol
.get_or_insert_with(|| PinProtocolState::new(trussed))
}
pub fn reset<T: CryptoClient + P256>(&mut self, trussed: &mut T) {
syscall!(trussed.delete_all(Location::Volatile));
self.clear_credential_cache();
self.active_get_assertion = None;
self.cached_rp = None;
self.cached_rk = None;
self.consecutive_pin_mismatches = 0;
if let Some(pin_protocol) = self.pin_protocol.take() {
pin_protocol.reset(trussed);
}
self.pin_protocol = Some(PinProtocolState::new(trussed));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use hex_literal::hex;
use trussed::{
backend::BackendId,
virt::{self, StoreConfig},
};
use trussed_staging::virt::{BackendIds, Dispatcher};
#[test]
fn deser() {
let _state: PersistentState = trussed::cbor_deserialize(&hex!(
"
a5726b65795f656e6372797074696f6e5f6b657950b19a5a2845e5ec71e3
2a1b890892376c706b65795f7772617070696e675f6b6579f6781a636f6e
73656375746976655f70696e5f6d69736d617463686573006870696e5f68
6173689018ef1879187c1881181818f0182d18fb186418960718dd185d18
3f188c18766974696d657374616d7009
"
))
.unwrap();
}
#[test]
fn test_signature_counter() {
let config = Config::new(0);
virt::with_platform(StoreConfig::ram(), |platform| {
platform.run_client_with_backends(
"fido",
Dispatcher::default(),
&[
BackendId::Custom(BackendIds::StagingBackend),
BackendId::Core,
],
|mut client| {
let mut state = PersistentState::default();
state.load_if_not_initialised(&mut client, &config);
let counter1 = state.signature_counter(&mut client).unwrap();
let counter2 = state.signature_counter(&mut client).unwrap();
let counter3 = state.signature_counter(&mut client).unwrap();
let counter4 = state.signature_counter(&mut client).unwrap();
let counter5 = state.signature_counter(&mut client).unwrap();
assert_eq!(counter1, 1);
assert!(counter2 > counter1);
assert!(counter3 > counter2);
assert!(counter4 > counter3);
assert!(counter5 > counter4);
assert!(counter5 > counter1 + 4);
assert!(counter5 < counter1 + 4 * 256);
state.timestamp = u32::MAX;
let counter6 = state.signature_counter(&mut client).unwrap();
let counter7 = state.signature_counter(&mut client).unwrap();
let counter8 = state.signature_counter(&mut client).unwrap();
let counter9 = state.signature_counter(&mut client).unwrap();
assert_eq!(counter6, u32::MAX);
assert_eq!(counter7, 0);
assert_eq!(counter8, 0);
assert_eq!(counter9, 0);
},
)
})
}
}