1pub mod migrate;
6
7use core::num::NonZeroU32;
8
9use ctap_types::{
10 ctap2::AttestationFormatsPreference,
11 sizes::MAX_CREDENTIAL_COUNT_IN_LIST, Error,
14 String,
15};
16use littlefs2_core::{path, Path};
17use trussed_core::{
18 mechanisms::{Chacha8Poly1305, P256},
19 syscall, try_syscall,
20 types::{KeyId, Location, Mechanism, Message, PathBuf},
21 CertificateClient, CryptoClient, FilesystemClient,
22};
23
24use heapless::binary_heap::{BinaryHeap, Max};
25
26use crate::{
27 credential::FullCredential,
28 ctap2::{self, pin::PinProtocolState},
29 Result,
30};
31
32#[derive(Clone, Debug, Default, Eq, PartialEq)]
33pub struct CachedCredential {
34 pub timestamp: u32,
35 pub path: String<37>,
38}
39
40impl PartialOrd for CachedCredential {
41 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
42 Some(self.cmp(other))
43 }
44}
45
46impl Ord for CachedCredential {
47 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
48 self.timestamp.cmp(&other.timestamp)
49 }
50}
51
52#[derive(Clone, Debug, Default)]
53pub struct CredentialCacheGeneric<const N: usize>(BinaryHeap<CachedCredential, Max, N>);
54impl<const N: usize> CredentialCacheGeneric<N> {
55 pub fn push(&mut self, item: CachedCredential) {
56 if self.0.len() == self.0.capacity() {
57 self.0.pop();
58 }
59 self.0.push(item).map_err(drop).unwrap();
61 }
62
63 pub fn pop(&mut self) -> Option<CachedCredential> {
64 self.0.pop()
65 }
66
67 pub fn len(&self) -> u32 {
68 self.0.len() as u32
69 }
70
71 pub fn is_empty(&self) -> bool {
72 self.0.is_empty()
73 }
74
75 pub fn clear(&mut self) {
76 self.0.clear()
77 }
78}
79
80pub type CredentialCache = CredentialCacheGeneric<MAX_CREDENTIAL_COUNT_IN_LIST>;
81
82#[derive(Debug)]
83pub struct State {
84 pub identity: Identity,
86 pub persistent: PersistentState,
87 pub runtime: RuntimeState,
88}
89
90impl Default for State {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl State {
97 pub fn new() -> Self {
99 let identity = Default::default();
101 let runtime: RuntimeState = Default::default();
102 let persistent = Default::default();
104
105 Self {
106 identity,
107 persistent,
108 runtime,
109 }
110 }
111
112 pub fn decrement_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
113 self.persistent.decrement_retries(trussed)?;
114 self.runtime.decrement_retries();
115 Ok(())
116 }
117
118 pub fn reset_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
119 self.persistent.reset_retries(trussed)?;
120 self.runtime.reset_retries();
121 Ok(())
122 }
123
124 pub fn pin_blocked(&self) -> Result<()> {
125 if self.persistent.pin_blocked() {
126 return Err(Error::PinBlocked);
127 }
128 if self.runtime.pin_blocked() {
129 return Err(Error::PinAuthBlocked);
130 }
131
132 Ok(())
133 }
134}
135
136#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct Identity {
139 attestation_key: Option<KeyId>,
142}
143
144pub type Aaguid = [u8; 16];
145pub type Certificate = trussed_core::types::Message;
146
147impl Identity {
148 fn yank_aaguid(&mut self, der: &[u8]) -> Option<[u8; 16]> {
150 let aaguid_start_sequence = [
151 0x06u8, 0x0B, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x01, 0x01, 0x04,
153 0x04, 0x12, 0x04, 0x10,
155 ];
156
157 let mut cert_reader = der;
159
160 while !cert_reader.is_empty() {
161 if cert_reader.starts_with(&aaguid_start_sequence) {
162 info_now!("found aaguid");
163 break;
164 }
165 cert_reader = &cert_reader[1..];
166 }
167 if cert_reader.is_empty() {
168 return None;
169 }
170
171 cert_reader = &cert_reader[aaguid_start_sequence.len()..];
172
173 let mut aaguid = [0u8; 16];
174 aaguid[..16].clone_from_slice(&cert_reader[..16]);
175 Some(aaguid)
176 }
177
178 pub fn attestation<T: CryptoClient + CertificateClient>(
180 &mut self,
181 trussed: &mut T,
182 ) -> (Option<(KeyId, Certificate)>, Aaguid) {
183 let key = crate::constants::ATTESTATION_KEY_ID;
184 let attestation_key_exists = syscall!(trussed.exists(Mechanism::P256, key)).exists;
185 if attestation_key_exists {
186 let cert =
188 syscall!(trussed.read_certificate(crate::constants::ATTESTATION_CERT_ID)).der;
189
190 let mut aaguid = self.yank_aaguid(cert.as_slice());
191
192 if aaguid.is_none() {
193 aaguid = Some(*b"AAGUID0123456789");
195 }
196
197 (Some((key, cert)), aaguid.unwrap())
198 } else {
199 info_now!("attestation key does not exist");
200 (None, *b"AAGUID0123456789")
201 }
202 }
203}
204
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub struct CredentialManagementEnumerateRps {
207 pub remaining: NonZeroU32,
208 pub rp_id_hash: [u8; 32],
209}
210
211#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct CredentialManagementEnumerateCredentials {
213 pub remaining: u32,
214 pub prev_filename: PathBuf,
215}
216
217#[derive(Clone, Debug, Default)]
218pub struct ActiveGetAssertionData {
219 pub rp_id_hash: [u8; 32],
220 pub client_data_hash: [u8; 32],
221 pub uv_performed: bool,
222 pub up_performed: bool,
223 pub multiple_credentials: bool,
224 pub extensions: Option<ctap_types::ctap2::get_assertion::ExtensionsInput>,
225 pub attestation_formats_preference: Option<AttestationFormatsPreference>,
226}
227
228#[derive(Debug, Default)]
229pub struct RuntimeState {
230 pin_protocol: Option<PinProtocolState>,
231 consecutive_pin_mismatches: u8,
232
233 cached_credentials: CredentialCache,
235 pub active_get_assertion: Option<ActiveGetAssertionData>,
236 pub cached_rp: Option<CredentialManagementEnumerateRps>,
237 pub cached_rk: Option<CredentialManagementEnumerateCredentials>,
238
239 pub large_blobs: ctap2::large_blobs::State,
241}
242
243#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Default)]
258pub struct PersistentState {
259 #[serde(skip)]
260 initialised: bool,
265
266 key_encryption_key: Option<KeyId>,
267 key_wrapping_key: Option<KeyId>,
268 consecutive_pin_mismatches: u8,
269 #[serde(with = "serde_bytes")]
270 pin_hash: Option<[u8; 16]>,
271 timestamp: u32,
275}
276
277impl PersistentState {
278 const RESET_RETRIES: u8 = 8;
279 const FILENAME: &'static Path = path!("persistent-state.cbor");
280
281 pub fn load<T: FilesystemClient>(trussed: &mut T) -> Result<Self> {
282 let result =
284 try_syscall!(trussed.read_file(Location::Internal, PathBuf::from(Self::FILENAME),))
285 .map_err(|_| Error::Other);
286
287 if result.is_err() {
288 info!("err loading: {:?}", result.err().unwrap());
289 return Err(Error::Other);
290 }
291
292 let data = result.unwrap().data;
293
294 let state: Self = cbor_smol::cbor_deserialize(&data).map_err(|_err| {
295 info!("err deser'ing: {_err:?}",);
296 info!("{}", hex_str!(&data));
297 Error::Other
298 })?;
299
300 debug!("Loaded state: {state:#?}");
301
302 Ok(state)
303 }
304
305 pub fn save<T: FilesystemClient>(&self, trussed: &mut T) -> Result<()> {
306 let mut data = Message::new();
307 cbor_smol::cbor_serialize_to(self, &mut data).unwrap();
308
309 syscall!(trussed.write_file(
310 Location::Internal,
311 PathBuf::from(Self::FILENAME),
312 data,
313 None,
314 ));
315 Ok(())
316 }
317
318 pub fn reset<T: CryptoClient + FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
319 if let Some(key) = self.key_encryption_key {
320 syscall!(trussed.delete(key));
321 }
322 if let Some(key) = self.key_wrapping_key {
323 syscall!(trussed.delete(key));
324 }
325 self.key_encryption_key = None;
326 self.key_wrapping_key = None;
327 self.consecutive_pin_mismatches = 0;
328 self.pin_hash = None;
329 self.timestamp = 0;
330 self.save(trussed)
331 }
332
333 pub fn load_if_not_initialised<T: FilesystemClient>(&mut self, trussed: &mut T) {
334 if !self.initialised {
335 match Self::load(trussed) {
336 Ok(previous_self) => {
337 info!("loaded previous state!");
338 *self = previous_self
339 }
340 Err(_err) => {
341 info!("error with previous state! {:?}", _err);
342 }
343 }
344 self.initialised = true;
345 }
346 }
347
348 pub fn timestamp<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<u32> {
349 let now = self.timestamp;
350 self.timestamp += 1;
351 self.save(trussed)?;
352 Ok(now)
353 }
354
355 pub fn key_encryption_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
356 &mut self,
357 trussed: &mut T,
358 ) -> Result<KeyId> {
359 match self.key_encryption_key {
360 Some(key) => Ok(key),
361 None => self.rotate_key_encryption_key(trussed),
362 }
363 }
364
365 pub fn rotate_key_encryption_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
366 &mut self,
367 trussed: &mut T,
368 ) -> Result<KeyId> {
369 if let Some(key) = self.key_encryption_key {
370 syscall!(trussed.delete(key));
371 }
372 let key = syscall!(trussed.generate_chacha8poly1305_key(Location::Internal)).key;
373 self.key_encryption_key = Some(key);
374 self.save(trussed)?;
375 Ok(key)
376 }
377
378 pub fn key_wrapping_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
379 &mut self,
380 trussed: &mut T,
381 ) -> Result<KeyId> {
382 match self.key_wrapping_key {
383 Some(key) => Ok(key),
384 None => self.rotate_key_wrapping_key(trussed),
385 }
386 }
387
388 pub fn rotate_key_wrapping_key<T: CryptoClient + Chacha8Poly1305 + FilesystemClient>(
389 &mut self,
390 trussed: &mut T,
391 ) -> Result<KeyId> {
392 self.load_if_not_initialised(trussed);
393 if let Some(key) = self.key_wrapping_key {
394 syscall!(trussed.delete(key));
395 }
396 let key = syscall!(trussed.generate_chacha8poly1305_key(Location::Internal)).key;
397 self.key_wrapping_key = Some(key);
398 self.save(trussed)?;
399 Ok(key)
400 }
401
402 pub fn pin_is_set(&self) -> bool {
403 self.pin_hash.is_some()
404 }
405
406 pub fn retries(&self) -> u8 {
407 Self::RESET_RETRIES.saturating_sub(self.consecutive_pin_mismatches)
408 }
409
410 pub fn pin_blocked(&self) -> bool {
411 self.consecutive_pin_mismatches >= Self::RESET_RETRIES
412 }
413
414 fn reset_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
415 if self.consecutive_pin_mismatches > 0 {
416 self.consecutive_pin_mismatches = 0;
417 self.save(trussed)?;
418 }
419 Ok(())
420 }
421
422 fn decrement_retries<T: FilesystemClient>(&mut self, trussed: &mut T) -> Result<()> {
423 if self.consecutive_pin_mismatches < Self::RESET_RETRIES {
425 self.consecutive_pin_mismatches += 1;
426 self.save(trussed)?;
427 if self.consecutive_pin_mismatches == 0 {
428 return Err(Error::PinBlocked);
429 }
430 }
431 Ok(())
432 }
433
434 pub fn pin_hash(&self) -> Option<[u8; 16]> {
435 self.pin_hash
436 }
437
438 pub fn set_pin_hash<T: FilesystemClient>(
439 &mut self,
440 trussed: &mut T,
441 pin_hash: [u8; 16],
442 ) -> Result<()> {
443 self.pin_hash = Some(pin_hash);
444 self.save(trussed)?;
445 Ok(())
446 }
447}
448
449impl RuntimeState {
450 const POWERCYCLE_RETRIES: u8 = 3;
451
452 fn decrement_retries(&mut self) {
453 if self.consecutive_pin_mismatches < Self::POWERCYCLE_RETRIES {
454 self.consecutive_pin_mismatches += 1;
455 }
456 }
457
458 fn reset_retries(&mut self) {
459 self.consecutive_pin_mismatches = 0;
460 }
461
462 pub fn pin_blocked(&self) -> bool {
463 self.consecutive_pin_mismatches >= Self::POWERCYCLE_RETRIES
464 }
465
466 pub fn clear_credential_cache(&mut self) {
475 self.cached_credentials.clear()
476 }
477
478 pub fn push_credential(&mut self, credential: CachedCredential) {
479 self.cached_credentials.push(credential);
480 }
481
482 pub fn pop_credential<T: FilesystemClient>(
483 &mut self,
484 trussed: &mut T,
485 ) -> Option<FullCredential> {
486 let cached_credential = self.cached_credentials.pop()?;
487
488 let credential_data = syscall!(trussed.read_file(
489 Location::Internal,
490 PathBuf::try_from(cached_credential.path.as_str()).unwrap(),
491 ))
492 .data;
493
494 FullCredential::deserialize(&credential_data).ok()
495 }
496
497 pub fn remaining_credentials(&self) -> u32 {
498 self.cached_credentials.len() as _
499 }
500
501 pub fn pin_protocol<T: P256>(&mut self, trussed: &mut T) -> &mut PinProtocolState {
502 self.pin_protocol
503 .get_or_insert_with(|| PinProtocolState::new(trussed))
504 }
505
506 pub fn reset<T: CryptoClient + P256>(&mut self, trussed: &mut T) {
507 syscall!(trussed.delete_all(Location::Volatile));
509 self.clear_credential_cache();
510 self.active_get_assertion = None;
511
512 if let Some(pin_protocol) = self.pin_protocol.take() {
513 pin_protocol.reset(trussed);
514 }
515 self.pin_protocol = Some(PinProtocolState::new(trussed));
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use hex_literal::hex;
524
525 #[test]
526 fn deser() {
527 let _state: PersistentState = trussed::cbor_deserialize(&hex!(
528 "
529 a5726b65795f656e6372797074696f6e5f6b657950b19a5a2845e5ec71e3
530 2a1b890892376c706b65795f7772617070696e675f6b6579f6781a636f6e
531 73656375746976655f70696e5f6d69736d617463686573006870696e5f68
532 6173689018ef1879187c1881181818f0182d18fb186418960718dd185d18
533 3f188c18766974696d657374616d7009
534 "
535 ))
536 .unwrap();
537 }
538}