diff --git a/CHANGELOG.md b/CHANGELOG.md index a71195b..99cfa10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased][] +[Unreleased]: https://github.com/trussed-dev/trussed-staging/compare/v0.3.0...HEAD + +- + +## [0.3.0][] - 2024-03-25 + +[0.3.0]: https://github.com/trussed-dev/trussed-staging/compare/v0.2.0...v0.3.0 + - Remove `manage` from default features. +- Implement `HkdfExtension` (moved from [Nitrokey/trussed-hkdf-backend][]) -[Unreleased]: https://github.com/trussed-dev/trussed-staging/compare/v0.2.0...HEAD +[Nitrokey/trussed-hkdf-backend]: https://github.com/Nitrokey/trussed-hkdf-backend ## [0.2.0][] - 2024-03-15 diff --git a/Cargo.toml b/Cargo.toml index a4d8c33..8392b37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: CC0-1.0 [workspace] -members = ["extensions/chunked", "extensions/manage", "extensions/wrap-key-to-file"] +members = ["extensions/chunked", "extensions/hkdf", "extensions/manage", "extensions/wrap-key-to-file"] [workspace.package] authors = ["Nitrokey GmbH "] @@ -17,7 +17,7 @@ trussed = { version = "0.1.0", features = ["serde-extensions"] } [package] name = "trussed-staging" -version = "0.2.0" +version = "0.3.0" description = "Work in progress trussed features" authors.workspace = true edition.workspace = true @@ -30,23 +30,29 @@ serde-byte-array.workspace = true trussed.workspace = true chacha20poly1305 = { version = "0.10", default-features = false, features = ["heapless", "reduced-round"], optional = true } -rand_core = { version = "0.6.4", default-features = false } delog = "0.1.6" +hkdf = { version = "0.12", optional = true } +rand_core = { version = "0.6.4", default-features = false } +sha2 = { version = "0.10", default-features = false, optional = true } littlefs2 = "0.4.0" trussed-chunked = { version = "0.1.0", optional = true } +trussed-hkdf = { version = "0.2.0", optional = true } trussed-manage = { version = "0.1.0", optional = true } trussed-wrap-key-to-file = { version = "0.1.0", optional = true } [dev-dependencies] +hex-literal = "0.3.4" +hmac = "0.12.0" trussed = { workspace = true, features = ["virt"] } [features] default = [] -wrap-key-to-file = ["chacha20poly1305", "trussed-wrap-key-to-file"] chunked = ["trussed-chunked", "chacha20poly1305/stream"] +hkdf = ["trussed-hkdf", "dep:hkdf", "dep:sha2"] manage = ["trussed-manage"] +wrap-key-to-file = ["chacha20poly1305", "trussed-wrap-key-to-file"] virt = ["std", "trussed/virt"] std = [] @@ -64,5 +70,6 @@ trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "45ed62ba9 littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } trussed-chunked = { path = "extensions/chunked" } +trussed-hkdf = { path = "extensions/hkdf" } trussed-manage = { path = "extensions/manage" } trussed-wrap-key-to-file = { path = "extensions/wrap-key-to-file" } diff --git a/Makefile b/Makefile index 7ec9e33..665e59c 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ check: RUSTLFAGS='-Dwarnings' cargo check --all-features --all-targets --workspace RUSTLFAGS='-Dwarnings' cargo check --no-default-features RUSTLFAGS='-Dwarnings' cargo check --features chunked + RUSTLFAGS='-Dwarnings' cargo check --features hkdf RUSTLFAGS='-Dwarnings' cargo check --features manage RUSTLFAGS='-Dwarnings' cargo check --features wrap-key-to-file diff --git a/extensions/hkdf/CHANGELOG.md b/extensions/hkdf/CHANGELOG.md new file mode 100644 index 0000000..41c9880 --- /dev/null +++ b/extensions/hkdf/CHANGELOG.md @@ -0,0 +1,34 @@ + + +# 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][] + +[Unreleased]: https://github.com/trussed-dev/trussed-staging/compare/hkdf-v0.2.0...HEAD + +- + +## [0.2.0][] - 2024-03-25 + +[0.2.0]: https://github.com/trussed-dev/trussed-staging/releases/tag/hkdf-v0.2.0 + +- Move the `trussed-hkdf` crate from the [Nitrokey/trussed-hkdf-backend][] + repository into the [trussed-dev/trussed-staging][] repository +- Remove the `HkdfBackend`. The `HkdfExtension` is now implemented by the + `StagingBackend` in `trussed-staging` if the `hkdf` feature is enabled + +[Nitrokey/trussed-hkdf-backend]: https://github.com/Nitrokey/trussed-hkdf-backend +[trussed-dev/trussed-staging]: https://github.com/trussed-dev/trussed-staging + +## [0.1.0][] - 2024-02-20 + +[0.1.0]: https://github.com/Nitrokey/trussed-hkdf-backend/releases/tag/v0.1.0 + +Initial release of the `HkdfExtension` and its implementation in the `HkdfBackend`. diff --git a/extensions/hkdf/Cargo.toml b/extensions/hkdf/Cargo.toml new file mode 100644 index 0000000..dfa5ebc --- /dev/null +++ b/extensions/hkdf/Cargo.toml @@ -0,0 +1,14 @@ +# Copyright (C) Nitrokey GmbH +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "trussed-hkdf" +version = "0.2.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +trussed.workspace = true diff --git a/extensions/hkdf/src/lib.rs b/extensions/hkdf/src/lib.rs new file mode 100644 index 0000000..c6b0cca --- /dev/null +++ b/extensions/hkdf/src/lib.rs @@ -0,0 +1,164 @@ +// Copyright (C) Nitrokey GmbH +// SPDX-License-Identifier: Apache-2.0 or MIT + +#![no_std] +#![warn(non_ascii_idents, trivial_casts, unused, unused_qualifications)] +#![deny(unsafe_code)] + +use serde::{Deserialize, Serialize}; +use trussed::{ + config::MAX_MEDIUM_DATA_LENGTH, + serde_extensions::{Extension, ExtensionClient, ExtensionResult}, + types::{Bytes, KeyId, Location, Message}, + Error, +}; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct OkmId(pub KeyId); + +/// Can represent either data or a key +#[derive(Serialize, Deserialize)] +pub enum KeyOrData { + Key(KeyId), + Data(Bytes), +} + +pub struct HkdfExtension; + +impl Extension for HkdfExtension { + type Request = HkdfRequest; + type Reply = HkdfReply; +} + +#[allow(clippy::large_enum_variant)] +#[derive(Serialize, Deserialize)] +pub enum HkdfRequest { + Extract(HkdfExtractRequest), + Expand(HkdfExpandRequest), +} +#[derive(Serialize, Deserialize)] +pub enum HkdfReply { + Extract(HkdfExtractReply), + Expand(HkdfExpandReply), +} + +impl From for HkdfRequest { + fn from(v: HkdfExpandRequest) -> Self { + Self::Expand(v) + } +} + +impl From for HkdfRequest { + fn from(v: HkdfExtractRequest) -> Self { + Self::Extract(v) + } +} + +impl From for HkdfReply { + fn from(v: HkdfExpandReply) -> Self { + Self::Expand(v) + } +} + +impl From for HkdfReply { + fn from(v: HkdfExtractReply) -> Self { + Self::Extract(v) + } +} + +impl TryFrom for HkdfExpandRequest { + type Error = Error; + fn try_from(v: HkdfRequest) -> Result { + match v { + HkdfRequest::Expand(v) => Ok(v), + _ => Err(Error::InternalError), + } + } +} +impl TryFrom for HkdfExtractRequest { + type Error = Error; + fn try_from(v: HkdfRequest) -> Result { + match v { + HkdfRequest::Extract(v) => Ok(v), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HkdfExpandReply { + type Error = Error; + fn try_from(v: HkdfReply) -> Result { + match v { + HkdfReply::Expand(v) => Ok(v), + _ => Err(Error::InternalError), + } + } +} +impl TryFrom for HkdfExtractReply { + type Error = Error; + fn try_from(v: HkdfReply) -> Result { + match v { + HkdfReply::Extract(v) => Ok(v), + _ => Err(Error::InternalError), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct HkdfExtractReply { + pub okm: OkmId, +} + +#[derive(Serialize, Deserialize)] +pub struct HkdfExtractRequest { + pub ikm: KeyOrData, + pub salt: Option>, + /// Location to store the OKM + pub storage: Location, +} + +#[derive(Serialize, Deserialize)] +pub struct HkdfExpandReply { + pub key: KeyId, +} + +#[derive(Serialize, Deserialize)] +pub struct HkdfExpandRequest { + pub prk: OkmId, + pub info: Message, + pub len: usize, + pub storage: Location, +} + +pub type HkdfResult<'a, R, C> = ExtensionResult<'a, HkdfExtension, R, C>; + +pub trait HkdfClient: ExtensionClient { + fn hkdf_extract( + &mut self, + ikm: KeyOrData, + salt: Option>, + storage: Location, + ) -> HkdfResult<'_, HkdfExtractReply, Self> { + self.extension(HkdfRequest::Extract(HkdfExtractRequest { + ikm, + salt, + storage, + })) + } + fn hkdf_expand( + &mut self, + prk: OkmId, + info: Message, + len: usize, + storage: Location, + ) -> HkdfResult<'_, HkdfExpandReply, Self> { + self.extension(HkdfRequest::Expand(HkdfExpandRequest { + prk, + info, + len, + storage, + })) + } +} + +impl> HkdfClient for C {} diff --git a/src/hkdf.rs b/src/hkdf.rs new file mode 100644 index 0000000..4ec0225 --- /dev/null +++ b/src/hkdf.rs @@ -0,0 +1,111 @@ +// Copyright (C) Nitrokey GmbH +// SPDX-License-Identifier: Apache-2.0 or MIT + +use hkdf::Hkdf; +use sha2::Sha256; +use trussed::{ + config::MAX_MEDIUM_DATA_LENGTH, + key::{Kind, Secrecy}, + serde_extensions::ExtensionImpl, + service::{ClientKeystore, Keystore, ServiceResources}, + store::Store, + types::{Bytes, CoreContext, MediumData, ShortData}, + Error, Platform, +}; +use trussed_hkdf::{ + HkdfExpandReply, HkdfExpandRequest, HkdfExtension, HkdfExtractReply, HkdfExtractRequest, + HkdfReply, HkdfRequest, KeyOrData, OkmId, +}; + +use crate::{StagingBackend, StagingContext}; + +impl ExtensionImpl for StagingBackend { + fn extension_request( + &mut self, + core_ctx: &mut CoreContext, + _backend_ctx: &mut StagingContext, + request: &HkdfRequest, + resources: &mut ServiceResources

