fido_authenticator/
lib.rs

1//! Open source reference implementation of FIDO CTAP.
2//!
3//! The core structure is [`Authenticator`], a Trussed® application.
4//!
5//! It implements the [`ctap_types::ctap1::Authenticator`] and [`ctap_types::ctap2::Authenticator`] traits,
6//! which express the interface defined in the CTAP specification.
7//!
8//! With feature `dispatch` activated, it also implements the `App` traits
9//! of [`apdu_dispatch`] and [`ctaphid_dispatch`].
10//!
11//! [`apdu_dispatch`]: https://docs.rs/apdu-dispatch
12//! [`ctaphid_dispatch`]: https://docs.rs/ctaphid-dispatch
13
14#![cfg_attr(not(test), no_std)]
15// #![warn(missing_docs)]
16
17#[macro_use]
18extern crate delog;
19generate_macros!();
20
21pub use state::migrate;
22
23use core::time::Duration;
24
25use trussed_core::{
26    mechanisms, syscall,
27    types::{
28        KeyId, KeySerialization, Location, Mechanism, SerializedKey, Signature,
29        SignatureSerialization, StorageAttributes,
30    },
31    CertificateClient, CryptoClient, FilesystemClient, ManagementClient, UiClient,
32};
33use trussed_fs_info::{FsInfoClient, FsInfoReply};
34use trussed_hkdf::HkdfClient;
35
36/// Re-export of `ctap-types` authenticator errors.
37pub use ctap_types::Error;
38
39mod ctap1;
40mod ctap2;
41
42#[cfg(feature = "dispatch")]
43mod dispatch;
44
45pub mod constants;
46pub mod credential;
47pub mod state;
48
49pub use ctap2::large_blobs::Config as LargeBlobsConfig;
50
51/// Results with our [`Error`].
52pub type Result<T> = core::result::Result<T, Error>;
53
54/// Trait bound on our implementation's requirements from a Trussed client.
55///
56/// - Client is core Trussed client functionality.
57/// - Ed25519 and P-256 are the core signature algorithms.
58/// - AES-256, SHA-256 and its HMAC are used within the CTAP protocols.
59/// - ChaCha8Poly1305 is our AEAD of choice, used e.g. for the key handles.
60/// - Some Trussed extensions might be required depending on the activated features, see
61///   [`ExtensionRequirements`][].
62pub trait TrussedRequirements:
63    CertificateClient
64    + CryptoClient
65    + FilesystemClient
66    + ManagementClient
67    + UiClient
68    + mechanisms::P256
69    + mechanisms::Chacha8Poly1305
70    + mechanisms::Aes256Cbc
71    + mechanisms::Sha256
72    + mechanisms::HmacSha256
73    + mechanisms::Ed255
74    + FsInfoClient
75    + HkdfClient
76    + ExtensionRequirements
77{
78}
79
80impl<T> TrussedRequirements for T where
81    T: CertificateClient
82        + CryptoClient
83        + FilesystemClient
84        + ManagementClient
85        + UiClient
86        + mechanisms::P256
87        + mechanisms::Chacha8Poly1305
88        + mechanisms::Aes256Cbc
89        + mechanisms::Sha256
90        + mechanisms::HmacSha256
91        + mechanisms::Ed255
92        + FsInfoClient
93        + HkdfClient
94        + ExtensionRequirements
95{
96}
97
98#[cfg(not(feature = "chunked"))]
99pub trait ExtensionRequirements {}
100
101#[cfg(not(feature = "chunked"))]
102impl<T> ExtensionRequirements for T {}
103
104#[cfg(feature = "chunked")]
105pub trait ExtensionRequirements: trussed_chunked::ChunkedClient {}
106
107#[cfg(feature = "chunked")]
108impl<T> ExtensionRequirements for T where T: trussed_chunked::ChunkedClient {}
109
110#[derive(Copy, Clone, Debug, Eq, PartialEq)]
111/// Externally defined configuration.
112pub struct Config {
113    /// Typically determined by surrounding USB-level decoder.
114    /// For Solo 2, this is usbd-ctaphid (and its buffer size).
115    pub max_msg_size: usize,
116    // pub max_creds_in_list: usize,
117    // pub max_cred_id_length: usize,
118    /// If set, the first Get Assertion or Authenticate request within the specified time after
119    /// boot is accepted without additional user presence verification.
120    pub skip_up_timeout: Option<Duration>,
121    /// The maximum number of resident credentials.
122    pub max_resident_credential_count: Option<u32>,
123    /// Configuration for the largeBlobKey extension and the largeBlobs command.
124    ///
125    /// If this is `None`, the extension and the command are disabled.
126    pub large_blobs: Option<ctap2::large_blobs::Config>,
127    /// Whether the authenticator supports the NFC transport.
128    pub nfc_transport: bool,
129}
130
131impl Config {
132    pub fn supports_large_blobs(&self) -> bool {
133        self.large_blobs.is_some()
134    }
135}
136
137// impl Default for Config {
138//     fn default() -> Self {
139//         Self {
140//             max_message_size: ctap_types::sizes::REALISTIC_MAX_MESSAGE_SIZE,
141//             max_credential_count_in_list: ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST,
142//             max_credential_id_length: ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH,
143//         }
144//     }
145// }
146
147/// Trussed® app implementing a FIDO authenticator.
148///
149/// It implements the [`ctap_types::ctap1::Authenticator`] and [`ctap_types::ctap2::Authenticator`] traits,
150/// which, in turn, express the interfaces defined in the CTAP specification.
151///
152/// The type parameter `T` selects a Trussed® client implementation, which
153/// must meet the [`TrussedRequirements`] in our implementation.
154///
155/// NB: `T` should be the first parameter, `UP` should default to `Conforming`,
156/// and probably `UP` shouldn't be a generic parameter at all, at least not this kind.
157pub struct Authenticator<UP, T>
158// TODO: changing the order is breaking, but default generic parameters must be trailing.
159// pub struct Authenticator<T, UP=Conforming>
160where
161    UP: UserPresence,
162{
163    trussed: T,
164    state: state::State,
165    up: UP,
166    config: Config,
167}
168
169// EWW.. this is a bit unsafe isn't it
170fn format_hex<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str {
171    const HEX_CHARS: &[u8] = b"0123456789abcdef";
172    assert!(data.len() * 2 >= buffer.len());
173    for (idx, byte) in data.iter().enumerate() {
174        buffer[idx * 2] = HEX_CHARS[(byte >> 4) as usize];
175        buffer[idx * 2 + 1] = HEX_CHARS[(byte & 0xf) as usize];
176    }
177
178    // SAFETY: we just added only ascii chars to buffer from 0 to data.len() - 1
179    unsafe { core::str::from_utf8_unchecked(&buffer[0..data.len() * 2]) }
180}
181
182// NB: to actually use this, replace the constant implementation with the inline assembly.
183// Once we move to a new cortex-m release, can use the version from there.
184//
185// use core::arch::asm;
186
187// #[inline]
188// pub fn msp() -> u32 {
189//     let r;
190//     unsafe { asm!("mrs {}, MSP", out(reg) r, options(nomem, nostack, preserves_flags)) };
191//     r
192// }
193
194#[inline]
195#[allow(dead_code)]
196pub(crate) fn msp() -> u32 {
197    0x2000_0000
198}
199
200/// Currently Ed25519 and P256.
201#[derive(Copy, Clone, Debug, Eq, PartialEq)]
202#[repr(i32)]
203#[non_exhaustive]
204pub enum SigningAlgorithm {
205    /// The Ed25519 signature algorithm.
206    Ed25519 = -8,
207    /// The NIST P-256 signature algorithm.
208    P256 = -7,
209}
210
211impl SigningAlgorithm {
212    pub fn mechanism(&self) -> Mechanism {
213        match self {
214            Self::Ed25519 => Mechanism::Ed255,
215            Self::P256 => Mechanism::P256,
216        }
217    }
218
219    pub fn signature_serialization(&self) -> SignatureSerialization {
220        match self {
221            Self::Ed25519 => SignatureSerialization::Raw,
222            Self::P256 => SignatureSerialization::Asn1Der,
223        }
224    }
225
226    pub fn generate_private_key<C: CryptoClient>(
227        &self,
228        trussed: &mut C,
229        location: Location,
230    ) -> KeyId {
231        syscall!(trussed.generate_key(
232            self.mechanism(),
233            StorageAttributes::new().set_persistence(location)
234        ))
235        .key
236    }
237
238    pub fn derive_public_key<C: CryptoClient>(
239        &self,
240        trussed: &mut C,
241        private_key: KeyId,
242    ) -> SerializedKey {
243        let mechanism = self.mechanism();
244        let public_key = syscall!(trussed.derive_key(
245            mechanism,
246            private_key,
247            None,
248            StorageAttributes::new().set_persistence(Location::Volatile)
249        ))
250        .key;
251        let cose_public_key =
252            syscall!(trussed.serialize_key(mechanism, public_key, KeySerialization::Cose))
253                .serialized_key;
254        if !syscall!(trussed.delete(public_key)).success {
255            error!("failed to delete credential public key");
256        }
257        cose_public_key
258    }
259
260    pub fn sign<C: CryptoClient>(&self, trussed: &mut C, key: KeyId, data: &[u8]) -> Signature {
261        syscall!(trussed.sign(self.mechanism(), key, data, self.signature_serialization()))
262            .signature
263    }
264}
265
266impl From<SigningAlgorithm> for i32 {
267    fn from(alg: SigningAlgorithm) -> Self {
268        match alg {
269            SigningAlgorithm::P256 => -7,
270            SigningAlgorithm::Ed25519 => -8,
271        }
272    }
273}
274
275impl TryFrom<i32> for SigningAlgorithm {
276    type Error = Error;
277
278    fn try_from(alg: i32) -> Result<Self> {
279        Ok(match alg {
280            -7 => SigningAlgorithm::P256,
281            -8 => SigningAlgorithm::Ed25519,
282            _ => return Err(Error::UnsupportedAlgorithm),
283        })
284    }
285}
286
287/// Method to check for user presence.
288pub trait UserPresence: Copy {
289    fn user_present<T: TrussedRequirements>(
290        self,
291        trussed: &mut T,
292        timeout_milliseconds: u32,
293    ) -> Result<()>;
294}
295
296#[deprecated(note = "use `Silent` directly`")]
297#[doc(hidden)]
298pub type SilentAuthenticator = Silent;
299
300/// No user presence verification.
301#[derive(Copy, Clone)]
302pub struct Silent {}
303
304impl UserPresence for Silent {
305    fn user_present<T: TrussedRequirements>(self, _: &mut T, _: u32) -> Result<()> {
306        Ok(())
307    }
308}
309
310#[deprecated(note = "use `Conforming` directly")]
311#[doc(hidden)]
312pub type NonSilentAuthenticator = Conforming;
313
314/// User presence verification via Trussed.
315#[derive(Copy, Clone)]
316pub struct Conforming {}
317
318impl UserPresence for Conforming {
319    fn user_present<T: TrussedRequirements>(
320        self,
321        trussed: &mut T,
322        timeout_milliseconds: u32,
323    ) -> Result<()> {
324        let result = syscall!(trussed.confirm_user_present(timeout_milliseconds)).result;
325        result.map_err(|err| match err {
326            trussed_core::types::consent::Error::TimedOut => Error::UserActionTimeout,
327            trussed_core::types::consent::Error::Interrupted => Error::KeepaliveCancel,
328            _ => Error::OperationDenied,
329        })
330    }
331}
332
333impl<UP, T> Authenticator<UP, T>
334where
335    UP: UserPresence,
336    T: TrussedRequirements,
337{
338    pub fn new(trussed: T, up: UP, config: Config) -> Self {
339        let state = state::State::new();
340        Self {
341            trussed,
342            state,
343            up,
344            config,
345        }
346    }
347
348    fn estimate_remaining_inner(info: &FsInfoReply) -> Option<u32> {
349        let block_size = info.block_info.as_ref()?.size;
350        // 1 block for the directory, 1 for the private key, 400 bytes for a reasonnable key and metadata
351        let size_taken = 2 * block_size + 400;
352        // Remove 5 block kept as buffer
353        Some((info.available_space.saturating_sub(5 * block_size) / size_taken) as u32)
354    }
355
356    fn estimate_remaining(&mut self) -> Option<u32> {
357        let info = syscall!(self.trussed.fs_info(Location::Internal));
358        debug!("Got filesystem info: {info:?}");
359        Self::estimate_remaining_inner(&info)
360    }
361
362    fn can_fit_inner(info: &FsInfoReply, size: usize) -> Option<bool> {
363        let block_size = info.block_info.as_ref()?.size;
364        // 1 block for the rp directory, 5 block of margin, 50 bytes for a reasonnable metadata
365        let size_taken = 6 * block_size + size + 50;
366        Some(size_taken < info.available_space)
367    }
368
369    /// Can a credential of size `size` be stored with safe margins
370    ///
371    /// This assumes that the key has already been generated and is stored.
372    fn can_fit(&mut self, size: usize) -> Option<bool> {
373        debug!("Can fit for {size} bytes");
374        let info = syscall!(self.trussed.fs_info(Location::Internal));
375        debug!("Got filesystem info: {info:?}");
376        debug!(
377            "Available storage: {:?}",
378            Self::estimate_remaining_inner(&info)
379        );
380        Self::can_fit_inner(&info, size)
381    }
382
383    fn hash(&mut self, data: &[u8]) -> [u8; 32] {
384        let hash = syscall!(self.trussed.hash_sha256(data)).hash;
385        hash.as_slice().try_into().expect("hash should fit")
386    }
387
388    fn nonce(&mut self) -> [u8; 12] {
389        let bytes = syscall!(self.trussed.random_bytes(12)).bytes;
390        bytes.as_slice().try_into().expect("hash should fit")
391    }
392
393    fn skip_up_check(&mut self) -> bool {
394        // If enabled in the configuration, we don't require an additional user presence
395        // verification for a certain duration after boot.
396        if let Some(timeout) = self.config.skip_up_timeout.take() {
397            let uptime = syscall!(self.trussed.uptime()).uptime;
398            if uptime < timeout {
399                info_now!("skip up check directly after boot");
400                return true;
401            }
402        }
403        false
404    }
405}
406
407#[cfg(test)]
408mod test {
409    use super::*;
410
411    #[test]
412    fn hex() {
413        let data = [0x01, 0x02, 0xB1, 0xA1];
414        let buffer = &mut [0; 8];
415        assert_eq!(format_hex(&data, buffer), "0102b1a1");
416        assert_eq!(buffer, b"0102b1a1");
417    }
418}