diff --git a/Cargo.toml b/Cargo.toml index 3a29443..938d20a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ license = "Apache-2.0 OR MIT" description = "Authentication extension and backend for Trussed" [dependencies] +hkdf = "0.12.3" +hmac = "0.12.1" +rand_core = "0.6.4" serde = { version = "1", default-features = false } serde-byte-array = "0.1.2" sha2 = { version = "0.10.6", default-features = false } diff --git a/src/backend.rs b/src/backend.rs index f40b43a..733c36d 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -3,6 +3,11 @@ mod data; +use core::fmt; + +use hkdf::Hkdf; +use rand_core::{CryptoRng, RngCore}; +use sha2::Sha256; use trussed::{ backend::Backend, error::Result, @@ -11,14 +16,34 @@ use trussed::{ service::ServiceResources, store::filestore::Filestore, types::{CoreContext, Location, PathBuf}, + Bytes, }; use crate::{ extension::{reply, AuthExtension, AuthReply, AuthRequest}, - PIN_PATH, + PIN_PATH, SALT_PATH, }; use data::PinData; +const MAX_HW_KEY_LEN: usize = 64; + +#[derive(Clone)] +enum HardwareKey { + None, + Raw(Bytes), + Extracted(Hkdf), +} + +impl fmt::Debug for HardwareKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => f.debug_tuple("None").finish(), + Self::Raw(_) => f.debug_tuple("Raw").field(&"[redacted]").finish(), + Self::Extracted(_) => f.debug_tuple("Raw").field(&"[redacted]").finish(), + } + } +} + /// A basic implementation of the [`AuthExtension`][]. /// /// This implementation stores PINs together with their retry counters on the filesystem. PINs are @@ -26,24 +51,128 @@ use data::PinData; #[derive(Clone, Debug)] pub struct AuthBackend { location: Location, + hw_key: HardwareKey, } impl AuthBackend { /// Creates a new `AuthBackend` using the given storage location for the PINs. pub fn new(location: Location) -> Self { - Self { location } + Self { + location, + hw_key: HardwareKey::None, + } + } + /// Creates a new `AuthBackend` with a given key. + /// + /// This key is used to strengthen key generation from the pins + pub fn with_hw_key(location: Location, hw_key: Bytes) -> Self { + Self { + location, + hw_key: HardwareKey::Raw(hw_key), + } + } + + fn get_salt( + &self, + trussed_filestore: &mut impl Filestore, + rng: &mut R, + ) -> Result<[u8; 32], Error> { + let path = PathBuf::from(SALT_PATH); + trussed_filestore + .read(&path, self.location) + .map(|d: Bytes<32>| (&**d).try_into().unwrap()) + .or_else(|_| { + if trussed_filestore + .metadata(&path, self.location) + .or(Err(Error::ReadFailed))? + .is_some() + { + return Err(Error::ReadFailed); + } + let mut salt = [0; 32]; + rng.fill_bytes(&mut salt); + trussed_filestore + .write(&path, self.location, &salt) + .or(Err(Error::WriteFailed)) + .and(Ok(salt)) + }) + } + + fn extract( + &mut self, + trussed_filestore: &mut impl Filestore, + ikm: Option>, + rng: &mut R, + ) -> Result<&Hkdf, Error> { + let ikm: &[u8] = ikm.as_deref().map(|i| &**i).unwrap_or(&[]); + let salt = self.get_salt(trussed_filestore, rng)?; + let kdf = Hkdf::new(Some(&salt), ikm); + self.hw_key = HardwareKey::Extracted(kdf); + match &self.hw_key { + HardwareKey::Extracted(kdf) => Ok(kdf), + // hw_key was just set to Extracted + _ => unreachable!(), + } + } + + fn expand(kdf: &Hkdf, client_id: &PathBuf) -> [u8; 32] { + let mut out = [0; 32]; + kdf.expand(client_id.as_ref().as_bytes(), &mut out).unwrap(); + out + } + + fn generate_app_key( + &mut self, + client_id: PathBuf, + trussed_filestore: &mut impl Filestore, + rng: &mut R, + ) -> Result<[u8; 32], Error> { + Ok(match &self.hw_key { + HardwareKey::Extracted(okm) => Self::expand(okm, &client_id), + HardwareKey::Raw(hw_k) => { + let kdf = self.extract(trussed_filestore, Some(hw_k.clone()), rng)?; + Self::expand(kdf, &client_id) + } + HardwareKey::None => { + let kdf = self.extract(trussed_filestore, None, rng)?; + Self::expand(kdf, &client_id) + } + }) + } + + #[allow(unused)] + fn get_app_key( + &mut self, + client_id: PathBuf, + trussed_filestore: &mut impl Filestore, + ctx: &mut AuthContext, + rng: &mut R, + ) -> Result<[u8; 32], Error> { + if let Some(app_key) = ctx.application_key { + return Ok(app_key); + } + + let app_key = self.generate_app_key(client_id, trussed_filestore, rng)?; + ctx.application_key = Some(app_key); + Ok(app_key) } } +/// Per-client context for [`AuthBackend`][] +#[derive(Default, Debug)] +pub struct AuthContext { + application_key: Option<[u8; 32]>, +} + impl Backend

for AuthBackend { - type Context = (); + type Context = AuthContext; } impl ExtensionImpl for AuthBackend { fn extension_request( &mut self, core_ctx: &mut CoreContext, - _ctx: &mut (), + _ctx: &mut AuthContext, request: &AuthRequest, resources: &mut ServiceResources

, ) -> Result { @@ -61,6 +190,9 @@ impl ExtensionImpl for AuthBackend { )?; Ok(reply::CheckPin { success }.into()) } + AuthRequest::GetPinKey(_request) => { + todo!() + } AuthRequest::SetPin(request) => { let mut rng = resources.rng().map_err(|_| Error::RngFailed)?; PinData::new(request.id, &request.pin, request.retries, &mut rng) diff --git a/src/extension.rs b/src/extension.rs index 451d811..53a3cef 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -30,6 +30,7 @@ impl Extension for AuthExtension { pub enum AuthRequest { HasPin(request::HasPin), CheckPin(request::CheckPin), + GetPinKey(request::GetPinKey), SetPin(request::SetPin), DeletePin(request::DeletePin), DeleteAllPins(request::DeleteAllPins), @@ -41,6 +42,7 @@ pub enum AuthRequest { pub enum AuthReply { HasPin(reply::HasPin), CheckPin(reply::CheckPin), + GetPinKey(reply::GetPinKey), SetPin(reply::SetPin), DeletePin(reply::DeletePin), DeleteAllPins(reply::DeleteAllPins), diff --git a/src/extension/reply.rs b/src/extension/reply.rs index 5a78f1e..d1d8e09 100644 --- a/src/extension/reply.rs +++ b/src/extension/reply.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 or MIT use serde::{Deserialize, Serialize}; -use trussed::error::{Error, Result}; +use trussed::{ + error::{Error, Result}, + types::KeyId, +}; use super::AuthReply; @@ -41,6 +44,19 @@ impl From for AuthReply { } } +#[derive(Debug, Deserialize, Serialize)] +#[must_use] +pub struct GetPinKey { + /// None means the check failed + pub result: Option, +} + +impl From for AuthReply { + fn from(reply: GetPinKey) -> Self { + Self::GetPinKey(reply) + } +} + impl TryFrom for CheckPin { type Error = Error; diff --git a/src/extension/request.rs b/src/extension/request.rs index 8fb2669..d65d809 100644 --- a/src/extension/request.rs +++ b/src/extension/request.rs @@ -29,6 +29,18 @@ impl From for AuthRequest { } } +#[derive(Debug, Deserialize, Serialize)] +pub struct GetPinKey { + pub id: PinId, + pub pin: Pin, +} + +impl From for AuthRequest { + fn from(request: GetPinKey) -> Self { + Self::GetPinKey(request) + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct SetPin { pub id: PinId, diff --git a/src/lib.rs b/src/lib.rs index 57e0f1b..2271dc2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,7 @@ use trussed::{ types::{Bytes, PathBuf}, }; -pub use backend::AuthBackend; +pub use backend::{AuthBackend, AuthContext}; pub use extension::{ reply, request, AuthClient, AuthExtension, AuthReply, AuthRequest, AuthResult, }; @@ -81,6 +81,7 @@ pub const MAX_PIN_LENGTH: usize = MAX_SHORT_DATA_LENGTH; pub type Pin = Bytes; const PIN_PATH: &str = "backend-auth/pin"; +const SALT_PATH: &str = "backend-auth/salt"; /// The ID of a PIN within the namespace of a client. /// diff --git a/tests/backend.rs b/tests/backend.rs index 2cc5659..e81f74f 100644 --- a/tests/backend.rs +++ b/tests/backend.rs @@ -11,7 +11,7 @@ mod dispatch { service::ServiceResources, types::{Context, Location}, }; - use trussed_auth::{AuthBackend, AuthExtension}; + use trussed_auth::{AuthBackend, AuthContext, AuthExtension}; pub const BACKENDS: &[BackendId] = &[BackendId::Custom(Backend::Auth), BackendId::Core]; @@ -47,6 +47,11 @@ mod dispatch { auth: AuthBackend, } + #[derive(Default)] + pub struct DispatchContext { + auth: AuthContext, + } + impl Dispatch { pub fn new() -> Self { Self { @@ -57,7 +62,7 @@ mod dispatch { impl ExtensionDispatch

for Dispatch { type BackendId = Backend; - type Context = (); + type Context = DispatchContext; type ExtensionId = Extension; fn core_request( @@ -68,9 +73,10 @@ mod dispatch { resources: &mut ServiceResources

, ) -> Result { match backend { - Backend::Auth => self - .auth - .request(&mut ctx.core, &mut (), request, resources), + Backend::Auth => { + self.auth + .request(&mut ctx.core, &mut ctx.backends.auth, request, resources) + } } } @@ -86,7 +92,7 @@ mod dispatch { Backend::Auth => match extension { Extension::Auth => self.auth.extension_request_serialized( &mut ctx.core, - &mut (), + &mut ctx.backends.auth, request, resources, ),