trussed_auth/
lib.rs

1// Copyright (C) Nitrokey GmbH
2// SPDX-License-Identifier: Apache-2.0 or MIT
3
4#![cfg_attr(not(test), no_std)]
5#![warn(
6    missing_debug_implementations,
7    missing_docs,
8    non_ascii_idents,
9    trivial_casts,
10    unused,
11    unused_qualifications,
12    clippy::expect_used,
13    clippy::unwrap_used
14)]
15#![deny(unsafe_code)]
16
17//! A Trussed API extension for authentication.
18//!
19//! This crate contains an API extension for [Trussed][], [`AuthExtension`][].  The extension
20//! currently provides basic PIN handling with retry counters.  Applications can access it using
21//! the [`AuthClient`][] trait.
22//!
23//! # Examples
24//!
25//! ```
26//! use heapless_bytes::Bytes;
27//! use trussed_auth::{AuthClient, PinId};
28//! use trussed_core::syscall;
29//!
30//! #[repr(u8)]
31//! enum Pin {
32//!     User = 0,
33//! }
34//!
35//! impl From<Pin> for PinId {
36//!     fn from(pin: Pin) -> Self {
37//!         (pin as u8).into()
38//!     }
39//! }
40//!
41//! fn authenticate_user<C: AuthClient>(client: &mut C, pin: Option<&[u8]>) -> bool {
42//!     if !syscall!(client.has_pin(Pin::User)).has_pin {
43//!         // no PIN set
44//!         return true;
45//!     }
46//!     let Some(pin) = pin else {
47//!         // PIN is set but not provided
48//!         return false;
49//!     };
50//!     let Ok(pin) = Bytes::from_slice(pin) else {
51//!         // provided PIN is too long
52//!         return false;
53//!     };
54//!     // check PIN
55//!     syscall!(client.check_pin(Pin::User, pin)).success
56//! }
57//! ```
58//!
59//! [Trussed]: https://docs.rs/trussed
60
61#[allow(missing_docs)]
62pub mod reply;
63#[allow(missing_docs)]
64pub mod request;
65
66use core::str::FromStr;
67
68use serde::{Deserialize, Serialize};
69use trussed_core::{
70    config::MAX_SHORT_DATA_LENGTH,
71    serde_extensions::{Extension, ExtensionClient, ExtensionResult},
72    types::{Bytes, KeyId, Message, PathBuf},
73};
74
75/// The maximum length of a PIN.
76pub const MAX_PIN_LENGTH: usize = MAX_SHORT_DATA_LENGTH;
77
78/// A PIN.
79pub type Pin = Bytes<MAX_PIN_LENGTH>;
80
81/// The ID of a PIN within the namespace of a client.
82///
83/// It is recommended that applications use an enum that implements `Into<PinId>`.
84///
85/// # Examples
86///
87/// ```
88/// use trussed_auth::PinId;
89///
90/// #[repr(u8)]
91/// enum Pin {
92///     User = 0,
93///     Admin = 1,
94///     ResetCode = 2,
95/// }
96///
97/// impl From<Pin> for PinId {
98///     fn from(pin: Pin) -> Self {
99///         (pin as u8).into()
100///     }
101/// }
102/// ```
103#[derive(
104    Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
105)]
106pub struct PinId(u8);
107
108/// Error obtained when trying to parse a [`PinId`][] either through [`PinId::from_path`][] or through the [`FromStr`][] implementation.
109#[derive(
110    Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
111)]
112pub struct PinIdFromStrError;
113
114impl PinId {
115    /// Get the path to the PIN id.
116    ///
117    /// Path are of the form `pin.XX` where `xx` is the hexadecimal representation of the PIN number.
118    pub fn path(&self) -> PathBuf {
119        let mut path = [0; 6];
120        path[0..4].copy_from_slice(b"pin.");
121        path[4..].copy_from_slice(&self.hex());
122
123        // path has only ASCII characters and is not too long
124        #[allow(clippy::unwrap_used)]
125        PathBuf::try_from(&path).ok().unwrap()
126    }
127
128    /// Get the hex representation of the PIN id
129    pub fn hex(&self) -> [u8; 2] {
130        const CHARS: &[u8; 16] = b"0123456789abcdef";
131        [
132            CHARS[usize::from(self.0 >> 4)],
133            CHARS[usize::from(self.0 & 0xf)],
134        ]
135    }
136
137    /// Parse a PinId path
138    pub fn from_path(path: &str) -> Result<Self, PinIdFromStrError> {
139        let path = path.strip_prefix("pin.").ok_or(PinIdFromStrError)?;
140        if path.len() != 2 {
141            return Err(PinIdFromStrError);
142        }
143
144        let id = u8::from_str_radix(path, 16).map_err(|_| PinIdFromStrError)?;
145        Ok(PinId(id))
146    }
147}
148
149impl From<u8> for PinId {
150    fn from(id: u8) -> Self {
151        Self(id)
152    }
153}
154
155impl From<PinId> for u8 {
156    fn from(id: PinId) -> Self {
157        id.0
158    }
159}
160
161impl FromStr for PinId {
162    type Err = PinIdFromStrError;
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        Self::from_path(s)
165    }
166}
167
168/// A result returned by [`AuthClient`][].
169pub type AuthResult<'a, R, C> = ExtensionResult<'a, AuthExtension, R, C>;
170
171/// An extension that provides basic PIN handling.
172///
173/// See [`AuthClient`][] for the requests provided by the extension.
174#[derive(Debug, Default)]
175pub struct AuthExtension;
176
177impl Extension for AuthExtension {
178    type Request = AuthRequest;
179    type Reply = AuthReply;
180}
181
182#[allow(clippy::large_enum_variant)]
183#[derive(Debug, Deserialize, Serialize)]
184#[allow(missing_docs)]
185pub enum AuthRequest {
186    HasPin(request::HasPin),
187    CheckPin(request::CheckPin),
188    GetPinKey(request::GetPinKey),
189    GetApplicationKey(request::GetApplicationKey),
190    SetPin(request::SetPin),
191    SetPinWithKey(request::SetPinWithKey),
192    ChangePin(request::ChangePin),
193    DeletePin(request::DeletePin),
194    DeleteAllPins(request::DeleteAllPins),
195    PinRetries(request::PinRetries),
196    ResetAppKeys(request::ResetAppKeys),
197    ResetAuthData(request::ResetAuthData),
198}
199
200#[derive(Debug, Deserialize, Serialize)]
201#[allow(missing_docs)]
202pub enum AuthReply {
203    HasPin(reply::HasPin),
204    CheckPin(reply::CheckPin),
205    GetPinKey(reply::GetPinKey),
206    GetApplicationKey(reply::GetApplicationKey),
207    SetPin(reply::SetPin),
208    SetPinWithKey(reply::SetPinWithKey),
209    ChangePin(reply::ChangePin),
210    DeletePin(reply::DeletePin),
211    DeleteAllPins(reply::DeleteAllPins),
212    PinRetries(reply::PinRetries),
213    ResetAppKeys(reply::ResetAppKeys),
214    ResetAuthData(reply::ResetAuthData),
215}
216
217/// Provides access to the [`AuthExtension`][].
218///
219/// The extension manages PINs identified by a [`PinId`][] within the namespace of this client.
220/// PINs can have a retry counter.  If a retry counter is configured when setting a PIN, it is
221/// decremented on every failed authentication attempt.  If the counter reaches zero, all further
222/// authentication attempts fail until the PIN is reset.
223///
224/// The extension does not enforce any constraints on the PINs (except for the maximum length, see
225/// [`MAX_PIN_LENGTH`][]).  Even empty PINs can be used.  Also, there is no authentication required
226/// to set, reset or delete a PIN.  It is up to the application to enforce any policies and
227/// constraints.
228///
229/// [`MAX_PIN_LENGTH`]: `crate::MAX_PIN_LENGTH`
230pub trait AuthClient: ExtensionClient<AuthExtension> {
231    /// Returns true if the PIN is set.
232    fn has_pin<I: Into<PinId>>(&mut self, id: I) -> AuthResult<'_, reply::HasPin, Self> {
233        self.extension(request::HasPin { id: id.into() })
234    }
235
236    /// Returns true if the provided PIN is correct and not blocked.
237    ///
238    /// If the PIN is not correct and a retry counter is configured, the counter is decremented.
239    /// Once it reaches zero, authentication attempts for that PIN fail.  If the PIN with the given
240    /// ID is not set, an error is returned.
241    fn check_pin<I>(&mut self, id: I, pin: Pin) -> AuthResult<'_, reply::CheckPin, Self>
242    where
243        I: Into<PinId>,
244    {
245        self.extension(request::CheckPin { id: id.into(), pin })
246    }
247
248    /// Returns a keyid if the provided PIN is correct and not blocked.
249    ///
250    /// The pin must have been created with `derive_key` set to true.
251    /// If the PIN is not correct and a retry counter is configured, the counter is decremented.
252    /// Once it reaches zero, authentication attempts for that PIN fail.  If the PIN with the given
253    /// ID is not set, an error is returned.
254    fn get_pin_key<I>(&mut self, id: I, pin: Pin) -> AuthResult<'_, reply::GetPinKey, Self>
255    where
256        I: Into<PinId>,
257    {
258        self.extension(request::GetPinKey { id: id.into(), pin })
259    }
260
261    /// Sets the given PIN and resets its retry counter.
262    ///
263    /// If the retry counter is `None`, the number of retries is not limited and the PIN will never
264    /// be blocked.
265    fn set_pin<I: Into<PinId>>(
266        &mut self,
267        id: I,
268        pin: Pin,
269        retries: Option<u8>,
270        derive_key: bool,
271    ) -> AuthResult<'_, reply::SetPin, Self> {
272        self.extension(request::SetPin {
273            id: id.into(),
274            pin,
275            retries,
276            derive_key,
277        })
278    }
279
280    /// Set a pin, resetting its retry counter and setting the key to be wrapped
281    ///
282    /// Similar to [`set_pin`](AuthClient::set_pin), but allows the key that the pin will unwrap to be configured.
283    /// Currently only symmetric 256 bit keys are accepted. This method should be used only with keys that were obtained through [`get_pin_key`](AuthClient::get_pin_key)
284    /// This allows for example backing up the key for a pin, to be able to restore it from another source.
285    fn set_pin_with_key<I: Into<PinId>>(
286        &mut self,
287        id: I,
288        pin: Pin,
289        retries: Option<u8>,
290        key: KeyId,
291    ) -> AuthResult<'_, reply::SetPinWithKey, Self> {
292        self.extension(request::SetPinWithKey {
293            id: id.into(),
294            pin,
295            retries,
296            key,
297        })
298    }
299
300    /// Change the given PIN and resets its retry counter.
301    ///
302    /// The key obtained by [`get_pin_key`](AuthClient::get_pin_key) will stay the same
303    fn change_pin<I: Into<PinId>>(
304        &mut self,
305        id: I,
306        old_pin: Pin,
307        new_pin: Pin,
308    ) -> AuthResult<'_, reply::ChangePin, Self> {
309        self.extension(request::ChangePin {
310            id: id.into(),
311            old_pin,
312            new_pin,
313        })
314    }
315
316    /// Deletes the given PIN (if it exists).
317    fn delete_pin<I: Into<PinId>>(&mut self, id: I) -> AuthResult<'_, reply::DeletePin, Self> {
318        self.extension(request::DeletePin { id: id.into() })
319    }
320
321    /// Deletes all PINs for this client.
322    fn delete_all_pins(&mut self) -> AuthResult<'_, reply::DeleteAllPins, Self> {
323        self.extension(request::DeleteAllPins)
324    }
325
326    /// Returns the remaining retries for the given PIN.
327    fn pin_retries<I: Into<PinId>>(&mut self, id: I) -> AuthResult<'_, reply::PinRetries, Self> {
328        self.extension(request::PinRetries { id: id.into() })
329    }
330
331    /// Returns a keyid that is persistent given the "info" parameter
332    fn get_application_key(
333        &mut self,
334        info: Message,
335    ) -> AuthResult<'_, reply::GetApplicationKey, Self> {
336        self.extension(request::GetApplicationKey { info })
337    }
338
339    /// Delete all application keys
340    fn reset_app_keys(&mut self) -> AuthResult<'_, reply::ResetAppKeys, Self> {
341        self.extension(request::ResetAppKeys {})
342    }
343
344    /// Combines [`reset_app_keys`][AuthClient::reset_app_keys] and [`delete_all_pins`](AuthClient::delete_all_pins)
345    fn reset_auth_data(&mut self) -> AuthResult<'_, reply::ResetAuthData, Self> {
346        self.extension(request::ResetAuthData {})
347    }
348}
349
350impl<C: ExtensionClient<AuthExtension>> AuthClient for C {}
351
352#[cfg(test)]
353mod tests {
354    use super::PinId;
355    use trussed_core::types::PathBuf;
356
357    #[test]
358    fn pin_id_path() {
359        for i in 0..=u8::MAX {
360            assert_eq!(Ok(PinId(i)), PinId::from_path(PinId(i).path().as_ref()));
361            let actual = PinId(i).path();
362            #[allow(clippy::unwrap_used)]
363            let expected = PathBuf::try_from(format!("pin.{i:02x}").as_str()).unwrap();
364            println!("id: {i}, actual: {actual}, expected: {expected}");
365            assert_eq!(actual, expected);
366        }
367    }
368}