From e27ad9147196b3e69daa41b9916b983a84a35da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 29 Mar 2024 15:25:57 +0100 Subject: [PATCH] Add HPKE extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will be useful for PIV encryption, working together with https://github.com/trussed-dev/trussed-auth/pull/41 This implements the standard HPKE from [RFC 9180](https://www.rfc-editor.org/rfc/rfc9180.html). This uses a custom implmentation instead of the `hpke` crate because this crate seals the trait to implement custom ciphers, and we want to use `ChaCha8` and not `ChaCha20`. The implementation is tested against the RFC test vectors for `ChaCha20`, and is made generic so that the same code can be used for `ChaCha8` in the backend. For ChaCha8Poly1305 AEAD ID, I used a custom `0xFFFE`, which is probably unused. I need to look if there is somewhere someone already using ChaCha8Poly1305 for HPKE and if there is a specified ID. --- Cargo.toml | 22 +- extensions/hpke/Cargo.toml | 15 + extensions/hpke/src/lib.rs | 451 ++++++++++++++++++++++++++ src/hpke.rs | 625 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/virt.rs | 22 ++ tests/hpke.rs | 119 +++++++ 7 files changed, 1253 insertions(+), 4 deletions(-) create mode 100644 extensions/hpke/Cargo.toml create mode 100644 extensions/hpke/src/lib.rs create mode 100644 src/hpke.rs create mode 100644 tests/hpke.rs diff --git a/Cargo.toml b/Cargo.toml index c70c83b..11eda5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,14 @@ # SPDX-License-Identifier: CC0-1.0 [workspace] -members = ["extensions/chunked", "extensions/fs-info", "extensions/hkdf", "extensions/manage", "extensions/wrap-key-to-file"] +members = [ + "extensions/chunked", + "extensions/fs-info", + "extensions/hkdf", + "extensions/hpke", + "extensions/manage", + "extensions/wrap-key-to-file", +] [workspace.package] authors = ["Nitrokey GmbH "] @@ -35,15 +42,20 @@ 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" +salty = { version = "0.3.0", default-features = false } +digest = { version = "0.10.7", default-features = false } +hex-literal = { version = "0.4.0", optional = true } +aead = { version = "0.5.2", optional = true, default-features = false } trussed-chunked = { version = "0.1.0", optional = true } trussed-hkdf = { version = "0.2.0", optional = true } +trussed-hpke = { version = "0.1.0", optional = true } trussed-manage = { version = "0.1.0", optional = true } trussed-wrap-key-to-file = { version = "0.1.0", optional = true } trussed-fs-info = { version = "0.1.0", optional = true } [dev-dependencies] -hex-literal = "0.3.4" +hex-literal = "0.4.0" hmac = "0.12.0" trussed = { workspace = true, features = ["virt"] } @@ -52,8 +64,9 @@ default = [] chunked = ["trussed-chunked", "chacha20poly1305/stream"] hkdf = ["trussed-hkdf", "dep:hkdf", "dep:sha2"] +hpke = ["trussed-hpke", "dep:hkdf", "dep:sha2", "dep:hex-literal", "dep:aead", "dep:chacha20poly1305"] manage = ["trussed-manage"] -wrap-key-to-file = ["chacha20poly1305", "trussed-wrap-key-to-file"] +wrap-key-to-file = ["dep:chacha20poly1305", "trussed-wrap-key-to-file"] fs-info = ["trussed-fs-info"] virt = ["std", "trussed/virt"] @@ -68,11 +81,12 @@ log-warn = [] log-error = [] [patch.crates-io] -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" } +trussed = { git = "https://github.com/nitrokey/trussed.git", rev = "540ad725ef44f0d6d3d2da7dd6ec0bacffaeb5bf" } littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" } trussed-chunked = { path = "extensions/chunked" } trussed-hkdf = { path = "extensions/hkdf" } +trussed-hpke = { path = "extensions/hpke" } trussed-manage = { path = "extensions/manage" } trussed-wrap-key-to-file = { path = "extensions/wrap-key-to-file" } trussed-fs-info= { path = "extensions/fs-info" } diff --git a/extensions/hpke/Cargo.toml b/extensions/hpke/Cargo.toml new file mode 100644 index 0000000..85eb696 --- /dev/null +++ b/extensions/hpke/Cargo.toml @@ -0,0 +1,15 @@ +# Copyright (C) Nitrokey GmbH +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "trussed-hpke" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +trussed.workspace = true +serde-byte-array = "0.1.2" diff --git a/extensions/hpke/src/lib.rs b/extensions/hpke/src/lib.rs new file mode 100644 index 0000000..a893160 --- /dev/null +++ b/extensions/hpke/src/lib.rs @@ -0,0 +1,451 @@ +// Copyright (C) Nitrokey GmbH +// SPDX-License-Identifier: Apache-2.0 or MIT + +//! Trussed Extension providing DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305 +//! For more details, see + +#![no_std] +#![warn(non_ascii_idents, trivial_casts, unused, unused_qualifications)] +#![deny(unsafe_code)] + +use serde::{Deserialize, Serialize}; +use serde_byte_array::ByteArray; + +use trussed::serde_extensions::{Extension, ExtensionClient, ExtensionResult}; +use trussed::types::{KeyId, Location, Message, PathBuf, ShortData}; +use trussed::Error; + +#[derive(Deserialize, Serialize)] +pub enum HpkeRequest { + Seal(HpkeSealRequest), + SealKey(HpkeSealKeyRequest), + SealKeyToFile(HpkeSealKeyToFileRequest), + Open(HpkeOpenRequest), + OpenKey(HpkeOpenKeyRequest), + OpenKeyFromFile(HpkeOpenKeyFromFileRequest), +} + +impl From for HpkeRequest { + fn from(value: HpkeSealRequest) -> Self { + Self::Seal(value) + } +} +impl From for HpkeRequest { + fn from(value: HpkeSealKeyRequest) -> Self { + Self::SealKey(value) + } +} +impl From for HpkeRequest { + fn from(value: HpkeSealKeyToFileRequest) -> Self { + Self::SealKeyToFile(value) + } +} +impl From for HpkeRequest { + fn from(value: HpkeOpenRequest) -> Self { + Self::Open(value) + } +} +impl From for HpkeRequest { + fn from(value: HpkeOpenKeyRequest) -> Self { + Self::OpenKey(value) + } +} +impl From for HpkeRequest { + fn from(value: HpkeOpenKeyFromFileRequest) -> Self { + Self::OpenKeyFromFile(value) + } +} +impl TryFrom for HpkeSealRequest { + type Error = Error; + fn try_from(value: HpkeRequest) -> Result { + match value { + HpkeRequest::Seal(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeSealKeyRequest { + type Error = Error; + fn try_from(value: HpkeRequest) -> Result { + match value { + HpkeRequest::SealKey(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeSealKeyToFileRequest { + type Error = Error; + fn try_from(value: HpkeRequest) -> Result { + match value { + HpkeRequest::SealKeyToFile(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeOpenRequest { + type Error = Error; + fn try_from(value: HpkeRequest) -> Result { + match value { + HpkeRequest::Open(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeOpenKeyRequest { + type Error = Error; + fn try_from(value: HpkeRequest) -> Result { + match value { + HpkeRequest::OpenKey(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeOpenKeyFromFileRequest { + type Error = Error; + fn try_from(value: HpkeRequest) -> Result { + match value { + HpkeRequest::OpenKeyFromFile(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +/// Seal to a public key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeSealRequest { + pub key: KeyId, + pub plaintext: Message, + pub aad: ShortData, + pub info: ShortData, + /// The location of the stored "enc" key + pub enc_location: Location, +} + +/// Seal to a public key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeSealKeyRequest { + pub public_key: KeyId, + pub key_to_seal: KeyId, + pub aad: ShortData, + pub info: ShortData, +} + +/// Seal to a public key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeSealKeyToFileRequest { + pub public_key: KeyId, + pub key_to_seal: KeyId, + pub aad: ShortData, + pub info: ShortData, + pub file: PathBuf, + pub location: Location, +} + +/// Open with a private key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeOpenRequest { + pub key: KeyId, + pub enc_key: KeyId, + pub ciphertext: Message, + pub tag: ByteArray<16>, + pub aad: ShortData, + pub info: ShortData, +} + +/// Open with a private key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeOpenKeyRequest { + pub key: KeyId, + pub sealed_key: Message, + pub aad: ShortData, + pub info: ShortData, + pub location: Location, +} + +/// Open with a private key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeOpenKeyFromFileRequest { + pub key: KeyId, + pub sealed_key: PathBuf, + pub sealed_location: Location, + pub unsealed_location: Location, + pub aad: ShortData, + pub info: ShortData, +} + +/// Seal to a public key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeSealReply { + pub enc: KeyId, + pub ciphertext: Message, + pub tag: ByteArray<16>, +} +/// Seal a key to a public key +#[derive(Deserialize, Serialize)] +pub struct HpkeSealKeyReply { + pub data: Message, +} + +/// Seal a key to a public key +#[derive(Deserialize, Serialize)] +pub struct HpkeSealKeyToFileReply {} + +#[derive(Deserialize, Serialize)] +pub enum HpkeReply { + Seal(HpkeSealReply), + SealKey(HpkeSealKeyReply), + SealKeyToFile(HpkeSealKeyToFileReply), + Open(HpkeOpenReply), + OpenKey(HpkeOpenKeyReply), + OpenKeyFromFile(HpkeOpenKeyFromFileReply), +} + +impl From for HpkeReply { + fn from(value: HpkeSealReply) -> Self { + Self::Seal(value) + } +} +impl From for HpkeReply { + fn from(value: HpkeSealKeyReply) -> Self { + Self::SealKey(value) + } +} +impl From for HpkeReply { + fn from(value: HpkeSealKeyToFileReply) -> Self { + Self::SealKeyToFile(value) + } +} +impl From for HpkeReply { + fn from(value: HpkeOpenReply) -> Self { + Self::Open(value) + } +} +impl From for HpkeReply { + fn from(value: HpkeOpenKeyReply) -> Self { + Self::OpenKey(value) + } +} +impl From for HpkeReply { + fn from(value: HpkeOpenKeyFromFileReply) -> Self { + Self::OpenKeyFromFile(value) + } +} +impl TryFrom for HpkeSealReply { + type Error = Error; + fn try_from(value: HpkeReply) -> Result { + match value { + HpkeReply::Seal(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeSealKeyReply { + type Error = Error; + fn try_from(value: HpkeReply) -> Result { + match value { + HpkeReply::SealKey(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeSealKeyToFileReply { + type Error = Error; + fn try_from(value: HpkeReply) -> Result { + match value { + HpkeReply::SealKeyToFile(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeOpenReply { + type Error = Error; + fn try_from(value: HpkeReply) -> Result { + match value { + HpkeReply::Open(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeOpenKeyReply { + type Error = Error; + fn try_from(value: HpkeReply) -> Result { + match value { + HpkeReply::OpenKey(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +impl TryFrom for HpkeOpenKeyFromFileReply { + type Error = Error; + fn try_from(value: HpkeReply) -> Result { + match value { + HpkeReply::OpenKeyFromFile(this) => Ok(this), + _ => Err(Error::InternalError), + } + } +} + +/// Open with a private key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeOpenReply { + pub plaintext: Message, +} + +/// Open with a private key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeOpenKeyReply { + pub key: KeyId, +} + +/// Open with a private key +/// +/// As described in 6.1 with mode "base" +#[derive(Deserialize, Serialize)] +pub struct HpkeOpenKeyFromFileReply { + pub key: KeyId, +} + +pub type HpkeResult<'a, R, C> = ExtensionResult<'a, HpkeExtension, R, C>; + +pub struct HpkeExtension; + +impl Extension for HpkeExtension { + type Request = HpkeRequest; + type Reply = HpkeReply; +} + +pub trait HpkeClient: ExtensionClient { + fn hpke_seal( + &mut self, + key: KeyId, + plaintext: Message, + aad: ShortData, + info: ShortData, + enc_location: Location, + ) -> HpkeResult<'_, HpkeSealReply, Self> { + self.extension(HpkeRequest::Seal(HpkeSealRequest { + key, + plaintext, + aad, + info, + enc_location, + })) + } + + fn hpke_seal_key( + &mut self, + public_key: KeyId, + key_to_seal: KeyId, + aad: ShortData, + info: ShortData, + ) -> HpkeResult<'_, HpkeSealKeyReply, Self> { + self.extension(HpkeRequest::SealKey(HpkeSealKeyRequest { + public_key, + key_to_seal, + aad, + info, + })) + } + + fn hpke_seal_key_to_file( + &mut self, + file: PathBuf, + location: Location, + public_key: KeyId, + key_to_seal: KeyId, + aad: ShortData, + info: ShortData, + ) -> HpkeResult<'_, HpkeSealKeyToFileReply, Self> { + self.extension(HpkeRequest::SealKeyToFile(HpkeSealKeyToFileRequest { + file, + public_key, + key_to_seal, + aad, + info, + location, + })) + } + + fn hpke_open( + &mut self, + key: KeyId, + enc_key: KeyId, + ciphertext: Message, + tag: ByteArray<16>, + aad: ShortData, + info: ShortData, + ) -> HpkeResult<'_, HpkeOpenReply, Self> { + self.extension(HpkeRequest::Open(HpkeOpenRequest { + key, + tag, + enc_key, + ciphertext, + aad, + info, + })) + } + fn hpke_open_key( + &mut self, + key: KeyId, + sealed_key: Message, + aad: ShortData, + info: ShortData, + location: Location, + ) -> HpkeResult<'_, HpkeOpenKeyReply, Self> { + self.extension(HpkeRequest::OpenKey(HpkeOpenKeyRequest { + key, + sealed_key, + aad, + info, + location, + })) + } + fn hpke_open_key_from_file( + &mut self, + key: KeyId, + sealed_key: PathBuf, + sealed_location: Location, + unsealed_location: Location, + aad: ShortData, + info: ShortData, + ) -> HpkeResult<'_, HpkeOpenKeyFromFileReply, Self> { + self.extension(HpkeRequest::OpenKeyFromFile(HpkeOpenKeyFromFileRequest { + key, + aad, + info, + sealed_key, + sealed_location, + unsealed_location, + })) + } +} + +impl> HpkeClient for T {} diff --git a/src/hpke.rs b/src/hpke.rs new file mode 100644 index 0000000..f5741ae --- /dev/null +++ b/src/hpke.rs @@ -0,0 +1,625 @@ +// Copyright (C) Nitrokey GmbH +// SPDX-License-Identifier: Apache-2.0 or MIT + +use crate::StagingBackend; + +use trussed::{ + config::MAX_SERIALIZED_KEY_LENGTH, + key, + serde_extensions::ExtensionImpl, + service::Filestore, + store::keystore::Keystore, + types::{KeyId, Message}, + Bytes, +}; +use trussed_hpke::*; + +type HkdfSha256 = hkdf::Hkdf; +type HkdfSha256Extract = hkdf::HkdfExtract; + +use rand_core::{CryptoRng, RngCore}; +use salty::agreement as x25519; + +const X25519_KEM_SUITE_ID: &[u8] = b"KEM\x00\x20"; +const X25519_HKDF_SHA256_CHACHA20_POLY1305_HPKE_SUITE_ID: &[u8] = b"HPKE\x00\x20\x00\x01\x00\x03"; + +fn labeled_extract( + suite_id: &[u8], + salt: &[u8], + label: &[u8], + ikm: &[u8], +) -> (HkdfSha256, [u8; 32]) { + let mut extract_ctx = HkdfSha256Extract::new(Some(salt)); + extract_ctx.input_ikm(b"HPKE-v1"); + extract_ctx.input_ikm(suite_id); + extract_ctx.input_ikm(label); + extract_ctx.input_ikm(ikm); + let (prk, hkdf) = extract_ctx.finalize(); + (hkdf, prk.into()) +} + +fn labeled_expand( + suite_id: &[u8], + prk: &HkdfSha256, + label: &[u8], + info: &[u8], + buffer: &mut [u8], +) -> Result<(), hkdf::InvalidLength> { + let Ok(l): Result = buffer.len().try_into() else { + return Err(hkdf::InvalidLength); + }; + prk.expand_multi_info( + &[&l.to_be_bytes(), b"HPKE-v1", suite_id, label, info], + buffer, + ) +} + +fn extract_and_expand(dh: x25519::SharedSecret, kem_context: &[u8]) -> [u8; 32] { + let (prk, _) = labeled_extract(X25519_KEM_SUITE_ID, b"", b"eae_prk", &dh.to_bytes()); + let mut shr = [0; 32]; + labeled_expand( + X25519_KEM_SUITE_ID, + &prk, + b"shared_secret", + kem_context, + &mut shr, + ) + .map_err(|_err| { + error!("Length of shr is known to be OK: {_err:?}"); + }) + .unwrap(); + shr +} + +fn encap( + pkr: x25519::PublicKey, + cspnrg: &mut R, +) -> ([u8; 32], x25519::PublicKey) { + let seed = &mut [0; 32]; + cspnrg.fill_bytes(seed); + let secret = x25519::SecretKey::from_seed(seed); + let dh = secret.agree(&pkr); + let enc = secret.public(); + + let kem_context = &mut [0; 64]; + kem_context[0..32].copy_from_slice(&enc.to_bytes()); + kem_context[32..].copy_from_slice(&pkr.to_bytes()); + let shared_secret = extract_and_expand(dh, kem_context); + (shared_secret, enc) +} + +fn decap(enc: x25519::PublicKey, skr: x25519::SecretKey) -> [u8; 32] { + let dh = skr.agree(&enc); + let kem_context = &mut [0; 64]; + kem_context[0..32].copy_from_slice(&enc.to_bytes()); + kem_context[32..].copy_from_slice(&skr.public().to_bytes()); + extract_and_expand(dh, kem_context) +} + +enum Role { + Sender, + Receiver, +} + +const MODE_BASE: u8 = 0x00; + +#[cfg_attr(test, derive(Clone))] +struct Context { + key: [u8; NK], + base_nonce: [u8; NN], + /// Used only in tests for comparison with the test vectors + #[allow(unused)] + exporter_secret: [u8; NH], + // Our limited version only allows one encryption/decryption + // seq: u128, +} + +trait Aead: + AeadMutInPlace + + KeyInit::KeySize> + + AeadCore< + NonceSize = ::NonceSize, + TagSize = ::TagSize, + > +{ + const AEAD_ID: u16; + const X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID: &'static [u8]; +} + +impl Aead for ChaCha20Poly1305 { + const AEAD_ID: u16 = 0x0003; + const X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID: &'static [u8] = + X25519_HKDF_SHA256_CHACHA20_POLY1305_HPKE_SUITE_ID; +} + +impl Aead for ChaCha8Poly1305 { + /// Custom non-standard Id + const AEAD_ID: u16 = 0xFFFE; + const X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID: &'static [u8] = b"HPKE\x00\x20\x00\x01\xFF\xFE"; +} + +const NK: usize = 32; +const NN: usize = 12; +const NH: usize = 32; + +fn key_schedule(_role: Role, shared_secret: [u8; 32], info: &[u8]) -> Context { + let (_, psk_id_hash) = labeled_extract( + T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, + b"", + b"psk_id_hash", + b"", + ); + let (_, info_hash) = labeled_extract( + T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, + b"", + b"info_hash", + info, + ); + let mut key_schedule_context = [0; 65]; + key_schedule_context[0] = MODE_BASE; + key_schedule_context[1..33].copy_from_slice(&psk_id_hash); + key_schedule_context[33..].copy_from_slice(&info_hash); + let (secret, _) = labeled_extract( + T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, + &shared_secret, + b"secret", + b"", + ); + let mut key = [0; NK]; + labeled_expand( + T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, + &secret, + b"key", + &key_schedule_context, + &mut key, + ) + .map_err(|_err| { + error!("KEY is not too large: {_err:?}"); + }) + .unwrap(); + let mut base_nonce = [0; NN]; + labeled_expand( + T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, + &secret, + b"base_nonce", + &key_schedule_context, + &mut base_nonce, + ) + .map_err(|_err| { + error!("NONCE is not too large: {_err:?}"); + }) + .unwrap(); + let mut exporter_secret = [0; NH]; + labeled_expand( + T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, + &secret, + b"exp", + &key_schedule_context, + &mut exporter_secret, + ) + .map_err(|_err| { + error!("EXP is not too large: {_err:?}"); + }) + .unwrap(); + Context { + key, + base_nonce, + exporter_secret, + } +} + +fn setup_base_s( + pkr: x25519::PublicKey, + info: &[u8], + cspnrg: &mut R, +) -> (x25519::PublicKey, Context) { + let (shared_secret, enc) = encap(pkr, cspnrg); + (enc, key_schedule::(Role::Sender, shared_secret, info)) +} + +fn setup_base_r(enc: x25519::PublicKey, skr: x25519::SecretKey, info: &[u8]) -> Context { + let shared_secret = decap(enc, skr); + key_schedule::(Role::Receiver, shared_secret, info) +} + +const TAG_LEN: usize = 16; + +use chacha20poly1305::{ + aead::{AeadCore, AeadMutInPlace, KeyInit, KeySizeUser}, + ChaCha20Poly1305, ChaCha8Poly1305, +}; + +impl Context { + fn seal_in_place_detached(self, aad: &[u8], plaintext: &mut [u8]) -> [u8; TAG_LEN] { + // We don't increment because the simplified API only allows 1 encryption + let nonce = (&self.base_nonce).into(); + let mut aead = T::new((&self.key).into()); + let tag = aead + .encrypt_in_place_detached(nonce, aad, plaintext) + .map_err(|_err| { + error!("Not used to encrypt data too large: {_err:?}"); + }) + .unwrap(); + + tag.into() + } + + fn open_in_place_detached( + self, + aad: &[u8], + ciphertext: &mut [u8], + tag: [u8; TAG_LEN], + ) -> Result<(), aead::Error> { + let nonce = (&self.base_nonce).into(); + let mut aead = T::new((&self.key).into()); + aead.decrypt_in_place_detached(nonce, aad, ciphertext, (&tag).into()) + } +} + +fn seal( + pkr: x25519::PublicKey, + info: &[u8], + aad: &[u8], + plaintext: &mut [u8], + csprng: &mut R, +) -> (x25519::PublicKey, [u8; TAG_LEN]) { + let (enc, ctx) = setup_base_s::<_, T>(pkr, info, csprng); + let tag = ctx.seal_in_place_detached::(aad, plaintext); + (enc, tag) +} +/// Seal with X25519-HKDF-SHA256-ChaCha8Poly1305 suite +fn seal8( + pkr: x25519::PublicKey, + info: &[u8], + aad: &[u8], + plaintext: &mut [u8], + csprng: &mut R, +) -> (x25519::PublicKey, [u8; TAG_LEN]) { + seal::(pkr, info, aad, plaintext, csprng) +} + +fn open( + enc: x25519::PublicKey, + skr: x25519::SecretKey, + info: &[u8], + aad: &[u8], + ciphertext: &mut [u8], + tag: [u8; TAG_LEN], +) -> Result<(), aead::Error> { + let ctx = setup_base_r::(enc, skr, info); + ctx.open_in_place_detached::(aad, ciphertext, tag) +} + +/// Open with X25519-HKDF-SHA256-ChaCha20Poly1305 suite +fn open8( + enc: x25519::PublicKey, + skr: x25519::SecretKey, + info: &[u8], + aad: &[u8], + ciphertext: &mut [u8], + tag: [u8; TAG_LEN], +) -> Result<(), aead::Error> { + open::(enc, skr, info, aad, ciphertext, tag) +} + +fn load_public_key( + key_id: &KeyId, + keystore: &mut impl Keystore, +) -> Result { + let public_bytes: [u8; 32] = keystore + .load_key(key::Secrecy::Public, Some(key::Kind::X255), key_id)? + .material + .as_slice() + .try_into() + .map_err(|_| trussed::Error::InternalError)?; + let public_key = x25519::PublicKey::from(public_bytes); + Ok(public_key) +} + +fn load_secret_key( + key_id: &KeyId, + keystore: &mut impl Keystore, +) -> Result { + let secret_bytes: [u8; 32] = keystore + .load_key(key::Secrecy::Secret, Some(key::Kind::X255), key_id)? + .material + .as_slice() + .try_into() + .map_err(|_| trussed::Error::InternalError)?; + let secret_key = x25519::SecretKey::from_seed(&secret_bytes); + Ok(secret_key) +} + +impl ExtensionImpl for StagingBackend { + fn extension_request( + &mut self, + core_ctx: &mut trussed::types::CoreContext, + _backend_ctx: &mut Self::Context, + request: &::Request, + resources: &mut trussed::service::ServiceResources

