Split backend and extension

This patch splits the trussed-auth crate into two crates:  trussed-auth
only defines the AuthExtension and can be used by clients or other
backends implementing the extension.  trussed-auth-backend contains the
AuthBackend that implements the extension using the filesystem.
This commit is contained in:
Robin Krahl
2025-01-21 21:08:09 +01:00
parent fc53539536
commit 2fc6856270
15 changed files with 310 additions and 248 deletions
+10 -26
View File
@@ -1,38 +1,22 @@
# Copyright (C) Nitrokey GmbH
# SPDX-License-Identifier: CC0-1.0
[package]
name = "trussed-auth"
version = "0.3.0"
[workspace]
members = ["backend", "extension"]
resolver = "2"
[workspace.package]
authors = ["Nitrokey GmbH <info@nitrokey.com>"]
edition = "2021"
repository = "https://github.com/trussed-dev/trussed-auth"
license = "Apache-2.0 OR MIT"
description = "Authentication extension and backend for Trussed"
repository = "https://github.com/trussed-dev/trussed-auth"
[dependencies]
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["reduced-round"] }
hkdf = "0.12.3"
hmac = "0.12.1"
rand_core = "0.6.4"
[workspace.dependencies]
serde = { version = "1", default-features = false }
serde-byte-array = "0.1.2"
sha2 = { version = "0.10.6", default-features = false }
subtle = { version = "2.4.1", default-features = false }
trussed = { version = "0.1.0", default-features = false, features = ["serde-extensions"] }
trussed-core = { version = "0.1.0-rc.1", features = ["serde-extensions"] }
littlefs2-core = "0.1.0"
[dev-dependencies]
quickcheck = { version = "1.0.3", default-features = false }
rand_core = { version = "0.6.4", default-features = false, features = ["getrandom"] }
serde_test = "1.0.176"
trussed = { version = "0.1.0", default-features = false, features = ["clients-1", "crypto-client", "filesystem-client", "hmac-sha256", "serde-extensions", "virt"] }
admin-app = { version = "0.1.0", features = ["migration-tests"] }
serde_cbor = { version = "0.11.2", features = ["std"] }
hex-literal = "0.4.1"
[patch.crates-io]
trussed-auth = { path = "extension" }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" }
trussed-manage = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "9355f700831c1a278c334f76382fbf98d82aedcd" }
admin-app = { git = "https://github.com/Nitrokey/admin-app.git", branch = "ctaphid-app" }
admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.19" }
+5 -5
View File
@@ -3,18 +3,18 @@
.PHONY: check
check:
RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets
RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets --workspace
.PHONY: lint
lint:
cargo clippy --all-features --all-targets -- --deny warnings
cargo fmt -- --check
RUSTDOCFLAGS='-Dwarnings' cargo doc --no-deps
cargo clippy --all-features --all-targets --workspace -- --deny warnings
cargo fmt --all -- --check
RUSTDOCFLAGS='-Dwarnings' cargo doc --no-deps --workspace
reuse lint
.PHONY: test
test:
cargo test --all-features
cargo test --all-features --workspace
.PHONY: ci
ci: check lint test
+6 -2
View File
@@ -5,10 +5,14 @@ SPDX-License-Identifier: CC0-1.0
# trussed-auth
`trussed-auth` is an extension and custom backend for [Trussed][] that provides
basic PIN handling.
`trussed-auth` is an extension for [Trussed][] that provides basic PIN
handling. `trussed-auth-backend` is a Trussed backend implementing that
extension using the filesystem. Other implementations are provided by these
backends:
- [`trussed-se050-backend`][]
[Trussed]: https://github.com/trussed-dev/trussed
[`trussed-se050-backend`]: https://github.com/Nitrokey/trussed-se050-backend
## License
+23
View File
@@ -0,0 +1,23 @@
<!--
Copyright (C) Nitrokey GmbH
SPDX-License-Identifier: CC0-1.0
-->
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
Extracted from `trussed-auth` v0.3.0.
### Breaking Changes
- Use serde(rename) to save space on on the size of stored credentials ([#38][])
- Remove the `dat` intermediary directory in file storage ([#39][])
- Use `trussed-core` and remove default features for `trussed`
[#38]: https://github.com/trussed-dev/trussed-auth/pull/38
[#39]: https://github.com/trussed-dev/trussed-auth/pull/39
+35
View File
@@ -0,0 +1,35 @@
# Copyright (C) Nitrokey GmbH
# SPDX-License-Identifier: CC0-1.0
[package]
name = "trussed-auth-backend"
version = "0.1.0"
description = "Authentication backend for Trussed"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde.workspace = true
trussed-core.workspace = true
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["reduced-round"] }
hkdf = "0.12.3"
hmac = "0.12.1"
littlefs2-core = "0.1"
rand_core = "0.6.4"
serde-byte-array = "0.1.2"
sha2 = { version = "0.10.6", default-features = false }
subtle = { version = "2.4.1", default-features = false }
trussed = { version = "0.1.0", default-features = false, features = ["serde-extensions"] }
trussed-auth = { version = "0.3.0" }
[dev-dependencies]
admin-app = { version = "0.1.0", features = ["migration-tests"] }
hex-literal = "0.4.1"
quickcheck = { version = "1.0.3", default-features = false }
rand_core = { version = "0.6.4", default-features = false, features = ["getrandom"] }
serde_cbor = { version = "0.11.2", features = ["std"] }
serde_test = "1.0.176"
trussed = { version = "0.1.0", default-features = false, features = ["clients-1", "crypto-client", "filesystem-client", "hmac-sha256", "serde-extensions", "virt"] }
+1 -1
View File
@@ -18,7 +18,7 @@ use trussed::{
};
use super::Error;
use crate::{Pin, PinId, MAX_PIN_LENGTH};
use trussed_auth::{Pin, PinId, MAX_PIN_LENGTH};
const APP_SALT_PATH: &Path = path!("application_salt");
+25 -9
View File
@@ -1,8 +1,28 @@
// Copyright (C) Nitrokey GmbH
// SPDX-License-Identifier: Apache-2.0 or MIT
#![no_std]
#![warn(
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
unused,
unused_qualifications,
clippy::expect_used,
clippy::unwrap_used
)]
#![deny(unsafe_code)]
//! A Trussed backend implementing the [`AuthExtension`][].
//!
//! [`AuthBackend`][] is an implementation of the [`AuthExtension`][] that stores PINs in the
//! filesystem.
mod data;
pub mod migrate;
use core::fmt;
use hkdf::Hkdf;
@@ -20,15 +40,11 @@ use trussed::{
types::{CoreContext, Location},
Bytes,
};
use trussed_auth::{reply, AuthExtension, AuthReply, AuthRequest};
use crate::{
backend::data::{expand_app_key, get_app_salt},
extension::{reply, AuthExtension, AuthReply, AuthRequest},
BACKEND_DIR,
};
use data::{Key, PinData, Salt, KEY_LEN, SALT_LEN};
use data::{delete_app_salt, expand_app_key, get_app_salt, Key, PinData, Salt, KEY_LEN, SALT_LEN};
use self::data::delete_app_salt;
const BACKEND_DIR: &Path = path!("backend-auth");
/// max accepted length for the hardware initial key material
pub const MAX_HW_KEY_LEN: usize = 64;
@@ -116,7 +132,7 @@ impl AuthBackend {
/// Creates a new `AuthBackend` with a missing hw key
///
/// Contrary to [`new`](Self::new) which uses a default `&[]` key, this will make operations depending on the hardware key to fail:
/// - [`set_pin`](crate::AuthClient::set_pin) with `derive_key = true`
/// - [`set_pin`](trussed_auth::AuthClient::set_pin) with `derive_key = true`
/// - All operations on a pin that was created with `derive_key = true`
pub fn with_missing_hw_key(location: Location, layout: FilesystemLayout) -> Self {
Self {
@@ -412,7 +428,7 @@ impl ExtensionImpl<AuthExtension> for AuthBackend {
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Error {
enum Error {
NotFound,
MissingHwKey,
ReadFailed,
+1 -1
View File
@@ -35,7 +35,7 @@ fn migrate_single(fs: &dyn DynFilesystem, path: &Path) -> Result<(), Error> {
///
/// ```rust
///# use littlefs2_core::{DynFilesystem, Error, path};
///# use trussed_auth::migrate::migrate_remove_dat;
///# use trussed_auth_backend::migrate::migrate_remove_dat;
///# fn test(fs: &dyn DynFilesystem) -> Result<(), Error> {
/// migrate_remove_dat(fs, &[path!("secrets"), path!("opcard")])?;
///# Ok(())
+10 -5
View File
@@ -11,7 +11,8 @@ mod dispatch {
service::ServiceResources,
types::{Bytes, Context, Location},
};
use trussed_auth::{AuthBackend, AuthContext, AuthExtension, MAX_HW_KEY_LEN};
use trussed_auth::AuthExtension;
use trussed_auth_backend::{AuthBackend, AuthContext, MAX_HW_KEY_LEN};
pub const BACKENDS: &[BackendId<Backend>] =
&[BackendId::Custom(Backend::Auth), BackendId::Core];
@@ -55,7 +56,10 @@ mod dispatch {
impl Dispatch {
pub fn new() -> Self {
Self {
auth: AuthBackend::new(Location::Internal, trussed_auth::FilesystemLayout::V0),
auth: AuthBackend::new(
Location::Internal,
trussed_auth_backend::FilesystemLayout::V0,
),
}
}
@@ -64,7 +68,7 @@ mod dispatch {
auth: AuthBackend::with_hw_key(
Location::Internal,
hw_key,
trussed_auth::FilesystemLayout::V0,
trussed_auth_backend::FilesystemLayout::V0,
),
}
}
@@ -72,7 +76,7 @@ mod dispatch {
Self {
auth: AuthBackend::with_missing_hw_key(
Location::Internal,
trussed_auth::FilesystemLayout::V0,
trussed_auth_backend::FilesystemLayout::V0,
),
}
}
@@ -136,7 +140,8 @@ use trussed::{
types::{Bytes, Location, Message, PathBuf},
virt::{self, Ram},
};
use trussed_auth::{AuthClient as _, PinId, MAX_HW_KEY_LEN};
use trussed_auth::{AuthClient as _, PinId};
use trussed_auth_backend::MAX_HW_KEY_LEN;
use dispatch::{Backend, Dispatch, BACKENDS};
+5 -7
View File
@@ -11,15 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased][]
- Use serde(rename) to save space on on the size of stored credentials ([#38][])
- Remove the `dat` intermediary directory in file storage ([#39][])
- Use `trussed-core` and remove default features for `trussed`
[#38]: https://github.com/trussed-dev/trussed-auth/pull/38
[#39]: https://github.com/trussed-dev/trussed-auth/pull/39
[Unreleased]: https://github.com/trussed-dev/trussed-auth/compare/v0.3.0...HEAD
### Breaking Changes
- Extract `AuthBackend` into `trussed-auth-backend` crate
- Use `trussed-core` and remove default features for `trussed`
## [0.3.0][] - 2024-03-22
[0.3.0]: https://github.com/trussed-dev/trussed-auth/releases/tag/v0.3.0
+18
View File
@@ -0,0 +1,18 @@
# Copyright (C) Nitrokey GmbH
# SPDX-License-Identifier: CC0-1.0
[package]
name = "trussed-auth"
version = "0.3.0"
description = "Authentication extension for Trussed"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde.workspace = true
trussed-core.workspace = true
[dev-dependencies]
heapless-bytes = "0.3"
+171 -2
View File
@@ -1,18 +1,169 @@
// Copyright (C) Nitrokey GmbH
// SPDX-License-Identifier: Apache-2.0 or MIT
#![cfg_attr(not(test), no_std)]
#![warn(
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
unused,
unused_qualifications,
clippy::expect_used,
clippy::unwrap_used
)]
#![deny(unsafe_code)]
//! A Trussed API extension for authentication.
//!
//! This crate contains an API extension for [Trussed][], [`AuthExtension`][]. The extension
//! currently provides basic PIN handling with retry counters. Applications can access it using
//! the [`AuthClient`][] trait.
//!
//! # Examples
//!
//! ```
//! use heapless_bytes::Bytes;
//! use trussed_auth::{AuthClient, PinId};
//! use trussed_core::syscall;
//!
//! #[repr(u8)]
//! enum Pin {
//! User = 0,
//! }
//!
//! impl From<Pin> for PinId {
//! fn from(pin: Pin) -> Self {
//! (pin as u8).into()
//! }
//! }
//!
//! fn authenticate_user<C: AuthClient>(client: &mut C, pin: Option<&[u8]>) -> bool {
//! if !syscall!(client.has_pin(Pin::User)).has_pin {
//! // no PIN set
//! return true;
//! }
//! let Some(pin) = pin else {
//! // PIN is set but not provided
//! return false;
//! };
//! let Ok(pin) = Bytes::from_slice(pin) else {
//! // provided PIN is too long
//! return false;
//! };
//! // check PIN
//! syscall!(client.check_pin(Pin::User, pin)).success
//! }
//! ```
//!
//! [Trussed]: https://docs.rs/trussed
#[allow(missing_docs)]
pub mod reply;
#[allow(missing_docs)]
pub mod request;
use core::str::FromStr;
use serde::{Deserialize, Serialize};
use trussed_core::{
config::MAX_SHORT_DATA_LENGTH,
serde_extensions::{Extension, ExtensionClient, ExtensionResult},
types::{KeyId, Message},
types::{Bytes, KeyId, Message, PathBuf},
};
use crate::{Pin, PinId};
/// The maximum length of a PIN.
pub const MAX_PIN_LENGTH: usize = MAX_SHORT_DATA_LENGTH;
/// A PIN.
pub type Pin = Bytes<MAX_PIN_LENGTH>;
/// The ID of a PIN within the namespace of a client.
///
/// It is recommended that applications use an enum that implements `Into<PinId>`.
///
/// # Examples
///
/// ```
/// use trussed_auth::PinId;
///
/// #[repr(u8)]
/// enum Pin {
/// User = 0,
/// Admin = 1,
/// ResetCode = 2,
/// }
///
/// impl From<Pin> for PinId {
/// fn from(pin: Pin) -> Self {
/// (pin as u8).into()
/// }
/// }
/// ```
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
)]
pub struct PinId(u8);
/// Error obtained when trying to parse a [`PinId`][] either through [`PinId::from_path`][] or through the [`FromStr`][] implementation.
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
)]
pub struct PinIdFromStrError;
impl PinId {
/// Get the path to the PIN id.
///
/// Path are of the form `pin.XX` where `xx` is the hexadecimal representation of the PIN number.
pub fn path(&self) -> PathBuf {
let mut path = [0; 6];
path[0..4].copy_from_slice(b"pin.");
path[4..].copy_from_slice(&self.hex());
// path has only ASCII characters and is not too long
#[allow(clippy::unwrap_used)]
PathBuf::try_from(&path).ok().unwrap()
}
/// Get the hex representation of the PIN id
pub fn hex(&self) -> [u8; 2] {
const CHARS: &[u8; 16] = b"0123456789abcdef";
[
CHARS[usize::from(self.0 >> 4)],
CHARS[usize::from(self.0 & 0xf)],
]
}
/// Parse a PinId path
pub fn from_path(path: &str) -> Result<Self, PinIdFromStrError> {
let path = path.strip_prefix("pin.").ok_or(PinIdFromStrError)?;
if path.len() != 2 {
return Err(PinIdFromStrError);
}
let id = u8::from_str_radix(path, 16).map_err(|_| PinIdFromStrError)?;
Ok(PinId(id))
}
}
impl From<u8> for PinId {
fn from(id: u8) -> Self {
Self(id)
}
}
impl From<PinId> for u8 {
fn from(id: PinId) -> Self {
id.0
}
}
impl FromStr for PinId {
type Err = PinIdFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_path(s)
}
}
/// A result returned by [`AuthClient`][].
pub type AuthResult<'a, R, C> = ExtensionResult<'a, AuthExtension, R, C>;
@@ -197,3 +348,21 @@ pub trait AuthClient: ExtensionClient<AuthExtension> {
}
impl<C: ExtensionClient<AuthExtension>> AuthClient for C {}
#[cfg(test)]
mod tests {
use super::PinId;
use trussed_core::types::PathBuf;
#[test]
fn pin_id_path() {
for i in 0..=u8::MAX {
assert_eq!(Ok(PinId(i)), PinId::from_path(PinId(i).path().as_ref()));
let actual = PinId(i).path();
#[allow(clippy::unwrap_used)]
let expected = PathBuf::try_from(format!("pin.{i:02x}").as_str()).unwrap();
println!("id: {i}, actual: {actual}, expected: {expected}");
assert_eq!(actual, expected);
}
}
}
-190
View File
@@ -1,190 +0,0 @@
// Copyright (C) Nitrokey GmbH
// SPDX-License-Identifier: Apache-2.0 or MIT
#![cfg_attr(not(test), no_std)]
#![warn(
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
unused,
unused_qualifications,
clippy::expect_used,
clippy::unwrap_used
)]
#![deny(unsafe_code)]
//! A Trussed API extension for authentication and a backend that implements it.
//!
//! This crate contains an API extension for [Trussed][], [`AuthExtension`][]. The extension
//! currently provides basic PIN handling with retry counters. Applications can access it using
//! the [`AuthClient`][] trait.
//!
//! This crate also contains [`AuthBackend`][], an implementation of the auth extension that stores
//! PINs in the filesystem.
//!
//! # Examples
//!
//! ```
//! use trussed_core::{types::Bytes, syscall};
//! use trussed_auth::{AuthClient, PinId};
//!
//! #[repr(u8)]
//! enum Pin {
//! User = 0,
//! }
//!
//! impl From<Pin> for PinId {
//! fn from(pin: Pin) -> Self {
//! (pin as u8).into()
//! }
//! }
//!
//! fn authenticate_user<C: AuthClient>(client: &mut C, pin: Option<&[u8]>) -> bool {
//! if !syscall!(client.has_pin(Pin::User)).has_pin {
//! // no PIN set
//! return true;
//! }
//! let Some(pin) = pin else {
//! // PIN is set but not provided
//! return false;
//! };
//! let Ok(pin) = Bytes::from_slice(pin) else {
//! // provided PIN is too long
//! return false;
//! };
//! // check PIN
//! syscall!(client.check_pin(Pin::User, pin)).success
//! }
//! ```
//!
//! [Trussed]: https://docs.rs/trussed
mod backend;
mod extension;
pub mod migrate;
use core::str::FromStr;
use littlefs2_core::{path, Path, PathBuf};
use serde::{Deserialize, Serialize};
use trussed_core::{config::MAX_SHORT_DATA_LENGTH, types::Bytes};
pub use backend::{AuthBackend, AuthContext, FilesystemLayout, MAX_HW_KEY_LEN};
pub use extension::{
reply, request, AuthClient, AuthExtension, AuthReply, AuthRequest, AuthResult,
};
/// The maximum length of a PIN.
pub const MAX_PIN_LENGTH: usize = MAX_SHORT_DATA_LENGTH;
/// A PIN.
pub type Pin = Bytes<MAX_PIN_LENGTH>;
const BACKEND_DIR: &Path = path!("backend-auth");
/// The ID of a PIN within the namespace of a client.
///
/// It is recommended that applications use an enum that implements `Into<PinId>`.
///
/// # Examples
///
/// ```
/// use trussed_auth::PinId;
///
/// #[repr(u8)]
/// enum Pin {
/// User = 0,
/// Admin = 1,
/// ResetCode = 2,
/// }
///
/// impl From<Pin> for PinId {
/// fn from(pin: Pin) -> Self {
/// (pin as u8).into()
/// }
/// }
/// ```
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
)]
pub struct PinId(u8);
/// Error obtained when trying to parse a [`PinId`][] either through [`PinId::from_path`][] or through the [`FromStr`][] implementation.
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
)]
pub struct PinIdFromStrError;
impl PinId {
/// Get the path to the PIN id.
///
/// Path are of the form `pin.XX` where `xx` is the hexadecimal representation of the PIN number.
pub fn path(&self) -> PathBuf {
let mut path = [0; 6];
path[0..4].copy_from_slice(b"pin.");
path[4..].copy_from_slice(&self.hex());
// path has only ASCII characters and is not too long
#[allow(clippy::unwrap_used)]
PathBuf::try_from(&path).ok().unwrap()
}
/// Get the hex representation of the PIN id
pub fn hex(&self) -> [u8; 2] {
const CHARS: &[u8; 16] = b"0123456789abcdef";
[
CHARS[usize::from(self.0 >> 4)],
CHARS[usize::from(self.0 & 0xf)],
]
}
/// Parse a PinId path
pub fn from_path(path: &str) -> Result<Self, PinIdFromStrError> {
let path = path.strip_prefix("pin.").ok_or(PinIdFromStrError)?;
if path.len() != 2 {
return Err(PinIdFromStrError);
}
let id = u8::from_str_radix(path, 16).map_err(|_| PinIdFromStrError)?;
Ok(PinId(id))
}
}
impl From<u8> for PinId {
fn from(id: u8) -> Self {
Self(id)
}
}
impl From<PinId> for u8 {
fn from(id: PinId) -> Self {
id.0
}
}
impl FromStr for PinId {
type Err = PinIdFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_path(s)
}
}
#[cfg(test)]
mod tests {
use super::PinId;
use trussed_core::types::PathBuf;
#[test]
fn pin_id_path() {
for i in 0..=u8::MAX {
assert_eq!(Ok(PinId(i)), PinId::from_path(PinId(i).path().as_ref()));
let actual = PinId(i).path();
#[allow(clippy::unwrap_used)]
let expected = PathBuf::try_from(format!("pin.{i:02x}").as_str()).unwrap();
println!("id: {i}, actual: {actual}, expected: {expected}");
assert_eq!(actual, expected);
}
}
}