diff --git a/Cargo.lock b/Cargo.lock index 5047ac05f..2029b3663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "aes-kw" +version = "0.3.0-pre" +source = "git+https://github.com/RustCrypto/key-wraps.git#15f7dd084793ea67360a8f66f771a8e420c1657f" +dependencies = [ + "aes", + "const-oid", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -65,6 +74,14 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "ansi-x963-kdf" +version = "0.0.1" +source = "git+https://github.com/RustCrypto/KDFs.git#b1d7fe67b3053deef498563adcf415ec631d1cd8" +dependencies = [ + "digest", +] + [[package]] name = "anstyle" version = "1.0.10" @@ -282,11 +299,15 @@ name = "cms" version = "0.3.0-pre.0" dependencies = [ "aes", + "aes-kw", + "ansi-x963-kdf", "cbc", "cipher", "const-oid", "der", + "digest", "ecdsa", + "elliptic-curve", "getrandom 0.3.2", "hex-literal 0.4.1", "p256", @@ -510,6 +531,7 @@ dependencies = [ "digest", "ff", "group", + "hkdf", "hybrid-array", "pem-rfc7468", "pkcs8", @@ -725,6 +747,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" +[[package]] +name = "hkdf" +version = "0.13.0-pre.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aaa7579d1176645cee5dc206aa74873b5b3be479af9606025f9b8905bcf597b" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.13.0-pre.5" diff --git a/Cargo.toml b/Cargo.toml index 68845726c..9f7f7875c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,14 @@ x509-ocsp = { path = "./x509-ocsp" } # https://github.com/RustCrypto/signatures/pull/923 ecdsa = { git = "https://github.com/RustCrypto/signatures.git" } rfc6979 = { git = "https://github.com/RustCrypto/signatures.git" } +# https://github.com/RustCrypto/key-wraps/pull/34 +# https://github.com/RustCrypto/key-wraps/pull/35 +# https://github.com/RustCrypto/key-wraps/pull/39 +aes-kw = { git = "https://github.com/RustCrypto/key-wraps.git" } + + +# https://github.com/RustCrypto/KDFs/pull/102 +ansi-x963-kdf = { git = "https://github.com/RustCrypto/KDFs.git" } # https://github.com/RustCrypto/traits/pull/1777 diff --git a/cms/Cargo.toml b/cms/Cargo.toml index 7d5804bd8..257ebb679 100644 --- a/cms/Cargo.toml +++ b/cms/Cargo.toml @@ -22,8 +22,12 @@ x509-cert = { version = "=0.3.0-pre.0", default-features = false } # optional dependencies aes = { version = "=0.9.0-pre.3", optional = true } +aes-kw = { version ="=0.3.0-pre", optional = true } +ansi-x963-kdf = { version = "0.0.1", optional = true } cbc = { version = "=0.2.0-pre.2", optional = true } cipher = { version = "=0.5.0-pre.8", features = ["alloc", "block-padding", "rand_core"], optional = true } +digest = { version = "0.11.0-pre.10", optional = true } +elliptic-curve = { version = "=0.14.0-rc.1", optional = true } rsa = { version = "=0.10.0-pre.4", optional = true } sha1 = { version = "=0.11.0-pre.5", optional = true } sha2 = { version = "=0.11.0-pre.5", optional = true } @@ -49,8 +53,13 @@ x509-cert = { version = "=0.3.0-pre.0", features = ["pem"] } std = ["der/std", "spki/std"] builder = [ "dep:aes", + "dep:aes-kw", + "dep:ansi-x963-kdf", "dep:cbc", "dep:cipher", + "dep:digest", + "elliptic-curve/ecdh", + "elliptic-curve/pkcs8", "dep:rsa", "dep:sha1", "dep:sha2", diff --git a/cms/src/builder.rs b/cms/src/builder.rs index 817a64edf..cd5f4820e 100644 --- a/cms/src/builder.rs +++ b/cms/src/builder.rs @@ -6,8 +6,8 @@ use crate::cert::CertificateChoices; use crate::content_info::{CmsVersion, ContentInfo}; use crate::enveloped_data::{ EncryptedContentInfo, EncryptedKey, EnvelopedData, KekIdentifier, KeyTransRecipientInfo, - OriginatorIdentifierOrKey, OriginatorInfo, PasswordRecipientInfo, RecipientIdentifier, - RecipientInfo, RecipientInfos, UserKeyingMaterial, + OriginatorInfo, PasswordRecipientInfo, RecipientIdentifier, RecipientInfo, RecipientInfos, + UserKeyingMaterial, }; use crate::revocation::{RevocationInfoChoice, RevocationInfoChoices}; use crate::signed_data::{ @@ -45,6 +45,16 @@ use x509_cert::attr::{Attribute, AttributeValue, Attributes}; use x509_cert::builder::{self, AsyncBuilder, Builder}; use zeroize::Zeroize; +// Modules +mod kari; +mod utils; + +// Exports +pub use kari::{ + DhSinglePassStdDhKdf, EcKeyEncryptionInfo, KeyAgreeRecipientInfoBuilder, KeyAgreementAlgorithm, +}; +pub use utils::kw::KeyWrapAlgorithm; + /// Error type #[derive(Debug)] #[non_exhaustive] @@ -689,64 +699,6 @@ where } } -/// Builds a `KeyAgreeRecipientInfo` according to RFC 5652 § 6. -/// This type uses key agreement: the recipient's public key and the sender's -/// private key are used to generate a pairwise symmetric key, then -/// the content-encryption key is encrypted in the pairwise symmetric key. -pub struct KeyAgreeRecipientInfoBuilder { - /// A CHOICE with three alternatives specifying the sender's key agreement public key. - pub originator: OriginatorIdentifierOrKey, - /// Optional information which helps generating different keys every time. - pub ukm: Option, - /// Encryption algorithm to be used for key encryption - pub key_enc_alg: AlgorithmIdentifierOwned, - _rng: PhantomData, -} - -impl KeyAgreeRecipientInfoBuilder { - /// Creates a `KeyAgreeRecipientInfoBuilder` - pub fn new( - originator: OriginatorIdentifierOrKey, - ukm: Option, - key_enc_alg: AlgorithmIdentifierOwned, - ) -> Result { - Ok(KeyAgreeRecipientInfoBuilder { - originator, - ukm, - key_enc_alg, - _rng: PhantomData, - }) - } -} - -impl RecipientInfoBuilder for KeyAgreeRecipientInfoBuilder -where - R: CryptoRng, -{ - type Rng = R; - - /// Returns the RecipientInfoType - fn recipient_info_type(&self) -> RecipientInfoType { - RecipientInfoType::Kari - } - - /// Returns the `CMSVersion` for this `RecipientInfo` - fn recipient_info_version(&self) -> CmsVersion { - CmsVersion::V3 - } - - /// Build a `KeyAgreeRecipientInfoBuilder`. See RFC 5652 § 6.2.1 - fn build_with_rng( - &mut self, - _content_encryption_key: &[u8], - _rng: &mut Self::Rng, - ) -> Result { - Err(Error::Builder(String::from( - "Building KeyAgreeRecipientInfo is not implemented, yet.", - ))) - } -} - /// Builds a `KekRecipientInfo` according to RFC 5652 § 6. /// Uses symmetric key-encryption keys: the content-encryption key is /// encrypted in a previously distributed symmetric key-encryption key. diff --git a/cms/src/builder/kari.rs b/cms/src/builder/kari.rs new file mode 100644 index 000000000..84cd22f1a --- /dev/null +++ b/cms/src/builder/kari.rs @@ -0,0 +1,467 @@ +//! Key Agreement Recipient Info (Kari) Builder +//! +//! This module contains the building logic for Key Agreement Recipient Info. +//! It partially implements [RFC 5753]. +//! +//! [RFC 5753]: https://datatracker.ietf.org/doc/html/rfc5753 +//! + +// Super imports +use super::{ + AlgorithmIdentifierOwned, CryptoRng, RecipientInfoBuilder, RecipientInfoType, Result, + UserKeyingMaterial, + utils::kw::{KeyWrapAlgorithm, WrappedKey}, +}; + +// Crate imports +#[cfg(doc)] +use crate::enveloped_data::EnvelopedData; +use crate::{ + content_info::CmsVersion, + enveloped_data::{ + EncryptedKey, KeyAgreeRecipientIdentifier, KeyAgreeRecipientInfo, + OriginatorIdentifierOrKey, OriginatorPublicKey, RecipientEncryptedKey, RecipientInfo, + }, +}; + +// Internal imports +use const_oid::{AssociatedOid, ObjectIdentifier, db::rfc5753}; +use der::{ + Any, Decode, Encode, Sequence, + asn1::{BitString, OctetString}, +}; + +// Core imports +use core::{marker::PhantomData, ops::Add}; + +// Alloc imports +use alloc::{string::String, vec, vec::Vec}; + +// RustCrypto imports +use aes::cipher::{ + KeySizeUser, + array::ArraySize, + typenum::{Sum, U8}, +}; +use digest::{Digest, FixedOutputReset}; +use elliptic_curve::{ + AffinePoint, Curve, CurveArithmetic, FieldBytesSize, PublicKey, + ecdh::{EphemeralSecret, SharedSecret}, + point::PointCompression, + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, +}; + +/// The `EccCmsSharedInfo` type is defined in [RFC 5753 Section 7.2]. +/// +/// ```text +/// EccCmsSharedInfo ::= SEQUENCE { +/// keyInfo AlgorithmIdentifier, +/// entityUInfo [0] EXPLICIT OCTET STRING OPTIONAL, +/// suppPubInfo [2] EXPLICIT OCTET STRING } +/// ``` +/// +/// [RFC 5753 Section 7.2]: https://www.rfc-editor.org/rfc/rfc5753#section-7.2 +#[derive(Clone, Debug, Eq, PartialEq, Sequence)] +pub struct EccCmsSharedInfo { + /// Object identifier of the key-encryption algorithm + pub key_info: AlgorithmIdentifierOwned, + /// Additional keying material - optional + #[asn1( + context_specific = "0", + tag_mode = "EXPLICIT", + constructed = "true", + optional = "true" + )] + pub entity_u_info: Option, + /// Length of the generated KEK, in bits, represented as a 32-bit number + #[asn1(context_specific = "2", tag_mode = "EXPLICIT", constructed = "true")] + pub supp_pub_info: OctetString, +} + +/// Represents supported key agreement algorithm for ECC - as defined in [RFC 5753 Section 7.1.4]. +/// +/// As per [RFC 5753 Section 8]: +/// ```text +/// Implementations that support EnvelopedData with the ephemeral-static +/// ECDH standard primitive: +/// +/// - MUST support the dhSinglePass-stdDH-sha256kdf-scheme key +/// agreement algorithm, the id-aes128-wrap key wrap algorithm, and +/// the id-aes128-cbc content encryption algorithm; and +/// - MAY support the dhSinglePass-stdDH-sha1kdf-scheme, dhSinglePass- +/// stdDH-sha224kdf-scheme, dhSinglePass-stdDH-sha384kdf-scheme, and +/// dhSinglePass-stdDH-sha512kdf-scheme key agreement algorithms; +/// the id-alg-CMS3DESwrap, id-aes192-wrap, and id-aes256-wrap key +/// wrap algorithms; and the des-ede3-cbc, id-aes192-cbc, and id- +/// aes256-cbc content encryption algorithms; other algorithms MAY +/// also be supported. +/// ``` +/// +/// As such the following are currently supported: +/// - dhSinglePass-stdDH-sha224kdf-scheme +/// - dhSinglePass-stdDH-sha256kdf-scheme +/// - dhSinglePass-stdDH-sha384kdf-scheme +/// - dhSinglePass-stdDH-sha512kdf-scheme +/// +/// [RFC 5753 Section 7.1.4]: https://datatracker.ietf.org/doc/html/rfc5753#section-7.1.4 +/// [RFC 5753 Section 8]: https://datatracker.ietf.org/doc/html/rfc5753#section-8 +pub trait KeyAgreementAlgorithm { + /// Compute a shared key with the provided parameters + fn kdf( + secret: &SharedSecret, + shared_info: &EccCmsSharedInfo, + key_wrapper: &mut WrappedKey, + ) -> Result<()> + where + C: Curve, + Enc: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add; +} + +/// Support for EnvelopedData with the ephemeral-static ECDH cofactor primitive +pub struct DhSinglePassStdDhKdf { + digest: PhantomData, +} + +impl KeyAgreementAlgorithm for DhSinglePassStdDhKdf +where + D: Digest + FixedOutputReset, +{ + fn kdf( + secret: &SharedSecret, + shared_info: &EccCmsSharedInfo, + key_wrapper: &mut WrappedKey, + ) -> Result<()> + where + C: Curve, + Enc: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add, + { + let shared_info_der = shared_info.to_der()?; + let secret_bytes = secret.raw_secret_bytes(); + + ansi_x963_kdf::derive_key_into::(secret_bytes, &shared_info_der, key_wrapper.as_mut()) + .map_err(|_| { + super::Error::Builder(String::from( + "Could not generate a shared secret via ansi-x9.63-kdf", + )) + })?; + Ok(()) + } +} + +impl AssociatedOid for DhSinglePassStdDhKdf { + const OID: ObjectIdentifier = rfc5753::DH_SINGLE_PASS_STD_DH_SHA_224_KDF_SCHEME; +} + +impl AssociatedOid for DhSinglePassStdDhKdf { + const OID: ObjectIdentifier = rfc5753::DH_SINGLE_PASS_STD_DH_SHA_256_KDF_SCHEME; +} + +impl AssociatedOid for DhSinglePassStdDhKdf { + const OID: ObjectIdentifier = rfc5753::DH_SINGLE_PASS_STD_DH_SHA_384_KDF_SCHEME; +} + +impl AssociatedOid for DhSinglePassStdDhKdf { + const OID: ObjectIdentifier = rfc5753::DH_SINGLE_PASS_STD_DH_SHA_512_KDF_SCHEME; +} + +/// Contains information required to encrypt the content encryption key with a method based on ECC key agreement +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EcKeyEncryptionInfo +where + C: CurveArithmetic, +{ + /// Encrypt key with EC + Ec(PublicKey), +} +impl EcKeyEncryptionInfo +where + C: CurveArithmetic + AssociatedOid, +{ + /// Returns the OID associated with the curve used. + pub fn get_oid(&self) -> ObjectIdentifier { + C::OID + } +} +impl From> for AlgorithmIdentifierOwned +where + C: CurveArithmetic + AssociatedOid, +{ + fn from(ec_key_encryption_info: EcKeyEncryptionInfo) -> Self { + let parameters = Some(Any::from(&ec_key_encryption_info.get_oid())); + AlgorithmIdentifierOwned { + oid: elliptic_curve::ALGORITHM_OID, // id-ecPublicKey + parameters, // Curve OID + } + } +} + +/// Builds a `KeyAgreeRecipientInfo` according to RFC 5652 § 6. +/// This type uses key agreement: the recipient's public key and the sender's +/// private key are used to generate a pairwise symmetric key, then +/// the content-encryption key is encrypted in the pairwise symmetric key. +pub struct KeyAgreeRecipientInfoBuilder +where + C: CurveArithmetic, + KA: KeyAgreementAlgorithm, + KW: KeyWrapAlgorithm, + Enc: KeySizeUser, +{ + /// Optional information which helps generating different keys every time. + pub ukm: Option, + /// Recipient identifier + pub rid: KeyAgreeRecipientIdentifier, + /// Recipient key info + pub eckey_encryption_info: EcKeyEncryptionInfo, + /// Key agreement algorithm + key_agreement_algorithm: PhantomData, + /// Key wrap algorithm + key_wrap_algorithm: PhantomData, + /// Content encryption cipher + enc_cipher: PhantomData, + /// Rng + _rng: PhantomData, +} + +impl KeyAgreeRecipientInfoBuilder +where + C: CurveArithmetic, + KA: KeyAgreementAlgorithm, + KW: KeyWrapAlgorithm, + Enc: KeySizeUser, +{ + /// Creates a `KeyAgreeRecipientInfoBuilder` + pub fn new( + ukm: Option, + rid: KeyAgreeRecipientIdentifier, + eckey_encryption_info: EcKeyEncryptionInfo, + ) -> Result { + Ok(KeyAgreeRecipientInfoBuilder { + ukm, + eckey_encryption_info, + rid, + key_agreement_algorithm: PhantomData, + key_wrap_algorithm: PhantomData, + enc_cipher: PhantomData, + _rng: PhantomData, + }) + } +} +impl RecipientInfoBuilder + for KeyAgreeRecipientInfoBuilder +where + R: CryptoRng, + KA: KeyAgreementAlgorithm + AssociatedOid, + C: CurveArithmetic + AssociatedOid + PointCompression, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + KW: KeyWrapAlgorithm, + Enc: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add, +{ + /// Associated Rng type + type Rng = R; + + /// Returns the RecipientInfoType + fn recipient_info_type(&self) -> RecipientInfoType { + RecipientInfoType::Kari + } + + /// Returns the `CMSVersion` for this `RecipientInfo` + fn recipient_info_version(&self) -> CmsVersion { + CmsVersion::V3 + } + + /// Build a `KeyAgreeRecipientInfo` as per [RFC 5652 Section 6.2.2] and [RFC 5753 Section 3]. + /// + /// For now only [EnvelopedData] using `(ephemeral-static) ECDH` is supported - [RFC 5753 Section 3.1.1] + /// + /// We follow the flow outlined in - [RFC 5753 Section 3.1.2]: + /// + /// Todo: + /// - Add support for `'Co-factor' ECDH` - see [RFC 5753 Section 3.1.1] + /// - Add support for `1-Pass ECMQV` - see [RFC 5753 Section 3.2.1] + /// + /// [RFC 5753 Section 3]: https://datatracker.ietf.org/doc/html/rfc5753#section-3 + /// [RFC 5652 Section 6.2.2]: https://datatracker.ietf.org/doc/html/rfc5652#section-6.2.2 + /// [RFC 5753 Section 3.1.1]: https://datatracker.ietf.org/doc/html/rfc5753#section-3.1.1 + /// [RFC 5753 Section 3.1.2]: https://datatracker.ietf.org/doc/html/rfc5753#section-3.1.2 + /// [RFC 5753 Section 3.2.1]: https://datatracker.ietf.org/doc/html/rfc5753#section-3.2.1 + fn build_with_rng( + &mut self, + content_encryption_key: &[u8], + rng: &mut Self::Rng, + ) -> Result { + // Encrypt key + let ( + encrypted_key, + ephemeral_pubkey_encoded_point, + originator_algorithm_identifier, + key_encryption_algorithm_identifier, + ) = match self.eckey_encryption_info { + EcKeyEncryptionInfo::Ec(recipient_public_key) => { + // Generate ephemeral key using ecdh + let ephemeral_secret = EphemeralSecret::random(rng); + let ephemeral_public_key_encoded_point = + ephemeral_secret.public_key().to_encoded_point(false); + + // Compute a shared secret with recipient public key. Non-uniformly random, but will be used as input for KDF later. + let non_uniformly_random_shared_secret = + ephemeral_secret.diffie_hellman(&recipient_public_key); + + // Generate shared info for KDF + // As per https://datatracker.ietf.org/doc/html/rfc5753#section-7.2" + // ``` + // keyInfo contains the object identifier of the key-encryption + // algorithm (used to wrap the CEK) and associated parameters. In + // this specification, 3DES wrap has NULL parameters while the AES + // wraps have absent parameters. + // ``` + let key_wrap_algorithm_identifier = KW::algorithm_identifier(); + let key_wrap_algorithm_der = key_wrap_algorithm_identifier.to_der()?; + + // As per https://datatracker.ietf.org/doc/html/rfc5753#section-7.2" + // ``` + // entityUInfo optionally contains additional keying material + // supplied by the sending agent. When used with ECDH and CMS, the + // entityUInfo field contains the octet string ukm. When used with + // ECMQV and CMS, the entityUInfo contains the octet string addedukm + // (encoded in MQVuserKeyingMaterial). + // ``` + let entity_u_info = self.ukm.clone(); + + // As per https://datatracker.ietf.org/doc/html/rfc5753#section-7.2" + // ``` + // suppPubInfo contains the length of the generated KEK, in bits, + // represented as a 32-bit number, as in [CMS-DH] and [CMS-AES]. + // (For example, for AES-256 it would be 00 00 01 00.) + // ``` + let key_wrap_algo_keysize_bits_in_be_bytes: [u8; 4] = + KW::key_size_in_bits().to_be_bytes(); + + let shared_info = EccCmsSharedInfo { + key_info: key_wrap_algorithm_identifier, + entity_u_info, + supp_pub_info: OctetString::new(key_wrap_algo_keysize_bits_in_be_bytes)?, + }; + + // Init a wrapping key (KEK) based on KeyWrapAlgorithm and on CEK (i.e. key to wrap) size + let kek = KW::init_kek(); + let mut wrapped: WrappedKey = KW::init_wrapped(); + + // Derive the Key Encryption Key (KEK) from Shared Secret using ANSI X9.63 KDF + KA::kdf( + &non_uniformly_random_shared_secret, + &shared_info, + &mut wrapped, + )?; + + // Wrap the Content Encryption Key (CEK) with the KEK + KW::try_wrap(&kek, content_encryption_key, wrapped.as_mut())?; + + // Return data + ( + Vec::from(wrapped), + ephemeral_public_key_encoded_point, + self.eckey_encryption_info.into(), + AlgorithmIdentifierOwned { + oid: KA::OID, + parameters: Some(Any::from_der(&key_wrap_algorithm_der)?), + }, + ) + } + }; + + // Build RecipientInfo + Ok(RecipientInfo::Kari(KeyAgreeRecipientInfo { + originator: OriginatorIdentifierOrKey::OriginatorKey(OriginatorPublicKey { + algorithm: originator_algorithm_identifier, + public_key: BitString::from_bytes(ephemeral_pubkey_encoded_point.as_bytes())?, + }), + version: self.recipient_info_version(), + ukm: self.ukm.clone(), + key_enc_alg: key_encryption_algorithm_identifier, + recipient_enc_keys: vec![RecipientEncryptedKey { + rid: self.rid.clone(), + enc_key: EncryptedKey::new(encrypted_key)?, + }], + })) + } +} + +#[cfg(test)] +mod tests { + use std::eprintln; + + use super::*; + use p256::{NistP256, PublicKey, pkcs8::DecodePublicKey}; + + /// Generate a test P256 EcKeyEncryptionInfo + fn get_test_ec_key_info() -> EcKeyEncryptionInfo { + // Public key der bytes: + // ```rust + // let public_key_der_bytes = include_bytes!("../../tests/examples/p256-pub.der"); + // ``` + // OR + // ```bash + // od -An -vtu1 cms/tests/examples/p256-pub.der | tr -s ' ' | tr -d '\n' | sed 's/ /, /g' | sed 's/^, //' |xargs + // ``` + let public_key_der_bytes: &[u8] = &[ + 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, + 3, 66, 0, 4, 28, 172, 255, 181, 95, 47, 44, 239, 216, 157, 137, 235, 55, 75, 38, 129, + 21, 36, 82, 128, 45, 238, 160, 153, 22, 6, 129, 55, 216, 57, 207, 127, 196, 129, 164, + 68, 146, 48, 77, 126, 246, 106, 193, 23, 190, 254, 131, 168, 208, 143, 21, 95, 43, 82, + 249, 246, 24, 221, 68, 112, 41, 4, 142, 15, + ]; + let p256_public_key = PublicKey::from_public_key_der(public_key_der_bytes) + .map_err(|e| eprintln!("{}", e)) + .expect("Getting PublicKey failed"); + EcKeyEncryptionInfo::Ec(p256_public_key) + } + + #[test] + fn test_keyagreementalgorithm_oid() { + assert_eq!( + DhSinglePassStdDhKdf::::OID, + rfc5753::DH_SINGLE_PASS_STD_DH_SHA_224_KDF_SCHEME + ); + assert_eq!( + DhSinglePassStdDhKdf::::OID, + rfc5753::DH_SINGLE_PASS_STD_DH_SHA_256_KDF_SCHEME + ); + assert_eq!( + DhSinglePassStdDhKdf::::OID, + rfc5753::DH_SINGLE_PASS_STD_DH_SHA_384_KDF_SCHEME + ); + assert_eq!( + DhSinglePassStdDhKdf::::OID, + rfc5753::DH_SINGLE_PASS_STD_DH_SHA_512_KDF_SCHEME + ); + } + + #[test] + fn test_eckeyencryptioninfo_get_oid() { + let ec_key_encryption_info = get_test_ec_key_info(); + assert_eq!( + ec_key_encryption_info.get_oid(), + ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7") + ); + } + + #[test] + fn test_algorithmidentifierowned_from_eckeyencryptioninfo() { + let ec_key_encryption_info = get_test_ec_key_info(); + + assert_eq!( + AlgorithmIdentifierOwned { + oid: ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"), + parameters: Some(ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7").into()), + }, + AlgorithmIdentifierOwned::from(ec_key_encryption_info) + ) + } +} diff --git a/cms/src/builder/utils.rs b/cms/src/builder/utils.rs new file mode 100644 index 000000000..311db1534 --- /dev/null +++ b/cms/src/builder/utils.rs @@ -0,0 +1,8 @@ +//! Utilities module +//! +//! Contains various utilities used during KARI building. +//! It currently contains: +//! - kw: AES Key Wrap +//! - kdf: KDF using ANSI-x9.63 Key Derivation Function + +pub(super) mod kw; diff --git a/cms/src/builder/utils/kw.rs b/cms/src/builder/utils/kw.rs new file mode 100644 index 000000000..8332bb2da --- /dev/null +++ b/cms/src/builder/utils/kw.rs @@ -0,0 +1,166 @@ +//! Key wrap module +//! +//! This module contains the key wrapping logic based on aes-kw algorithms +//! + +// Self imports +use crate::builder::{Error, Result}; + +// Internal imports +use const_oid::AssociatedOid; +use spki::AlgorithmIdentifierOwned; + +// Alloc imports +use alloc::{string::String, vec::Vec}; + +// Core imports +use core::ops::Add; + +// Rust crypto imports +use aes::cipher::{ + BlockCipherDecrypt, BlockCipherEncrypt, BlockSizeUser, Key, KeyInit, KeySizeUser, + array::{Array, ArraySize}, + typenum::{Sum, U8, U16, Unsigned}, +}; +use aes_kw::AesKw; + +/// Represents supported key wrap algorithm for ECC - as defined in [RFC 5753 Section 7.1.5]. +/// +/// As per [RFC 5753 Section 8]: +/// ```text +/// Implementations that support EnvelopedData with the ephemeral-static +/// ECDH standard primitive: +/// +/// - MUST support the dhSinglePass-stdDH-sha256kdf-scheme key +/// agreement algorithm, the id-aes128-wrap key wrap algorithm, and +/// the id-aes128-cbc content encryption algorithm; and +/// - MAY support the dhSinglePass-stdDH-sha1kdf-scheme, dhSinglePass- +/// stdDH-sha224kdf-scheme, dhSinglePass-stdDH-sha384kdf-scheme, and +/// dhSinglePass-stdDH-sha512kdf-scheme key agreement algorithms; +/// the id-alg-CMS3DESwrap, id-aes192-wrap, and id-aes256-wrap key +/// wrap algorithms; and the des-ede3-cbc, id-aes192-cbc, and id- +/// aes256-cbc content encryption algorithms; other algorithms MAY +/// also be supported. +/// ``` +/// +/// As such the following algorithm are currently supported +/// - id-aes128-wrap +/// - id-aes192-wrap +/// - id-aes256-wrap +/// +/// [RFC 5753 Section 8]: https://datatracker.ietf.org/doc/html/rfc5753#section-8 +/// [RFC 5753 Section 7.1.5]: https://datatracker.ietf.org/doc/html/rfc5753#section-7.1.5 +/// +/// Represents key wrap algorithms methods. +pub trait KeyWrapAlgorithm: AssociatedOid + KeySizeUser { + /// Return key size of the key-wrap algorithm in bits + fn key_size_in_bits() -> u32; + + /// Return algorithm identifier AlgorithmIdentifierOwned` associated with the key-wrap algorithm + /// + /// [RFC 3565 Section 2.3.2]: + /// ```text + /// NIST has assigned the following OIDs to define the AES key wrap + /// algorithm. + /// + /// id-aes128-wrap OBJECT IDENTIFIER ::= { aes 5 } + /// id-aes192-wrap OBJECT IDENTIFIER ::= { aes 25 } + /// id-aes256-wrap OBJECT IDENTIFIER ::= { aes 45 } + /// + /// In all cases the parameters field MUST be absent. + /// ``` + /// + /// [RFC 3565 Section 2.3.2]: https://datatracker.ietf.org/doc/html/rfc3565#section-2.3.2 + /// [RFC 5753 Section 7.2]: https://datatracker.ietf.org/doc/html/rfc5753#section-7.2 + fn algorithm_identifier() -> AlgorithmIdentifierOwned; + + /// Return an empty wrapping key (KEK) with the adequate size to be used with aes-key-wrap + fn init_kek() -> Key; + + /// Return an empty wrapped key with the adequate size to be used with aes-key-wrap + fn init_wrapped() -> WrappedKey + where + T: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add; + + /// Try to wrap some data using given wrapping key + fn try_wrap(key: &Key, data: &[u8], out: &mut [u8]) -> Result<()>; +} + +/// Struct representing a wrapped key +/// +/// Can be used to abstract wrapped key over different incoming key sizes. +pub struct WrappedKey +where + T: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add, +{ + inner: Array>, +} + +impl AsMut<[u8]> for WrappedKey +where + T: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add, +{ + fn as_mut(&mut self) -> &mut [u8] { + self.inner.as_mut() + } +} + +impl From> for Vec +where + T: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add, +{ + fn from(wrapped_key: WrappedKey) -> Self { + wrapped_key.inner.to_vec() + } +} + +impl KeyWrapAlgorithm for AesKw +where + AesWrap: KeyInit + BlockSizeUser + BlockCipherEncrypt + BlockCipherDecrypt, + AesKw: AssociatedOid + KeyInit, +{ + fn key_size_in_bits() -> u32 { + AesWrap::KeySize::U32 * 8u32 + } + + fn algorithm_identifier() -> AlgorithmIdentifierOwned { + AlgorithmIdentifierOwned { + oid: Self::OID, + parameters: None, + } + } + + fn init_kek() -> Key { + Key::::default() + } + + fn init_wrapped() -> WrappedKey + where + AesEnc: KeySizeUser, + Sum: ArraySize, + ::KeySize: Add, + { + WrappedKey:: { + inner: Array::>::default(), + } + } + + fn try_wrap(key: &Key, data: &[u8], out: &mut [u8]) -> Result<()> { + let kek = AesKw::new(key); + let res = kek + .wrap_key(data, out) + .map_err(|_| Error::Builder(String::from("could not wrap key")))?; + if res.len() != out.len() { + return Err(Error::Builder(String::from("output buffer invalid size"))); + } + Ok(()) + } +} diff --git a/cms/tests/builder.rs b/cms/tests/builder.rs index e3584cf5e..e4e9bba21 100644 --- a/cms/tests/builder.rs +++ b/cms/tests/builder.rs @@ -32,6 +32,10 @@ use spki::AlgorithmIdentifierOwned; use x509_cert::attr::{Attribute, AttributeValue}; use x509_cert::serial_number::SerialNumber; +// Modules +#[path = "builder/kari.rs"] +mod kari; + // TODO bk replace this by const_oid definitions as soon as released const RFC8894_ID_MESSAGE_TYPE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.113733.1.9.2"); diff --git a/cms/tests/builder/kari.rs b/cms/tests/builder/kari.rs new file mode 100644 index 000000000..0f2d7f930 --- /dev/null +++ b/cms/tests/builder/kari.rs @@ -0,0 +1,90 @@ +use aes_kw::AesKw; +use cms::{ + builder::{ + ContentEncryptionAlgorithm, DhSinglePassStdDhKdf, EcKeyEncryptionInfo, + EnvelopedDataBuilder, KeyAgreeRecipientInfoBuilder, + }, + cert::IssuerAndSerialNumber, + content_info::ContentInfo, + enveloped_data::KeyAgreeRecipientIdentifier, +}; +use der::{Any, AnyRef, Encode}; +use p256::{SecretKey, pkcs8::DecodePrivateKey}; +use pem_rfc7468::LineEnding; +use rand::{TryRngCore, rngs::OsRng}; +use x509_cert::serial_number::SerialNumber; + +fn key_agreement_recipient_identifier(id: i32) -> KeyAgreeRecipientIdentifier { + let issuer = format!("CN=test client {id}").parse().unwrap(); + KeyAgreeRecipientIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { + issuer, + serial_number: SerialNumber::new(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]) + .expect("failed to create a serial number"), + }) +} + +/// Generate a CMS message encrypted with recipient public EC key +/// +/// Can be decrypted using: +/// ```bash +/// openssl cms -decrypt -inkey cms/tests/examples/p256-priv.der -inform PEM +/// ``` +#[test] +fn test_build_enveloped_data_ec() { + // Recipient identifier + let key_agreement_recipient_identifier = key_agreement_recipient_identifier(1); + + // Recipient key material + let recipient_private_key_der = include_bytes!("../examples/p256-priv.der"); + let recipient_private_key = SecretKey::from_pkcs8_der(recipient_private_key_der) + .expect("could not parse in private key"); + let recipient_public_key = recipient_private_key.public_key(); + + // KARI builder + let kari_builder = KeyAgreeRecipientInfoBuilder::< + _, + _, + DhSinglePassStdDhKdf, + AesKw, + aes::Aes128, + >::new( + None, + key_agreement_recipient_identifier, + EcKeyEncryptionInfo::Ec(recipient_public_key), + ) + .expect("Could not create a KeyAgreeRecipientInfoBuilder"); + + // Enveloped data builder + let mut rng = OsRng.unwrap_err(); + let mut builder = EnvelopedDataBuilder::new( + None, + "Arbitrary unencrypted content, encrypted using ECC".as_bytes(), + ContentEncryptionAlgorithm::Aes128Cbc, + None, + ) + .expect("Could not create an EnvelopedData builder."); + + // Enveloped data + let enveloped_data = builder + .add_recipient_info(kari_builder) + .expect("Could not add a recipient info") + .build_with_rng(&mut rng) + .expect("Building EnvelopedData failed"); + let enveloped_data_der = enveloped_data + .to_der() + .expect("conversion of enveloped data to DER failed."); + + // Content info + let content = AnyRef::try_from(enveloped_data_der.as_slice()).unwrap(); + let content_info = ContentInfo { + content_type: const_oid::db::rfc5911::ID_ENVELOPED_DATA, + content: Any::from(content), + }; + let content_info_der = content_info.to_der().unwrap(); + + println!( + "{}", + pem_rfc7468::encode_string("CMS", LineEnding::LF, &content_info_der) + .expect("PEM encoding of enveloped data DER failed") + ); +} diff --git a/cms/tests/examples/p256-pub.der b/cms/tests/examples/p256-pub.der new file mode 100644 index 000000000..67c719c76 Binary files /dev/null and b/cms/tests/examples/p256-pub.der differ