, + ) -> Result<::Reply, trussed::Error> + { + let filestore = &mut resources.filestore(core_ctx.path.clone()); + let keystore = &mut resources.keystore(core_ctx.path.clone())?; + + match request { + HpkeRequest::Seal(req) => { + let mut pt = req.plaintext.clone(); + let public_key = load_public_key(&req.key, keystore)?; + let (pk, tag) = seal8(public_key, &req.info, &req.aad, &mut pt, keystore.rng()); + let enc = keystore.store_key( + req.enc_location, + key::Secrecy::Public, + key::Kind::X255, + &pk.to_bytes(), + )?; + Ok(HpkeSealReply { + enc, + ciphertext: pt, + tag: tag.into(), + } + .into()) + } + HpkeRequest::SealKey(req) => { + // TODO: need to check both secret and public keys + let serialized_key = + keystore.load_key(key::Secrecy::Secret, None, &req.key_to_seal)?; + let mut message = Message::from_slice(&serialized_key.serialize()).unwrap(); + + let public_key = load_public_key(&req.public_key, keystore)?; + + let (pk, tag) = seal8( + public_key, + &req.info, + &req.aad, + &mut message, + keystore.rng(), + ); + + message + .extend_from_slice(&pk.to_bytes()) + .map_err(|_| trussed::Error::SignDataTooLarge)?; + message + .extend_from_slice(&tag) + .map_err(|_| trussed::Error::SignDataTooLarge)?; + + Ok(HpkeSealKeyReply { data: message }.into()) + } + HpkeRequest::SealKeyToFile(req) => { + // TODO: need to check both secret and public keys + let serialized_key = + keystore.load_key(key::Secrecy::Secret, None, &req.key_to_seal)?; + let mut message = Bytes::<{ MAX_SERIALIZED_KEY_LENGTH + 32 + 16 }>::from_slice( + &serialized_key.serialize(), + ) + .unwrap(); + + let public_key = load_public_key(&req.public_key, keystore)?; + + let (pk, tag) = seal8( + public_key, + &req.info, + &req.aad, + &mut message, + keystore.rng(), + ); + + message + .extend_from_slice(&pk.to_bytes()) + .map_err(|_| trussed::Error::SignDataTooLarge)?; + message + .extend_from_slice(&tag) + .map_err(|_| trussed::Error::SignDataTooLarge)?; + filestore.write(&req.file, req.location, &message)?; + + Ok(HpkeSealKeyToFileReply {}.into()) + } + HpkeRequest::Open(req) => { + let enc = load_public_key(&req.enc_key, keystore)?; + let secret_key = load_secret_key(&req.key, keystore)?; + + let mut ct = req.ciphertext.clone(); + open8( + enc, + secret_key, + &req.info, + &req.aad, + &mut ct, + req.tag.into(), + ) + .map_err(|_| trussed::Error::AeadError)?; + + Ok(HpkeOpenReply { plaintext: ct }.into()) + } + HpkeRequest::OpenKey(req) => { + let secret_key = load_secret_key(&req.key, keystore)?; + let mut ct = req.sealed_key.clone(); + let (ct, tag) = ct.split_last_chunk_mut().ok_or(trussed::Error::AeadError)?; + let (ct, enc_bytes) = ct.split_last_chunk_mut().ok_or(trussed::Error::AeadError)?; + let enc = x25519::PublicKey::from(*enc_bytes); + + open8(enc, secret_key, &req.info, &req.aad, ct, *tag) + .map_err(|_| trussed::Error::AeadError)?; + + let key::Key { + flags: _, + kind, + material, + } = key::Key::try_deserialize(ct)?; + + let key = + keystore.store_key(req.location, key::Secrecy::Secret, kind, &material)?; + + Ok(HpkeOpenKeyReply { key }.into()) + } + HpkeRequest::OpenKeyFromFile(req) => { + let secret_key = load_secret_key(&req.key, keystore)?; + let mut ct: Bytes<{ MAX_SERIALIZED_KEY_LENGTH + 32 + 16 }> = + filestore.read(&req.sealed_key, req.sealed_location)?; + let (ct, tag) = ct.split_last_chunk_mut().ok_or(trussed::Error::AeadError)?; + let (ct, enc_bytes) = ct.split_last_chunk_mut().ok_or(trussed::Error::AeadError)?; + let enc = x25519::PublicKey::from(*enc_bytes); + + open8(enc, secret_key, &req.info, &req.aad, ct, *tag) + .map_err(|_| trussed::Error::AeadError)?; + + let key::Key { + flags: _, + kind, + material, + } = key::Key::try_deserialize(ct)?; + + let key = keystore.store_key( + req.unsealed_location, + key::Secrecy::Secret, + kind, + &material, + )?; + + Ok(HpkeOpenKeyFromFileReply { key }.into()) + } + } + } +} + +#[cfg(test)] +mod tests { + use core::num::NonZeroU32; + + use hex_literal::hex; + + use super::*; + + struct TestRng<'a>(&'a [u8]); + impl<'a> CryptoRng for TestRng<'a> {} + impl<'a> RngCore for TestRng<'a> { + fn next_u32(&mut self) -> u32 { + let (value, rem) = self.0.split_first_chunk().unwrap(); + self.0 = rem; + u32::from_be_bytes(*value) + } + fn next_u64(&mut self) -> u64 { + let (value, rem) = self.0.split_first_chunk().unwrap(); + self.0 = rem; + u64::from_be_bytes(*value) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let (value, rem) = self.0.split_at(dest.len()); + self.0 = rem; + dest.copy_from_slice(value); + } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + if self.0.len() < dest.len() { + let error_code: NonZeroU32 = rand_core::Error::CUSTOM_START.try_into().unwrap(); + return Err(rand_core::Error::from(error_code)); + } + self.fill_bytes(dest); + Ok(()) + } + } + + /// Seal with X25519-HKDF-SHA256-ChaCha20Poly1305 suite + fn seal20( + pkr: x25519::PublicKey, + info: &[u8], + aad: &[u8], + plaintext: &mut [u8], + csprng: &mut R, + ) -> (x25519::PublicKey, [u8; TAG_LEN]) { + seal::(pkr, info, aad, plaintext, csprng) + } + + /// Open with X25519-HKDF-SHA256-ChaCha20Poly1305 suite + fn open20( + enc: x25519::PublicKey, + skr: x25519::SecretKey, + info: &[u8], + aad: &[u8], + ciphertext: &mut [u8], + tag: [u8; TAG_LEN], + ) -> Result<(), aead::Error> { + open::(enc, skr, info, aad, ciphertext, tag) + } + + #[allow(non_snake_case)] + #[test] + fn chacha20() { + let info = hex!("4f6465206f6e2061204772656369616e2055726e"); + let pkEm = hex!("1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a"); + let skEm = hex!("f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600"); + let alice_sk = x25519::SecretKey::from_seed(&skEm); + assert_eq!(pkEm, alice_sk.public().to_bytes()); + let pkRm = hex!("4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a"); + let skRm = hex!("8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb"); + let bob_sk = x25519::SecretKey::from_seed(&skRm); + assert_eq!(pkRm, bob_sk.public().to_bytes()); + let expected_shared_secret = + hex!("0bbe78490412b4bbea4812666f7916932b828bba79942424abb65244930d69a7"); + let (shared_secret, enc) = encap(bob_sk.public(), &mut TestRng(&skEm)); + assert_eq!(enc.to_bytes(), pkEm); + assert_eq!(shared_secret, expected_shared_secret); + + assert_eq!( + decap(alice_sk.public(), bob_sk.clone()), + expected_shared_secret + ); + let (enc, ctx) = + setup_base_s::<_, ChaCha20Poly1305>(bob_sk.public(), &info, &mut TestRng(&skEm)); + assert_eq!(enc.to_bytes(), pkEm); + assert_eq!( + ctx.key, + hex!("ad2744de8e17f4ebba575b3f5f5a8fa1f69c2a07f6e7500bc60ca6e3e3ec1c91") + ); + assert_eq!(ctx.base_nonce, hex!("5c4d98150661b848853b547f")); + assert_eq!( + ctx.exporter_secret, + hex!("a3b010d4994890e2c6968a36f64470d3c824c8f5029942feb11e7a74b2921922") + ); + + let pt = hex!("4265617574792069732074727574682c20747275746820626561757479"); + let mut buffer = pt; + let aad = hex!("436f756e742d30"); + let ct = hex!("1c5250d8034ec2b784ba2cfd69dbdb8af406cfe3ff938e131f0def8c8b"); + let expected_tag = hex!("60b4db21993c62ce81883d2dd1b51a28"); + + let (enc, tag) = seal20( + bob_sk.public(), + &info, + &aad, + &mut buffer, + &mut TestRng(&skEm), + ); + assert_eq!(enc.to_bytes(), pkEm); + assert_eq!(buffer, ct); + assert_eq!(tag, expected_tag); + open20(enc, bob_sk, &info, &aad, &mut buffer, tag).unwrap(); + assert_eq!(buffer, pt); + } + + const X25519_KEM_ID: u16 = 0x0020; + const HKDF_SHA256_KDF_ID: u16 = 0x0001; + fn assert_suite_id() { + let calculated_id: Vec = b"HPKE" + .iter() + .copied() + .chain(X25519_KEM_ID.to_be_bytes()) + .chain(HKDF_SHA256_KDF_ID.to_be_bytes()) + .chain(T::AEAD_ID.to_be_bytes()) + .collect(); + assert_eq!(T::X25519_HKDF_SHA256_SELF_HPKE_SUITE_ID, &calculated_id); + } + + #[test] + fn ids() { + let calculated_id: Vec = b"KEM" + .iter() + .copied() + .chain(X25519_KEM_ID.to_be_bytes()) + .collect(); + assert_eq!(X25519_KEM_SUITE_ID, &calculated_id); + + assert_suite_id::(); + assert_suite_id::(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5b197dc..b70f14e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,9 @@ mod chunked; #[cfg(feature = "hkdf")] mod hkdf; +#[cfg(feature = "hpke")] +mod hpke; + #[cfg(feature = "manage")] mod manage; #[cfg(feature = "manage")] diff --git a/src/virt.rs b/src/virt.rs index 61a1941..ed1c8b1 100644 --- a/src/virt.rs +++ b/src/virt.rs @@ -12,6 +12,8 @@ use trussed_chunked::ChunkedExtension; use trussed_fs_info::FsInfoExtension; #[cfg(feature = "hkdf")] use trussed_hkdf::HkdfExtension; +#[cfg(feature = "hpke")] +use trussed_hpke::HpkeExtension; #[cfg(feature = "manage")] use trussed_manage::ManageExtension; #[cfg(feature = "wrap-key-to-file")] @@ -41,6 +43,8 @@ pub enum ExtensionIds { WrapKeyToFile, #[cfg(feature = "fs-info")] FsInfo, + #[cfg(feature = "hpke")] + Hpke, } #[cfg(feature = "chunked")] @@ -73,6 +77,12 @@ impl ExtensionId for Dispatcher { const ID: ExtensionIds = ExtensionIds::FsInfo; } +#[cfg(feature = "hpke")] +impl ExtensionId for Dispatcher { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Hpke; +} + impl From for u8 { fn from(value: ExtensionIds) -> Self { match value { @@ -86,6 +96,8 @@ impl From for u8 { ExtensionIds::WrapKeyToFile => 3, #[cfg(feature = "fs-info")] ExtensionIds::FsInfo => 4, + #[cfg(feature = "hpke")] + ExtensionIds::Hpke => 5, } } } @@ -104,6 +116,8 @@ impl TryFrom for ExtensionIds { 3 => Ok(Self::WrapKeyToFile), #[cfg(feature = "fs-info")] 4 => Ok(Self::FsInfo), + #[cfg(feature = "hpke")] + 5 => Ok(Self::Hpke), _ => Err(Error::FunctionNotSupported), } } @@ -186,6 +200,14 @@ impl ExtensionDispatch for Dispatcher { request, resources, ), + #[cfg(feature = "hpke")] + ExtensionIds::Hpke => ExtensionImpl::::extension_request_serialized( + &mut self.backend, + &mut ctx.core, + &mut ctx.backends, + request, + resources, + ), } } } diff --git a/tests/hpke.rs b/tests/hpke.rs new file mode 100644 index 0000000..900c809 --- /dev/null +++ b/tests/hpke.rs @@ -0,0 +1,119 @@ +// Copyright (C) Nitrokey GmbH +// SPDX-License-Identifier: Apache-2.0 or MIT + +#![cfg(all(feature = "virt", feature = "hpke"))] + +use littlefs2::path; +use trussed::client::{CryptoClient, X255}; +use trussed::{ + syscall, + types::{Bytes, KeyId, Location, Mechanism, SignatureSerialization}, +}; + +use trussed_hpke::HpkeClient; + +use trussed_staging::virt; + +fn assert_symkey_eq(this: KeyId, other: KeyId, client: &mut C) { + let hmac_this = syscall!(client.sign( + Mechanism::HmacSha256, + this, + b"DATA", + SignatureSerialization::Raw + )) + .signature; + let hmac_other = syscall!(client.sign( + Mechanism::HmacSha256, + other, + b"DATA", + SignatureSerialization::Raw + )) + .signature; + + assert_eq!(hmac_other, hmac_this); +} + +#[test] +fn hpke_message() { + virt::with_ram_client("hpke_test_message", |mut client| { + let secret_key = syscall!(client.generate_x255_secret_key(Location::Volatile)).key; + let public_key = + syscall!(client.derive_x255_public_key(secret_key, Location::Volatile)).key; + + let pl = Bytes::from_slice(b"Plaintext").unwrap(); + let aad = Bytes::from_slice(b"AAD").unwrap(); + let info = Bytes::from_slice(b"INFO").unwrap(); + let seal = syscall!(client.hpke_seal( + public_key, + pl.clone(), + aad.clone(), + info.clone(), + Location::Volatile + )); + + assert!(seal.ciphertext != b"Plaintext"); + + let opened = + syscall!(client.hpke_open(secret_key, seal.enc, seal.ciphertext, seal.tag, aad, info)); + assert_eq!(opened.plaintext, pl); + }) +} + +#[test] +fn hpke_wrap_key() { + virt::with_ram_client("hpke_test_wrap_key", |mut client| { + let secret_key = syscall!(client.generate_x255_secret_key(Location::Volatile)).key; + let public_key = + syscall!(client.derive_x255_public_key(secret_key, Location::Volatile)).key; + + let key_to_wrap = syscall!(client.generate_secret_key(32, Location::Volatile)).key; + + let aad = Bytes::from_slice(b"AAD").unwrap(); + let info = Bytes::from_slice(b"INFO").unwrap(); + let seal = + syscall!(client.hpke_seal_key(public_key, key_to_wrap, aad.clone(), info.clone())); + + let unwrapped = + syscall!(client.hpke_open_key(secret_key, seal.data, aad, info, Location::Volatile)) + .key; + assert_ne!(unwrapped, key_to_wrap); + + assert_symkey_eq(key_to_wrap, unwrapped, &mut client); + }) +} + +#[test] +fn hpke_wrap_key_to_file() { + virt::with_ram_client("hpke_test_wrap_key_to_file", |mut client| { + let secret_key = syscall!(client.generate_x255_secret_key(Location::Volatile)).key; + let public_key = + syscall!(client.derive_x255_public_key(secret_key, Location::Volatile)).key; + + let key_to_wrap = syscall!(client.generate_secret_key(32, Location::Volatile)).key; + + let path = path!("WRAPPED_KEY"); + let aad = Bytes::from_slice(b"AAD").unwrap(); + let info = Bytes::from_slice(b"INFO").unwrap(); + syscall!(client.hpke_seal_key_to_file( + path.into(), + Location::Volatile, + public_key, + key_to_wrap, + aad.clone(), + info.clone() + )); + + let unwrapped = syscall!(client.hpke_open_key_from_file( + secret_key, + path.into(), + Location::Volatile, + Location::Volatile, + aad, + info + )) + .key; + assert_ne!(unwrapped, key_to_wrap); + + assert_symkey_eq(key_to_wrap, unwrapped, &mut client); + }) +}