, + ) -> Result { + let mut keystore = resources.keystore(core_ctx.path.clone())?; + Ok(match request { + HkdfRequest::Extract(req) => extract(req, &mut keystore)?.into(), + HkdfRequest::Expand(req) => expand(req, &mut keystore)?.into(), + }) + } +} + +fn get_mat( + req: &KeyOrData, + keystore: &mut ClientKeystore, +) -> Result { + Ok(match req { + KeyOrData::Data(d) => d.clone(), + KeyOrData::Key(key_id) => { + let key_mat = keystore.load_key(Secrecy::Secret, None, key_id)?; + if !matches!(key_mat.kind, Kind::Symmetric(..) | Kind::Shared(..)) { + warn!("Attempt to HKDF on a private key"); + return Err(Error::MechanismInvalid); + } + Bytes::from_slice(&key_mat.material).map_err(|_| { + warn!("Attempt to HKDF a too large key"); + Error::InternalError + })? + } + }) +} + +fn extract( + req: &HkdfExtractRequest, + keystore: &mut ClientKeystore, +) -> Result { + let ikm = get_mat(&req.ikm, keystore)?; + let salt = req + .salt + .as_ref() + .map(|s| get_mat(s, keystore)) + .transpose()?; + let salt_ref = salt.as_deref().map(|d| &**d); + let (prk, _) = Hkdf::::extract(salt_ref, &ikm); + assert_eq!(prk.len(), 256 / 8); + let key_id = keystore.store_key( + req.storage, + Secrecy::Secret, + Kind::Symmetric(prk.len()), + &prk, + )?; + Ok(HkdfExtractReply { okm: OkmId(key_id) }) +} +fn expand( + req: &HkdfExpandRequest, + keystore: &mut ClientKeystore, +) -> Result { + let prk = keystore.load_key(Secrecy::Secret, None, &req.prk.0)?; + if !matches!(prk.kind, Kind::Symmetric(32)) { + error!("Attempt to use wrong key for HKDF expand"); + return Err(Error::ObjectHandleInvalid); + } + + let hkdf = Hkdf::::from_prk(&prk.material).map_err(|_| { + warn!("Failed to create HKDF"); + Error::InternalError + })?; + let mut okm = ShortData::new(); + okm.resize_default(req.len).map_err(|_| { + error!("Attempt to run HKDF with too large output"); + Error::WrongMessageLength + })?; + hkdf.expand(&req.info, &mut okm).map_err(|_| { + warn!("Bad HKDF expand length"); + Error::WrongMessageLength + })?; + + let key = keystore.store_key( + req.storage, + Secrecy::Secret, + Kind::Symmetric(okm.len()), + &okm, + )?; + + Ok(HkdfExpandReply { key }) +} diff --git a/src/lib.rs b/src/lib.rs index 0f85088..c6c7ff7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,9 @@ mod wrap_key_to_file; #[cfg(feature = "chunked")] mod chunked; +#[cfg(feature = "hkdf")] +mod hkdf; + #[cfg(feature = "manage")] mod manage; #[cfg(feature = "manage")] diff --git a/src/virt.rs b/src/virt.rs index b7f26d9..57d3d14 100644 --- a/src/virt.rs +++ b/src/virt.rs @@ -8,6 +8,8 @@ use trussed::types::{Location, Path}; #[cfg(feature = "chunked")] use trussed_chunked::ChunkedExtension; +#[cfg(feature = "hkdf")] +use trussed_hkdf::HkdfExtension; #[cfg(feature = "manage")] use trussed_manage::ManageExtension; #[cfg(feature = "wrap-key-to-file")] @@ -27,18 +29,14 @@ pub enum BackendIds { #[derive(Debug)] pub enum ExtensionIds { - #[cfg(feature = "wrap-key-to-file")] - WrapKeyToFile, #[cfg(feature = "chunked")] Chunked, + #[cfg(feature = "hkdf")] + Hkdf, #[cfg(feature = "manage")] Manage, -} - -#[cfg(feature = "wrap-key-to-file")] -impl ExtensionId for Dispatcher { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::WrapKeyToFile; + #[cfg(feature = "wrap-key-to-file")] + WrapKeyToFile, } #[cfg(feature = "chunked")] @@ -47,21 +45,35 @@ impl ExtensionId for Dispatcher { const ID: ExtensionIds = ExtensionIds::Chunked; } +#[cfg(feature = "hkdf")] +impl ExtensionId for Dispatcher { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Hkdf; +} + #[cfg(feature = "manage")] impl ExtensionId for Dispatcher { type Id = ExtensionIds; const ID: ExtensionIds = ExtensionIds::Manage; } +#[cfg(feature = "wrap-key-to-file")] +impl ExtensionId for Dispatcher { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::WrapKeyToFile; +} + impl From for u8 { fn from(value: ExtensionIds) -> Self { match value { - #[cfg(feature = "wrap-key-to-file")] - ExtensionIds::WrapKeyToFile => 0, #[cfg(feature = "chunked")] - ExtensionIds::Chunked => 1, + ExtensionIds::Chunked => 0, + #[cfg(feature = "hkdf")] + ExtensionIds::Hkdf => 1, #[cfg(feature = "manage")] ExtensionIds::Manage => 2, + #[cfg(feature = "wrap-key-to-file")] + ExtensionIds::WrapKeyToFile => 3, } } } @@ -70,12 +82,14 @@ impl TryFrom for ExtensionIds { type Error = Error; fn try_from(value: u8) -> Result { match value { - #[cfg(feature = "wrap-key-to-file")] - 0 => Ok(Self::WrapKeyToFile), #[cfg(feature = "chunked")] - 1 => Ok(Self::Chunked), + 0 => Ok(Self::Chunked), + #[cfg(feature = "hkdf")] + 1 => Ok(Self::Hkdf), #[cfg(feature = "manage")] 2 => Ok(Self::Manage), + #[cfg(feature = "wrap-key-to-file")] + 3 => Ok(Self::WrapKeyToFile), _ => Err(Error::FunctionNotSupported), } } @@ -133,6 +147,15 @@ impl ExtensionDispatch for Dispatcher { ) } + #[cfg(feature = "chunked")] + ExtensionIds::Hkdf => ExtensionImpl::::extension_request_serialized( + &mut self.backend, + &mut ctx.core, + &mut ctx.backends, + request, + resources, + ), + #[cfg(feature = "manage")] ExtensionIds::Manage => ExtensionImpl::::extension_request_serialized( &mut self.backend, diff --git a/tests/hkdf.rs b/tests/hkdf.rs new file mode 100644 index 0000000..46f33cc --- /dev/null +++ b/tests/hkdf.rs @@ -0,0 +1,47 @@ +// Copyright (C) Nitrokey GmbH +// SPDX-License-Identifier: Apache-2.0 or MIT + +#![cfg(all(feature = "virt", feature = "manage"))] + +use hex_literal::hex; +use hkdf::Hkdf; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use trussed::{ + client::HmacSha256, + syscall, + types::{Bytes, Location}, +}; +use trussed_hkdf::{HkdfClient, KeyOrData::*}; +use trussed_staging::virt; + +const SALT: &[u8] = &hex!("0011223344556677889900AABBCCDDEE"); +const IKM: &[u8] = &hex!("AABBCCDDEE0011223344556677889900"); +const INFO: &[u8] = b"INFO"; +const MSG: &[u8] = b"MSG"; + +#[test] +fn hkdf() { + let ref_hkdf = Hkdf::::new(Some(SALT), IKM); + let mut okm = [0; 16]; + ref_hkdf.expand(INFO, &mut okm).unwrap(); + let mut mac = Hmac::::new_from_slice(&okm).unwrap(); + mac.update(MSG); + virt::with_ram_client("hkdf_test", |mut client| { + let prk = syscall!(client.hkdf_extract( + Data(Bytes::from_slice(IKM).unwrap()), + Some(Data(Bytes::from_slice(SALT).unwrap())), + Location::External, + )) + .okm; + let expanded = syscall!(client.hkdf_expand( + prk, + Bytes::from_slice(INFO).unwrap(), + 16, + Location::Volatile + )) + .key; + let signed = syscall!(client.sign_hmacsha256(expanded, MSG)).signature; + mac.verify_slice(&signed).unwrap(); + }); +}