pub mod migrate;
use core::num::NonZeroU32;
use ctap_types::{
ctap2::AttestationFormatsPreference,
sizes::MAX_CREDENTIAL_COUNT_IN_LIST, Error,
String,
};
use littlefs2_core::{path, Path};
use trussed_core::{
mechanisms::{Chacha8Poly1305, P256},
syscall, try_syscall,
types::{KeyId, Location, Mechanism, Message, PathBuf},
CertificateClient, CryptoClient, FilesystemClient,
};
use heapless::binary_heap::{BinaryHeap, Max};
use crate::{
credential::FullCredential,
ctap2::{self, pin::PinProtocolState},
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,
key_encryption_key: Option<KeyId>,
key_wrapping_key: Option<KeyId>,
consecutive_pin_mismatches: u8,
#[serde(with = "serde_bytes")]
pin_hash: Option<[u8; 16]>,
timestamp: u32,
}
impl PersistentState {
const RESET_RETRIES: u8 = 8;
const FILENAME: &'static Path = path!("persistent-state.cbor");
pub 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) -> 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.key_encryption_key = None;
self.key_wrapping_key = None;
self.consecutive_pin_mismatches = 0;
self.pin_hash = None;
self.timestamp = 0;
self.save(trussed)
}
pub fn load_if_not_initialised<T: FilesystemClient>(&mut self, trussed: &mut T) {
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.initialised = true;
}
}
pub fn timestamp<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<u32> {
let now = self.timestamp;
self.timestamp += 1;
self.save(trussed)?;
Ok(now)
}
pub fn key_encryption_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyId> {
match self.key_encryption_key {
Some(key) => Ok(key),
None => self.rotate_key_encryption_key(trussed),
}
}
pub fn rotate_key_encryption_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyId> {
if let Some(key) = self.key_encryption_key {
syscall!(trussed.delete(key));
}
let key = syscall!(trussed.generate_chacha8poly1305_key(Location::Internal)).key;
self.key_encryption_key = Some(key);
self.save(trussed)?;
Ok(key)
}
pub fn key_wrapping_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyId> {
match self.key_wrapping_key {
Some(key) => Ok(key),
None => self.rotate_key_wrapping_key(trussed),
}
}
pub fn rotate_key_wrapping_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
&mut self,
trussed: &mut T,
) -> Result<KeyId> {
self.load_if_not_initialised(trussed);
if let Some(key) = self.key_wrapping_key {
syscall!(trussed.delete(key));
}
let key = syscall!(trussed.generate_chacha8poly1305_key(Location::Internal)).key;
self.key_wrapping_key = Some(key);
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 set_pin_hash<T: FilesystemClient>(
&mut self,
trussed: &mut T,
pin_hash: [u8; 16],
) -> Result<()> {
self.pin_hash = Some(pin_hash);
self.save(trussed)?;
Ok(())
}
}
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;
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 hex_literal::hex;
#[test]
fn deser() {
let _state: PersistentState = trussed::cbor_deserialize(&hex!(
"
a5726b65795f656e6372797074696f6e5f6b657950b19a5a2845e5ec71e3
2a1b890892376c706b65795f7772617070696e675f6b6579f6781a636f6e
73656375746976655f70696e5f6d69736d617463686573006870696e5f68
6173689018ef1879187c1881181818f0182d18fb186418960718dd185d18
3f188c18766974696d657374616d7009
"
))
.unwrap();
}
}