diff --git a/Cargo.lock b/Cargo.lock index 325d2469..db489856 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,6 +362,7 @@ dependencies = [ name = "dhkem" version = "0.1.0-rc.1" dependencies = [ + "ctutils", "elliptic-curve", "getrandom", "hex-literal", diff --git a/dhkem/Cargo.toml b/dhkem/Cargo.toml index 399efb56..5b31615c 100644 --- a/dhkem/Cargo.toml +++ b/dhkem/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["crypto", "ecdh", "ecc"] readme = "README.md" [dependencies] +ctutils = "0.4" hkdf = "0.13" kem = "0.3" rand_core = "0.10" diff --git a/dhkem/src/ecdh_kem.rs b/dhkem/src/ecdh_kem.rs index 58bbecc3..6edbcbc2 100644 --- a/dhkem/src/ecdh_kem.rs +++ b/dhkem/src/ecdh_kem.rs @@ -1,10 +1,11 @@ //! Generic Elliptic Curve Diffie-Hellman KEM adapter. -use crate::{DecapsulationKey, EncapsulationKey}; +use crate::{DecapsulationKey, EncapsulationKey, Error, HpkeKemId}; use core::marker::PhantomData; use elliptic_curve::{ - AffinePoint, CurveArithmetic, Error, FieldBytes, FieldBytesSize, PublicKey, SecretKey, + AffinePoint, CurveArithmetic, FieldBytes, FieldBytesSize, PublicKey, SecretKey, bigint, ecdh::EphemeralSecret, + sec1, sec1::{FromSec1Point, ModulusSize, ToSec1Point, UncompressedPoint, UncompressedPointSize}, }; use kem::{ @@ -124,11 +125,14 @@ where { type Error = Error; + #[inline] fn try_decapsulate( &self, encapsulated_key: &Ciphertext>, ) -> Result>, Error> { - let encapsulated_key = PublicKey::::from_sec1_bytes(encapsulated_key)?; + let encapsulated_key = + PublicKey::::from_sec1_bytes(encapsulated_key).map_err(|_| Error::Decapsulation)?; + let shared_secret = self.dk.diffie_hellman(&encapsulated_key); Ok(*shared_secret.raw_secret_bytes()) } @@ -225,3 +229,77 @@ where (pk, *ss.raw_secret_bytes()) } } + +impl FromSec1Point for EcdhEncapsulationKey +where + C: CurveArithmetic, + C::FieldBytesSize: ModulusSize, + PublicKey: FromSec1Point, +{ + fn from_sec1_point(point: &sec1::Sec1Point) -> bigint::CtOption { + PublicKey::::from_sec1_point(point).map(Into::into) + } +} + +impl ToSec1Point for EcdhEncapsulationKey +where + C: CurveArithmetic, + C::FieldBytesSize: ModulusSize, + PublicKey: ToSec1Point, +{ + fn to_sec1_point(&self, compress: bool) -> sec1::Sec1Point { + self.0.to_sec1_point(compress) + } +} + +/// NIST P-256 ECDH Decapsulation Key. +#[cfg(feature = "p256")] +pub type NistP256DecapsulationKey = EcdhDecapsulationKey; +/// NIST P-256 ECDH Encapsulation Key. +#[cfg(feature = "p256")] +pub type NistP256EncapsulationKey = EcdhEncapsulationKey; +/// NIST P-256 DHKEM. +#[cfg(feature = "p256")] +pub type NistP256Kem = EcdhKem; +#[cfg(feature = "p256")] +impl HpkeKemId for NistP256Kem { + const KEM_ID: u16 = 0x10; +} + +/// NIST P-384 ECDH Decapsulation Key. +#[cfg(feature = "p384")] +pub type NistP384DecapsulationKey = EcdhDecapsulationKey; +/// NIST P-384 ECDH Encapsulation Key. +#[cfg(feature = "p384")] +pub type NistP384EncapsulationKey = EcdhEncapsulationKey; +/// NIST P-384 DHKEM. +#[cfg(feature = "p384")] +pub type NistP384Kem = EcdhKem; +#[cfg(feature = "p384")] +impl HpkeKemId for NistP384Kem { + const KEM_ID: u16 = 0x11; +} + +/// NIST P-521 ECDH Decapsulation Key. +#[cfg(feature = "p521")] +pub type NistP521DecapsulationKey = EcdhDecapsulationKey; +/// NIST P-521 ECDH Encapsulation Key. +#[cfg(feature = "p521")] +pub type NistP521EncapsulationKey = EcdhEncapsulationKey; +/// NIST P-521 DHKEM. +#[cfg(feature = "p521")] +pub type NistP521Kem = EcdhKem; +#[cfg(feature = "p521")] +impl HpkeKemId for NistP521Kem { + const KEM_ID: u16 = 0x12; +} + +/// secp256k1 ECDH Decapsulation Key. +#[cfg(feature = "k256")] +pub type Secp256k1DecapsulationKey = EcdhDecapsulationKey; +/// secp256k1 ECDH Encapsulation Key. +#[cfg(feature = "k256")] +pub type Secp256k1EncapsulationKey = EcdhEncapsulationKey; +/// secp256k1 DHKEM. +#[cfg(feature = "k256")] +pub type Secp256k1Kem = EcdhKem; diff --git a/dhkem/src/error.rs b/dhkem/src/error.rs new file mode 100644 index 00000000..16ef7a0b --- /dev/null +++ b/dhkem/src/error.rs @@ -0,0 +1,29 @@ +use core::fmt; + +/// Error type. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum Error { + /// Decapsulation failed. + Decapsulation, + + /// Length invalid. + Length, +} + +impl core::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Decapsulation => write!(f, "decapsulation error"), + Error::Length => write!(f, "invalid length"), + } + } +} + +impl From for Error { + fn from(_: hkdf::InvalidLength) -> Self { + Error::Length + } +} diff --git a/dhkem/src/expander.rs b/dhkem/src/expander.rs index 6e1481ff..15d27d96 100644 --- a/dhkem/src/expander.rs +++ b/dhkem/src/expander.rs @@ -1,7 +1,7 @@ -pub use hkdf::InvalidLength; +pub use hkdf::{InvalidLength, hmac::digest::block_api::EagerHash}; use core::iter; -use hkdf::{Hkdf, hmac::digest::block_api::EagerHash}; +use hkdf::Hkdf; #[cfg(feature = "zeroize")] use zeroize::Zeroize; @@ -12,17 +12,32 @@ const PREFIXES_MAX: usize = 256; /// Maximum size of input key material or info after applying prefixes. const LABELED_INPUT_MAX: usize = PREFIXES_MAX + 64; -/// HPKE version identifier from `RFC9810 §4`. +/// HPKE version identifier from `RFC9180 §4`. const HPKE_VERSION_ID: &[u8] = b"HPKE-v1"; -/// HPKE suite ID from `RFC9810 §4`. -const HPKE_SUITE_ID: &[u8] = b"KEM\x00\x10"; +/// Type representing an HPKE suite identifier with `KEM` prefix. +type HpkeSuiteId = [u8; 5]; + +/// Associates a given HPKE KEM ID with the given type. +pub trait HpkeKemId { + /// Identifier for a particular KEM, from [RFC9180 §7.1]. + /// + /// [RFC9180 §7.1]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1 + const KEM_ID: u16; + + /// Compute the full HPKE suite ID for this type. + #[must_use] + fn suite_id() -> HpkeSuiteId { + let [b0, b1] = Self::KEM_ID.to_be_bytes(); + [b'K', b'E', b'M', b0, b1] + } +} /// Expander: wrapper for [RFC5869] HKDF-Expand operation which can be used for HPKE's -/// `LabeledExtract` and `LabeledExpand` as described in [RFC9810 §4]. +/// `LabeledExtract` and `LabeledExpand` as described in [RFC9180 §4]. /// /// [RFC5869]: https://datatracker.ietf.org/doc/html/rfc5869 -/// [RFC9810 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 +/// [RFC9180 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 #[derive(Debug)] pub struct Expander { /// Inner HKDF instance @@ -65,20 +80,20 @@ impl Expander { } /// Create a new expander which uses the prefixes that implement HPKE `LabeledExtract` as - /// described in [RFC9810 §4]. + /// described in [RFC9180 §4]. /// /// # Errors /// Returns [`InvalidLength`] if the concatenated prefixes are too long. /// - /// [RFC9810 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 - pub fn new_labeled_hpke( + /// [RFC9180 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 + pub fn new_labeled_hpke( salt: &[u8], label: &[u8], input_key_material: &[u8], ) -> Result { Self::new_prefixed( salt, - &[HPKE_VERSION_ID, HPKE_SUITE_ID, label], + &[HPKE_VERSION_ID, &K::suite_id(), label], input_key_material, ) } @@ -92,7 +107,7 @@ impl Expander { /// Returns [`InvalidLength`] if info is too long. /// /// [RFC5869]: https://datatracker.ietf.org/doc/html/rfc5869 - /// [RFC9810 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 + /// [RFC9180 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 pub fn expand(&self, info: &[u8], okm: &mut [u8]) -> Result<(), InvalidLength> { self.hkdf.expand(info, okm) } @@ -115,13 +130,13 @@ impl Expander { } /// Create a new expander which uses the prefixes that implement HPKE `LabeledExpand` as - /// described in [RFC9810 §4]. + /// described in [RFC9180 §4]. /// /// # Errors /// Returns [`InvalidLength`] if label and/or info is too long. /// - /// [RFC9810 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 - pub fn expand_labeled_hpke( + /// [RFC9180 §4]: https://datatracker.ietf.org/doc/html/rfc9180#section-4 + pub fn expand_labeled_hpke( &self, label: &[u8], info: &[u8], @@ -132,7 +147,7 @@ impl Expander { &[ &okm_len.to_be_bytes(), HPKE_VERSION_ID, - HPKE_SUITE_ID, + &K::suite_id(), label, info, ], diff --git a/dhkem/src/lib.rs b/dhkem/src/lib.rs index 862a742c..88deaa79 100644 --- a/dhkem/src/lib.rs +++ b/dhkem/src/lib.rs @@ -29,10 +29,15 @@ //! [RFC9180]: https://datatracker.ietf.org/doc/html/rfc9180#name-dh-based-kem-dhkem //! [TLS KEM combiner]: https://datatracker.ietf.org/doc/html/draft-ietf-tls-hybrid-design-10 +mod error; mod expander; -pub use expander::{Expander, InvalidLength}; -pub use kem::{self, Decapsulator, Encapsulate, Generate, Kem, TryDecapsulate}; +pub use crate::{ + error::Error, + expander::{EagerHash, Expander, HpkeKemId, InvalidLength}, +}; +pub use kem::{self, Ciphertext, Decapsulator, Encapsulate, Generate, Kem, TryDecapsulate}; +use rand_core::CryptoRng; #[cfg(feature = "ecdh")] mod ecdh_kem; @@ -40,15 +45,9 @@ mod ecdh_kem; mod x25519_kem; #[cfg(feature = "ecdh")] -pub use ecdh_kem::{EcdhDecapsulationKey, EcdhEncapsulationKey, EcdhKem}; +pub use ecdh_kem::*; #[cfg(feature = "x25519")] -pub use x25519_kem::{X25519DecapsulationKey, X25519EncapsulationKey, X25519Kem}; - -#[cfg(feature = "ecdh")] -use elliptic_curve::{ - CurveArithmetic, PublicKey, bigint, - sec1::{self, FromSec1Point, ToSec1Point}, -}; +pub use x25519_kem::*; #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -63,7 +62,27 @@ pub struct DecapsulationKey { } impl DecapsulationKey { - /// Consumes `self` and returns the wrapped value + /// Perform decapsulation and initialize an [`Expander`] using the resulting shared secret. + /// + /// # Errors + /// - Returns [`Error::Decapsulation`] if the decapsulation operation failed. + /// - Returns [`Error::Length`] if `salt` or `label` are too long. + pub fn decapsulate_and_expand( + &self, + salt: &[u8], + label: &[u8], + ciphertext: &Ciphertext<::Kem>, + ) -> Result, Error> + where + Self: TryDecapsulate + Decapsulator, + D: EagerHash, + { + let ikm = self.try_decapsulate(ciphertext)?; + let ex = Expander::new_labeled_hpke::<::Kem>(salt, label, &ikm)?; + Ok(ex) + } + + /// Consumes `self` and returns the inner decapsulation key. pub fn into_inner(self) -> DK { self.dk } @@ -90,39 +109,52 @@ where pub struct EncapsulationKey(EK); impl EncapsulationKey { - /// Consumes `self` and returns the wrapped value + /// Consumes `self` and returns the inner encapsulation key. pub fn into_inner(self) -> EK { self.0 } } -impl From for EncapsulationKey { - fn from(inner: EK) -> Self { - Self(inner) - } -} - -#[cfg(feature = "ecdh")] -impl FromSec1Point for EcdhEncapsulationKey +impl EncapsulationKey where - C: CurveArithmetic, - C::FieldBytesSize: sec1::ModulusSize, - PublicKey: FromSec1Point, + Self: Encapsulate, { - fn from_sec1_point(point: &sec1::Sec1Point) -> bigint::CtOption { - PublicKey::::from_sec1_point(point).map(Into::into) + /// Generate a new shared secret from the system's RNG, then encrypt it, returning its + /// ciphertext and an [`Expander`] which has been initialized with it. + /// + /// # Errors + /// Returns [`Error::Length`] if `salt` or `label` are too long. + #[cfg(feature = "getrandom")] + pub fn encapsulate_and_expand( + &self, + salt: &[u8], + label: &[u8], + ) -> Result<(Ciphertext<::Kem>, Expander), InvalidLength> { + let (ct, ikm) = self.encapsulate(); + let expander = Expander::new_labeled_hpke::<::Kem>(salt, label, &ikm)?; + Ok((ct, expander)) + } + + /// Generate a new shared secret from the provided `rng`, then encrypt it, returning its + /// ciphertext and an [`Expander`] which has been initialized with it. + /// + /// # Errors + /// Returns [`Error::Length`] if `salt` or `label` are too long. + pub fn encapsulate_with_rng_and_expand( + &self, + rng: &mut R, + salt: &[u8], + label: &[u8], + ) -> Result<(Ciphertext<::Kem>, Expander), InvalidLength> { + let (ct, ikm) = self.encapsulate_with_rng(rng); + let expander = Expander::new_labeled_hpke::<::Kem>(salt, label, &ikm)?; + Ok((ct, expander)) } } -#[cfg(feature = "ecdh")] -impl ToSec1Point for EcdhEncapsulationKey -where - C: CurveArithmetic, - C::FieldBytesSize: sec1::ModulusSize, - PublicKey: ToSec1Point, -{ - fn to_sec1_point(&self, compress: bool) -> sec1::Sec1Point { - self.0.to_sec1_point(compress) +impl From for EncapsulationKey { + fn from(inner: EK) -> Self { + Self(inner) } } @@ -135,43 +167,3 @@ impl Zeroize for DecapsulationKey { #[cfg(feature = "zeroize")] impl ZeroizeOnDrop for DecapsulationKey {} - -/// NIST P-256 DHKEM. -#[cfg(feature = "p256")] -pub type NistP256Kem = EcdhKem; -/// NIST P-256 ECDH Decapsulation Key. -#[cfg(feature = "p256")] -pub type NistP256DecapsulationKey = EcdhDecapsulationKey; -/// NIST P-256 ECDH Encapsulation Key. -#[cfg(feature = "p256")] -pub type NistP256EncapsulationKey = EcdhEncapsulationKey; - -/// NIST P-256 DHKEM. -#[cfg(feature = "p384")] -pub type NistP384Kem = EcdhKem; -/// NIST P-384 ECDH Decapsulation Key. -#[cfg(feature = "p384")] -pub type NistP384DecapsulationKey = EcdhDecapsulationKey; -/// NIST P-384 ECDH Encapsulation Key. -#[cfg(feature = "p384")] -pub type NistP384EncapsulationKey = EcdhEncapsulationKey; - -/// NIST P-521 DHKEM. -#[cfg(feature = "p521")] -pub type NistP521Kem = EcdhKem; -/// NIST P-521 ECDH Decapsulation Key. -#[cfg(feature = "p521")] -pub type NistP521DecapsulationKey = EcdhDecapsulationKey; -/// NIST P-521 ECDH Encapsulation Key. -#[cfg(feature = "p521")] -pub type NistP521EncapsulationKey = EcdhEncapsulationKey; - -/// secp256k1 DHKEM. -#[cfg(feature = "k256")] -pub type Secp256k1Kem = EcdhKem; -/// secp256k1 ECDH Decapsulation Key. -#[cfg(feature = "k256")] -pub type Secp256k1DecapsulationKey = EcdhDecapsulationKey; -/// secp256k1 ECDH Encapsulation Key. -#[cfg(feature = "k256")] -pub type Secp256k1EncapsulationKey = EcdhEncapsulationKey; diff --git a/dhkem/src/x25519_kem.rs b/dhkem/src/x25519_kem.rs index 020c2172..db4df398 100644 --- a/dhkem/src/x25519_kem.rs +++ b/dhkem/src/x25519_kem.rs @@ -1,7 +1,10 @@ -use crate::{DecapsulationKey, EncapsulationKey}; +//! KEM which uses the X25519 Diffie-Hellman function. + +use crate::{DecapsulationKey, EncapsulationKey, Error, HpkeKemId}; +use ctutils::CtEq; use kem::{ - Decapsulate, Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, KeyInit, - KeySizeUser, TryKeyInit, + Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, KeyInit, KeySizeUser, + TryDecapsulate, TryKeyInit, common::array::{Array, sizes::U32}, }; use rand_core::{CryptoRng, TryCryptoRng}; @@ -29,6 +32,10 @@ impl Kem for X25519Kem { type SharedKeySize = U32; } +impl HpkeKemId for X25519Kem { + const KEM_ID: u16 = 0x20; +} + /// Elliptic Curve Diffie-Hellman Decapsulation Key (i.e. secret decryption key) /// /// Generic around an elliptic curve `C`. @@ -87,10 +94,23 @@ impl Generate for X25519DecapsulationKey { /// To produce something suitable for e.g. symmetric key(s), use the [`Expander`] type to derive /// output keys. /// -impl Decapsulate for X25519DecapsulationKey { - fn decapsulate(&self, encapsulated_key: &Ciphertext) -> SharedKey { +impl TryDecapsulate for X25519DecapsulationKey { + type Error = Error; + + #[inline] + fn try_decapsulate(&self, encapsulated_key: &Ciphertext) -> Result { let public_key = PublicKey::from(encapsulated_key.0); - self.dk.diffie_hellman(&public_key).to_bytes().into() + let sk = self.dk.diffie_hellman(&public_key).to_bytes(); + + // From RFC9810 §7.1.4. Validation of Inputs and Outputs: + // > For X25519 and X448, public keys and Diffie-Hellman outputs MUST be validated as + // > described in RFC7748. In particular, recipients MUST check whether the Diffie-Hellman + // > shared secret is the all-zero value and abort if so. + if sk.ct_eq(&[0u8; 32]).into() { + return Err(Error::Decapsulation); + } + + Ok(sk.into()) } } diff --git a/dhkem/tests/hpke_p256_test.rs b/dhkem/tests/hpke_p256_test.rs index 3ca07707..7ef55c08 100644 --- a/dhkem/tests/hpke_p256_test.rs +++ b/dhkem/tests/hpke_p256_test.rs @@ -5,7 +5,7 @@ #![allow(clippy::unwrap_used, reason = "tests")] use core::convert::Infallible; -use dhkem::NistP256DecapsulationKey; +use dhkem::{HpkeKemId, NistP256DecapsulationKey, NistP256Kem}; use hex_literal::hex; use kem::{Encapsulate, KeyExport, TryDecapsulate, TryKeyInit}; use rand_core::{TryCryptoRng, TryRng}; @@ -42,11 +42,11 @@ impl TryRng for ConstantRng<'_> { // this is only ever ok for testing impl TryCryptoRng for ConstantRng<'_> {} -fn extract_and_expand(shared_secret: &[u8], kem_context: &[u8]) -> [u8; 32] { +fn extract_and_expand(shared_secret: &[u8], kem_context: &[u8]) -> [u8; 32] { let mut out = [0u8; 32]; - let expander = Expander::new_labeled_hpke(b"", b"eae_prk", shared_secret).unwrap(); + let expander = Expander::new_labeled_hpke::(b"", b"eae_prk", shared_secret).unwrap(); expander - .expand_labeled_hpke(b"shared_secret", kem_context, &mut out) + .expand_labeled_hpke::(b"shared_secret", kem_context, &mut out) .unwrap(); out @@ -81,7 +81,7 @@ fn test_dhkem_p256_hkdf_sha256() { assert_eq!(ss1, ss2); let kem_context = [example_pke, example_pkr].concat(); - let shared_secret = extract_and_expand(&ss1, &kem_context); + let shared_secret = extract_and_expand::(&ss1, &kem_context); assert_eq!(&shared_secret, &example_shared_secret); }