fido_authenticator/
state.rs

1//! Various state of the authenticator.
2//!
3//! Needs cleanup.
4
5pub mod migrate;
6
7use core::num::NonZeroU32;
8
9use ctap_types::{
10    ctap2::AttestationFormatsPreference,
11    // 2022-02-27: 10 credentials
12    sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently
13    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    // PathBuf has length 255 + 1, we only need 36 + 1
36    // with `rk/<16B rp_id>/<16B cred_id>` = 4 + 2*32
37    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).ok();
60        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    /// Batch device identity (aaguid, certificate, key).
85    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(trussed: &mut TrussedClient) -> Self {
98    pub fn new() -> Self {
99        // let identity = Identity::get(trussed);
100        let identity = Default::default();
101        let runtime: RuntimeState = Default::default();
102        // let persistent = PersistentState::load_or_reset(trussed);
103        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/// Batch device identity (aaguid, certificate, key).
137#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct Identity {
139    // can this be [u8; 16] or need Bytes for serialization?
140    // aaguid: Option<Bytes<consts::U16>>,
141    attestation_key: Option<KeyId>,
142}
143
144pub type Aaguid = [u8; 16];
145pub type Certificate = trussed_core::types::Message;
146
147impl Identity {
148    // Attempt to yank out the aaguid of a certificate.
149    fn yank_aaguid(&mut self, der: &[u8]) -> Option<[u8; 16]> {
150        let aaguid_start_sequence = [
151            // OBJECT IDENTIFIER 1.3.6.1.4.1.45724.1.1.4 (AAGUID)
152            0x06u8, 0x0B, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x01, 0x01, 0x04,
153            // Sequence, 16 bytes
154            0x04, 0x12, 0x04, 0x10,
155        ];
156
157        // Scan for the beginning sequence for AAGUID.
158        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    /// Lookup batch key and certificate, together with AAUGID.
179    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            // Will panic if certificate does not exist.
187            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                // Provide a default
194                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    // both of these are a cache for previous Get{Next,}Assertion call
234    cached_credentials: CredentialCache,
235    pub active_get_assertion: Option<ActiveGetAssertionData>,
236    pub cached_rp: Option<CredentialManagementEnumerateRps>,
237    pub cached_rk: Option<CredentialManagementEnumerateCredentials>,
238
239    // largeBlob command
240    pub large_blobs: ctap2::large_blobs::State,
241}
242
243// TODO: Plan towards future extensibility
244//
245// - if we set all fields as optional, and annotate with `skip_serializing if None`,
246// then, missing fields in older fw versions should not cause problems with newer fw
247// versions that potentially add new fields.
248//
249// - empirically, the implementation of Deserialize doesn't seem to mind moving around
250// the order of fields, which is already nice
251//
252// - adding new non-optional fields definitely doesn't parse (but maybe it could?)
253// - same for removing a field
254// Currently, this causes the entire authnr to reset state. Maybe it should even reformat disk
255//
256// - An alternative would be `heapless::Map`, but I'd prefer something more typed.
257#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Default)]
258pub struct PersistentState {
259    #[serde(skip)]
260    // TODO: there has to be a better way than.. this
261    // Pro-tip: it should involve types ^^
262    //
263    // We could alternatively make all methods take a TrussedClient as parameter
264    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    // Ideally, we'd dogfood a "Monotonic Counter" from trussed.
272    // TODO: Add per-key counters for resident keys.
273    // counter: Option<CounterId>,
274    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        // TODO: add "exists_file" method instead?
283        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        // error to call before initialization
424        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 cached_credentials(&mut self) -> &mut CredentialCache {
467    //     &mut self.cached_credentials
468    //     // if let Some(cache) = self.cached_credentials.as_mut() {
469    //     //     return cache
470    //     // }
471    //     // self.cached_credentials.insert(CredentialCache::new())
472    // }
473
474    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        // Could use `free_credential_heap`, but since we're deleting everything here, this is quicker.
508        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        // to speed up future operations, we already generate the key agreement key
516        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}