From 5aa431c805b7645b6d86b97dd0f16986f3884876 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Tue, 21 Nov 2023 08:18:46 -0800 Subject: [PATCH] wrap: implement encryption/decryption of messages (#494) This also provides for deserialization of asymmetric private keys. --- Cargo.lock | 3 + Cargo.toml | 10 +-- src/mockhsm/error.rs | 4 -- src/mockhsm/object.rs | 8 ++- src/mockhsm/object/objects.rs | 55 +++++++++++----- src/wrap.rs | 2 + src/wrap/info.rs | 76 +++++++++++++++++++++ src/wrap/key.rs | 73 +++++++++++++++++++++ src/wrap/message.rs | 113 +++++++++++++++++++++++++++++++- src/wrap/nonce.rs | 7 +- tests/command/export_wrapped.rs | 65 ++++++++++++++++++ 11 files changed, 384 insertions(+), 32 deletions(-) create mode 100644 src/wrap/info.rs diff --git a/Cargo.lock b/Cargo.lock index 48fef4e3..48bf8c1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,7 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -939,6 +940,8 @@ dependencies = [ "hmac", "k256", "log", + "num-bigint-dig", + "num-traits", "once_cell", "p256", "p384", diff --git a/Cargo.toml b/Cargo.toml index 1134cf96..d9b8b661 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,17 +17,21 @@ edition = "2021" rust-version = "1.67" [dependencies] -aes = "0.8" +aes = { version = "0.8", features = ["zeroize"] } bitflags = "2" cmac = "0.7" cbc = "0.1" +ccm = { version = "0.5", features = ["std"] } ecdsa = { version = "0.16", default-features = false } ed25519 = "2" log = "0.4" +num-bigint = { version = "0.8.2", features = ["i128", "prime", "zeroize"], default-features = false, package = "num-bigint-dig" } +num-traits = "0.2" p256 = { version = "0.13", default-features = false, features = ["ecdsa"] } p384 = { version = "0.13", default-features = false, features = ["ecdsa"] } serde = { version = "1", features = ["serde_derive"] } rand_core = { version = "0.6", features = ["std"] } +rsa = "0.9" signature = { version = "2", features = ["derive"] } subtle = "2" thiserror = "1" @@ -36,7 +40,6 @@ uuid = { version = "1", default-features = false } zeroize = { version = "1", features = ["zeroize_derive"] } # optional dependencies -ccm = { version = "0.5", optional = true, features = ["std"] } digest = { version = "0.10", optional = true, default-features = false } ed25519-dalek = { version = "2", optional = true, features = ["rand_core"] } hmac = { version = "0.12", optional = true } @@ -51,13 +54,12 @@ tiny_http = { version = "0.12", optional = true } ed25519-dalek = "2" once_cell = "1" p256 = { version = "0.13", features = ["ecdsa"] } -rsa = "0.9" [features] default = ["http", "passwords", "setup"] http-server = ["tiny_http"] http = [] -mockhsm = ["ccm", "digest", "ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"] +mockhsm = ["digest", "ecdsa/arithmetic", "ed25519-dalek", "p256/ecdsa", "secp256k1"] passwords = ["hmac", "pbkdf2", "sha2"] secp256k1 = ["k256"] setup = ["passwords", "serde_json", "uuid/serde"] diff --git a/src/mockhsm/error.rs b/src/mockhsm/error.rs index 41c0e2e6..0a2f1f3c 100644 --- a/src/mockhsm/error.rs +++ b/src/mockhsm/error.rs @@ -20,10 +20,6 @@ pub enum ErrorKind { /// Object does not exist #[error("object not found")] ObjectNotFound, - - /// Unsupported algorithm - #[error("unsupported algorithm")] - UnsupportedAlgorithm, } impl ErrorKind { diff --git a/src/mockhsm/object.rs b/src/mockhsm/object.rs index 0f2b9e4b..b6c3e30f 100644 --- a/src/mockhsm/object.rs +++ b/src/mockhsm/object.rs @@ -6,7 +6,7 @@ mod objects; mod payload; pub(crate) use self::{objects::Objects, payload::Payload}; -use crate::{object, Algorithm}; +use crate::{object, wrap, Algorithm}; use serde::{Deserialize, Serialize}; /// Label for the default auth key @@ -34,14 +34,16 @@ impl Object { /// A serialized object which can be exported/imported #[derive(Serialize, Deserialize, Debug)] pub(crate) struct WrappedObject { - pub object_info: object::Info, + pub alg_id: Algorithm, + pub object_info: wrap::Info, pub data: Vec, } impl<'a> From<&'a Object> for WrappedObject { fn from(obj: &'a Object) -> Self { Self { - object_info: obj.object_info.clone(), + alg_id: Algorithm::Wrap(wrap::Algorithm::Aes128Ccm), + object_info: obj.object_info.clone().into(), data: obj.payload.to_bytes(), } } diff --git a/src/mockhsm/object/objects.rs b/src/mockhsm/object/objects.rs index 6663a18d..51b7fc6b 100644 --- a/src/mockhsm/object/objects.rs +++ b/src/mockhsm/object/objects.rs @@ -8,24 +8,30 @@ use crate::{ serialization::{deserialize, serialize}, wrap, Algorithm, Capability, Domain, }; -use aes::cipher::consts::{U13, U8}; +use aes::cipher::consts::{U13, U16}; use ccm::aead::{AeadInPlace, KeyInit}; use std::collections::{btree_map::Iter as MapIter, BTreeMap as Map}; /// AES-CCM with a 128-bit key -pub(crate) type Aes128Ccm = ccm::Ccm; +pub(crate) type Aes128Ccm = ccm::Ccm; + +/// AES-CCM with a 192-bit key +pub(crate) type Aes192Ccm = ccm::Ccm; /// AES-CCM with a 256-bit key -pub(crate) type Aes256Ccm = ccm::Ccm; +pub(crate) type Aes256Ccm = ccm::Ccm; /// AES-CCM key #[allow(clippy::large_enum_variant)] pub(crate) enum AesCcmKey { /// AES-CCM with a 128-bit key - Aes128Ccm(Aes128Ccm), + Aes128(Aes128Ccm), + + /// AES-CCM with a 192-bit key + Aes192(Aes192Ccm), /// AES-CCM with a 256-bit key - Aes256Ccm(Aes256Ccm), + Aes256(Aes256Ccm), } impl AesCcmKey { @@ -38,10 +44,13 @@ impl AesCcmKey { buffer: &mut Vec, ) -> Result<(), Error> { match self { - AesCcmKey::Aes128Ccm(ccm) => { + AesCcmKey::Aes128(ccm) => { + ccm.encrypt_in_place(&nonce.0.into(), associated_data, buffer) + } + AesCcmKey::Aes192(ccm) => { ccm.encrypt_in_place(&nonce.0.into(), associated_data, buffer) } - AesCcmKey::Aes256Ccm(ccm) => { + AesCcmKey::Aes256(ccm) => { ccm.encrypt_in_place(&nonce.0.into(), associated_data, buffer) } } @@ -57,15 +66,26 @@ impl AesCcmKey { buffer: &mut Vec, ) -> Result<(), Error> { match self { - AesCcmKey::Aes128Ccm(ccm) => { + AesCcmKey::Aes128(ccm) => { ccm.decrypt_in_place(&nonce.0.into(), associated_data, buffer) } - AesCcmKey::Aes256Ccm(ccm) => { + AesCcmKey::Aes192(ccm) => { + ccm.decrypt_in_place(&nonce.0.into(), associated_data, buffer) + } + AesCcmKey::Aes256(ccm) => { ccm.decrypt_in_place(&nonce.0.into(), associated_data, buffer) } } .map_err(|_| format_err!(ErrorKind::CryptoError, "error decrypting wrapped object!").into()) } + + fn algorithm(&self) -> Algorithm { + match self { + AesCcmKey::Aes128(_) => Algorithm::Wrap(wrap::Algorithm::Aes128Ccm), + AesCcmKey::Aes192(_) => Algorithm::Wrap(wrap::Algorithm::Aes192Ccm), + AesCcmKey::Aes256(_) => Algorithm::Wrap(wrap::Algorithm::Aes256Ccm), + } + } } /// Objects stored in the `MockHsm` @@ -235,7 +255,8 @@ impl Objects { } let mut wrapped_object = serialize(&WrappedObject { - object_info, + alg_id: wrap_key.algorithm(), + object_info: object_info.into(), data: object_to_wrap.payload.to_bytes(), }) .unwrap(); @@ -271,7 +292,7 @@ impl Objects { ); let object = Object { - object_info: unwrapped_object.object_info, + object_info: unwrapped_object.object_info.into(), payload, }; @@ -297,17 +318,15 @@ impl Objects { }; match wrap_key.algorithm().wrap().unwrap() { - wrap::Algorithm::Aes128Ccm => Ok(AesCcmKey::Aes128Ccm( + wrap::Algorithm::Aes128Ccm => Ok(AesCcmKey::Aes128( Aes128Ccm::new_from_slice(&wrap_key.payload.to_bytes()).unwrap(), )), - wrap::Algorithm::Aes256Ccm => Ok(AesCcmKey::Aes256Ccm( + wrap::Algorithm::Aes192Ccm => Ok(AesCcmKey::Aes192( + Aes192Ccm::new_from_slice(&wrap_key.payload.to_bytes()).unwrap(), + )), + wrap::Algorithm::Aes256Ccm => Ok(AesCcmKey::Aes256( Aes256Ccm::new_from_slice(&wrap_key.payload.to_bytes()).unwrap(), )), - unsupported => fail!( - ErrorKind::UnsupportedAlgorithm, - "unsupported wrap key algorithm: {:?}", - unsupported - ), } } } diff --git a/src/wrap.rs b/src/wrap.rs index 01e2b87c..8a48a5ec 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -4,6 +4,7 @@ mod algorithm; pub(crate) mod commands; mod error; +mod info; mod key; mod message; mod nonce; @@ -11,6 +12,7 @@ mod nonce; pub use self::{ algorithm::Algorithm, error::{Error, ErrorKind}, + info::Info, key::Key, message::Message, nonce::Nonce, diff --git a/src/wrap/info.rs b/src/wrap/info.rs new file mode 100644 index 00000000..38f1a82b --- /dev/null +++ b/src/wrap/info.rs @@ -0,0 +1,76 @@ +use crate::{ + algorithm, + object::{self, SequenceId}, + Capability, Domain, +}; +use serde::{Deserialize, Serialize}; + +/// Information about an object +/// +/// This is a wrap-specific version of [`object::Info']. It does not carry any +/// delegated_capabilities. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Info { + /// Capabilities (bitfield) + pub capabilities: Capability, + + /// Object identifier + pub object_id: object::Id, + + /// Length of object in bytes + pub length: u16, + + /// Domains from which object is accessible + pub domains: Domain, + + /// Object type + pub object_type: object::Type, + + /// Algorithm this object is intended to be used with + pub algorithm: algorithm::Algorithm, + + /// Sequence: number of times an object with this key ID and type has + /// previously existed + pub sequence: SequenceId, + + /// How did this object originate? (generated, imported, etc) + pub origin: object::Origin, + + /// Label of object + pub label: object::Label, +} + +impl From for Info { + fn from(i: object::Info) -> Self { + Self { + capabilities: i.capabilities, + object_id: i.object_id, + length: i.length, + domains: i.domains, + object_type: i.object_type, + algorithm: i.algorithm, + sequence: i.sequence, + origin: i.origin, + label: i.label, + } + } +} + +impl From for object::Info { + fn from(i: Info) -> Self { + Self { + capabilities: i.capabilities, + object_id: i.object_id, + length: i.length, + domains: i.domains, + object_type: i.object_type, + algorithm: i.algorithm, + sequence: i.sequence, + origin: i.origin, + label: i.label, + // This is a wrapped object, delegate capabilities applies to wrap keys themselves. + // (and authentication keys). + delegated_capabilities: Capability::empty(), + } + } +} diff --git a/src/wrap/key.rs b/src/wrap/key.rs index 173d24dd..1f6a7782 100644 --- a/src/wrap/key.rs +++ b/src/wrap/key.rs @@ -7,10 +7,83 @@ // TODO(tarcieri): use this for `yubihsm::client::put_wrap_key` in general? use crate::{client, device, object, wrap, Capability, Client, Domain}; +use aes::{Aes128, Aes192, Aes256}; +use ccm::{ + consts::{U0, U13, U16}, + AeadCore, AeadInPlace, Ccm, KeyInit, +}; use rand_core::{OsRng, RngCore}; use std::fmt::{self, Debug}; use zeroize::{Zeroize, Zeroizing}; +pub(super) type Aes128Ccm = Ccm; +pub(super) type Aes192Ccm = Ccm; +pub(super) type Aes256Ccm = Ccm; + +pub(super) enum AesCcm { + Aes128(Aes128Ccm), + Aes192(Aes192Ccm), + Aes256(Aes256Ccm), +} + +impl AesCcm { + fn from_bytes(bytes: &[u8]) -> Self { + match bytes.len() { + 16 => Self::Aes128(Aes128Ccm::new_from_slice(bytes).unwrap()), + 24 => Self::Aes192(Aes192Ccm::new_from_slice(bytes).unwrap()), + 32 => Self::Aes256(Aes256Ccm::new_from_slice(bytes).unwrap()), + len => panic!("unexpected length for aesccm {len}"), + } + } +} + +impl From<&Key> for AesCcm { + fn from(key: &Key) -> Self { + Self::from_bytes(&key.data) + } +} + +impl AeadCore for AesCcm { + type NonceSize = U13; + type TagSize = U16; + type CiphertextOverhead = U0; +} + +impl AeadInPlace for AesCcm { + fn encrypt_in_place_detached( + &self, + nonce: &ccm::Nonce, + associated_data: &[u8], + buffer: &mut [u8], + ) -> Result, ccm::Error> { + match self { + Self::Aes128(inner) => inner.encrypt_in_place_detached(nonce, associated_data, buffer), + Self::Aes192(inner) => inner.encrypt_in_place_detached(nonce, associated_data, buffer), + Self::Aes256(inner) => inner.encrypt_in_place_detached(nonce, associated_data, buffer), + } + } + + fn decrypt_in_place_detached( + &self, + nonce: &ccm::Nonce, + associated_data: &[u8], + buffer: &mut [u8], + tag: &ccm::Tag, + ) -> Result<(), ccm::Error> { + match self { + Self::Aes128(inner) => { + inner.decrypt_in_place_detached(nonce, associated_data, buffer, tag) + } + Self::Aes192(inner) => { + inner.decrypt_in_place_detached(nonce, associated_data, buffer, tag) + } + Self::Aes256(inner) => { + inner.decrypt_in_place_detached(nonce, associated_data, buffer, tag) + } + } + } +} + /// Wrap key to import into the device #[derive(Clone)] pub struct Key { diff --git a/src/wrap/message.rs b/src/wrap/message.rs index 381bddb9..1f696a71 100644 --- a/src/wrap/message.rs +++ b/src/wrap/message.rs @@ -1,7 +1,25 @@ //! Wrap messages use super::nonce::{self, Nonce}; -use super::{Error, ErrorKind}; +use super::{Algorithm, Error, ErrorKind}; +use crate::{ + algorithm, asymmetric, + ecdsa::algorithm::CurveAlgorithm, + serialization::{deserialize, serialize}, + wrap, +}; +use aes::cipher::Unsigned; +use ccm::aead::Aead; +use ecdsa::{ + elliptic_curve::{ + sec1::{ModulusSize, ValidatePublicKey}, + FieldBytesSize, SecretKey, + }, + PrimeCurve, +}; +use num_bigint::traits::ModInverse; +use num_traits::{cast::FromPrimitive, identities::One}; +use rsa::{BigUint, RsaPrivateKey}; use serde::{Deserialize, Serialize}; /// Wrap wessage (encrypted HSM object or arbitrary data) encrypted under a wrap key @@ -62,3 +80,96 @@ impl Into> for Message { vec } } + +impl Message { + /// Decrypt the [`Message`] with the provided [`wrap::Key`] + pub fn decrypt(&self, key: &wrap::Key) -> Result { + let cipher: super::key::AesCcm = key.into(); + let plaintext = cipher + .decrypt(&self.nonce.to_nonce(), &*self.ciphertext) + .unwrap(); + + let plaintext = deserialize(&plaintext).unwrap(); + Ok(plaintext) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Plaintext { + pub alg_id: Algorithm, + pub object_info: wrap::Info, + pub data: Vec, +} + +impl Plaintext { + pub fn encrypt(&self, key: &super::Key) -> Result { + let cipher: super::key::AesCcm = key.into(); + let nonce = Nonce::generate(); + let wire = serialize(&self).unwrap(); + let ciphertext = cipher.encrypt(&nonce.to_nonce(), wire.as_slice()).unwrap(); + + Ok(Message { nonce, ciphertext }) + } + + pub fn ecdsa(&self) -> Option> + where + C: PrimeCurve + CurveAlgorithm + ValidatePublicKey, + FieldBytesSize: ModulusSize + Unsigned, + { + if let algorithm::Algorithm::Asymmetric(alg) = self.object_info.algorithm { + if C::asymmetric_algorithm() == alg { + let mut reader = SliceReader(&self.data); + + SecretKey::::from_slice(reader.read(FieldBytesSize::::USIZE)?).ok() + } else { + None + } + } else { + None + } + } + + pub fn rsa(&self) -> Option { + let (component_size, modulus_size) = match self.object_info.algorithm { + algorithm::Algorithm::Asymmetric(asymmetric::Algorithm::Rsa2048) => (128, 256), + algorithm::Algorithm::Asymmetric(asymmetric::Algorithm::Rsa3072) => (192, 384), + algorithm::Algorithm::Asymmetric(asymmetric::Algorithm::Rsa4096) => (256, 512), + _ => return None, + }; + + let mut reader = SliceReader(&self.data); + + let p = BigUint::from_bytes_be(reader.read(component_size)?); + let q = BigUint::from_bytes_be(reader.read(component_size)?); + let dp = BigUint::from_bytes_be(reader.read(component_size)?); + let dq = BigUint::from_bytes_be(reader.read(component_size)?); + let _qinv = BigUint::from_bytes_be(reader.read(component_size)?); + let n = BigUint::from_bytes_be(reader.read(modulus_size)?); + const EXP: u64 = 65537; + let e = BigUint::from_u64(EXP).expect("invalid static exponent"); + + let d = e + .clone() + .mod_inverse((dp - BigUint::one()) * (dq - BigUint::one()))? + .to_biguint()?; + + let private_key = RsaPrivateKey::from_components(n, e, d, vec![p, q]).ok()?; + Some(private_key) + } +} + +/// Support structure to read from a slice like a reader +struct SliceReader<'a>(&'a [u8]); + +impl<'a> SliceReader<'a> { + #[inline] + fn read(&mut self, len: usize) -> Option<&'a [u8]> { + if len > self.0.len() { + None + } else { + let (out, new) = self.0.split_at(len); + self.0 = new; + Some(out) + } + } +} diff --git a/src/wrap/nonce.rs b/src/wrap/nonce.rs index 3025098d..94669b4b 100644 --- a/src/wrap/nonce.rs +++ b/src/wrap/nonce.rs @@ -1,6 +1,6 @@ //! Nonces used by the YubiHSM 2's AES-CCM encrypted `wrap::Message` -#[cfg(feature = "mockhsm")] +use ccm::consts::U13; use rand_core::{OsRng, RngCore}; /// Number of bytes in a nonce used for "wrapping" (i.e AES-CCM encryption) @@ -12,12 +12,15 @@ pub struct Nonce(pub [u8; SIZE]); impl Nonce { /// Generate a random `wrap::Nonce` - #[cfg(feature = "mockhsm")] pub fn generate() -> Self { let mut bytes = [0u8; SIZE]; OsRng.fill_bytes(&mut bytes); Nonce(bytes) } + + pub(crate) fn to_nonce(&self) -> ccm::Nonce { + self.0.into() + } } impl AsRef<[u8]> for Nonce { diff --git a/tests/command/export_wrapped.rs b/tests/command/export_wrapped.rs index 7443c002..6d3efbcc 100644 --- a/tests/command/export_wrapped.rs +++ b/tests/command/export_wrapped.rs @@ -78,3 +78,68 @@ fn wrap_key_test() { TEST_EXPORTED_KEY_LABEL ); } + +#[test] +fn wrap_deserialize() { + let client = crate::get_hsm_client(); + let algorithm = wrap::Algorithm::Aes128Ccm; + let capabilities = Capability::EXPORT_WRAPPED | Capability::IMPORT_WRAPPED; + let delegated_capabilities = Capability::all(); + + clear_test_key_slot(&client, object::Type::WrapKey); + + let key_id = client + .put_wrap_key( + TEST_KEY_ID, + TEST_KEY_LABEL.into(), + TEST_DOMAINS, + capabilities, + delegated_capabilities, + algorithm, + AESCCM_TEST_VECTORS[0].key, + ) + .unwrap_or_else(|err| panic!("error generating wrap key: {err}")); + + assert_eq!(key_id, TEST_KEY_ID); + + // Create a key to export + let exported_key_type = object::Type::AsymmetricKey; + let exported_key_capabilities = Capability::SIGN_ECDSA | Capability::EXPORTABLE_UNDER_WRAP; + let exported_key_algorithm = asymmetric::Algorithm::EcP256; + + let _ = client.delete_object(TEST_EXPORTED_KEY_ID, exported_key_type); + + client + .generate_asymmetric_key( + TEST_EXPORTED_KEY_ID, + TEST_EXPORTED_KEY_LABEL.into(), + TEST_DOMAINS, + exported_key_capabilities, + exported_key_algorithm, + ) + .unwrap_or_else(|err| panic!("error generating asymmetric key: {err}")); + + let wrap_data = client + .export_wrapped(TEST_KEY_ID, exported_key_type, TEST_EXPORTED_KEY_ID) + .unwrap_or_else(|err| panic!("error exporting key: {err}")); + + let wrap_key = wrap::Key::from_bytes(TEST_KEY_ID, AESCCM_TEST_VECTORS[0].key).unwrap(); + + let plaintext = wrap_data + .decrypt(&wrap_key) + .expect("failed to decrypt the wrapped key"); + + let private_key: p256::SecretKey = plaintext + .ecdsa() + .expect("Object did not contain a NistP256 object"); + let public_key: p256::EncodedPoint = private_key.public_key().into(); + + assert_eq!( + client + .get_public_key(TEST_EXPORTED_KEY_ID) + .unwrap_or_else(|err| panic!("error getting public key: {err}")) + .ecdsa::() + .expect("public key was not a NistP256 object"), + public_key + ); +}