From b037755226d35c26146df343bbb9ba778ee97a51 Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Fri, 16 Feb 2024 13:19:39 -0500 Subject: [PATCH 1/6] Simplified API significantly; updated HPKE test --- kem/src/kem.rs | 41 ++++-------- kem/tests/hpke.rs | 155 +++++++--------------------------------------- 2 files changed, 36 insertions(+), 160 deletions(-) diff --git a/kem/src/kem.rs b/kem/src/kem.rs index 1963883ef..6df31d7cc 100644 --- a/kem/src/kem.rs +++ b/kem/src/kem.rs @@ -1,40 +1,21 @@ //! KEM Traits -use crate::errors::Error; +use rand_core::CryptoRngCore; -use core::fmt; +pub trait Encapsulate { + type Error; -use generic_array::{ArrayLength, GenericArray}; -use rand_core::{CryptoRng, RngCore}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -/// Trait impl'd by concrete types that represent an encapsulated key. This is intended to be, in -/// essence, a bag of bytes. -pub trait EncappedKey: AsRef<[u8]> + fmt::Debug + Sized { - /// The size, in bytes, of an encapsulated key. - type EncappedKeySize: ArrayLength; - - /// The size, in bytes, of the shared secret that this KEM produces. - type SharedSecretSize: ArrayLength; - - /// Represents the identity key of an encapsulator. This is used in authenticated - /// decapsulation. - type SenderPublicKey; - - /// The public key of a decapsulator. This is used in encapsulation. - type RecipientPublicKey; + fn encapsulate(&self, rng: impl CryptoRngCore) -> Result<(EK, SS), Self::Error>; +} - /// Parses an encapsulated key from its byte representation. - fn from_bytes(bytes: &GenericArray) -> Result; +pub trait Decapsulate { + type Error; - /// Borrows a byte slice representing the serialized form of this encapsulated key. - fn as_bytes(&self) -> &GenericArray { - // EncappedKey is already AsRef<[u8]>, so we don't need to do any work. This will panic iff - // the underlying bytestring is not precisely NEnc bytes long. - self.as_ref().into() - } + fn decapsulate(&self, encapped_key: &EK) -> Result; } +/* + /// The shared secret that results from key exchange. pub struct SharedSecret(GenericArray); @@ -98,3 +79,5 @@ pub trait AuthDecapsulator { sender_pubkey: &EK::SenderPublicKey, ) -> Result, Error>; } + +*/ diff --git a/kem/tests/hpke.rs b/kem/tests/hpke.rs index 43df15cd2..4726a7a8e 100644 --- a/kem/tests/hpke.rs +++ b/kem/tests/hpke.rs @@ -1,163 +1,56 @@ +use kem::{Decapsulate, Encapsulate}; + use hpke::{ kem::{Kem as KemTrait, X25519HkdfSha256}, - Deserializable as HpkeDeserializable, Serializable as HpkeSerializable, -}; -use kem::{ - generic_array::GenericArray, AuthDecapsulator, Decapsulator, EncappedKey, Encapsulator, Error, - SharedSecret, + HpkeError, }; use rand::rngs::OsRng; -use rand_core::{CryptoRng, RngCore}; +use rand_core::{CryptoRng, CryptoRngCore, RngCore}; + +type SharedSecret = hpke::kem::SharedSecret; // Define the pubkey type. This has no trait bounds required by the library #[derive(Clone)] struct X25519PublicKey(::PublicKey); -// Define the encapsulated key type and impl the necessary traits. Since authenticated and -// unauthenticated DHKEMs have the same encapped key type, this will support both types of -// algorithms. In practice, one should use types to distinguish between the two. But this is just -// test code, so whatever. -#[derive(Debug)] -struct X25519EncappedKey( - // It's just an array of bytes - GenericArray::EncappedKey as HpkeSerializable>::OutputSize>, -); -impl EncappedKey for X25519EncappedKey { - type SharedSecretSize = ::NSecret; - type EncappedKeySize = - <::PublicKey as HpkeSerializable>::OutputSize; - // In HPKE the only recipient public key is the identity key - type RecipientPublicKey = X25519PublicKey; - // The sender's pubkey is the identity too - type SenderPublicKey = X25519PublicKey; +struct X25519PrivateKey(::PrivateKey); - fn from_bytes(bytes: &GenericArray) -> Result { - Ok(X25519EncappedKey(*bytes)) - } -} -impl AsRef<[u8]> for X25519EncappedKey { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} +type X25519EncappedKey = ::EncappedKey; -// Define some convenience types -type X25519PrivateKey = ::PrivateKey; -type X25519SharedSecret = SharedSecret; - -// Define an authenticated encapsulator. To authenticate, we need a full sender keypair. -struct X25519AuthEncap(X25519PrivateKey, X25519PublicKey); -impl Encapsulator for X25519AuthEncap { - fn try_encap( - &self, - csprng: &mut R, - recip_pubkey: &X25519PublicKey, - ) -> Result<(X25519EncappedKey, X25519SharedSecret), Error> { - ::encap(&recip_pubkey.0, Some((&self.0, &(self.1).0)), csprng) - .map(|(ss, ek)| { - ( - X25519EncappedKey(ek.to_bytes()), - X25519SharedSecret::new(ss.0), - ) - }) - .map_err(|_| Error) - } -} +impl Encapsulate for X25519PublicKey { + type Error = HpkeError; -// Define an unauthenticated encapsulator. This doesn't need any state at all. -struct X25519Encap; -impl Encapsulator for X25519Encap { - fn try_encap( + fn encapsulate( &self, - csprng: &mut R, - recip_pubkey: &X25519PublicKey, - ) -> Result<(X25519EncappedKey, X25519SharedSecret), Error> { - ::encap(&recip_pubkey.0, None, csprng) - .map(|(ss, ek)| { - ( - X25519EncappedKey(ek.to_bytes()), - X25519SharedSecret::new(ss.0), - ) - }) - .map_err(|_| Error) + mut csprng: impl CryptoRngCore, + ) -> Result<(X25519EncappedKey, SharedSecret), HpkeError> { + ::encap(&self.0, None, &mut csprng).map(|(ek, ss)| (ss, ek)) } } -// Define an decapsulator. Since authenticated and unauthenticated encapped keys are represented by -// the same type (which, outside of testing, should not be the case), this can do both auth'd and -// unauth'd decapsulation. -impl Decapsulator for X25519PrivateKey { - fn try_decap(&self, encapped_key: &X25519EncappedKey) -> Result { - // First parse the encapped key, since it's just bytes right now - let deserialized_encapped_key = - <::EncappedKey as HpkeDeserializable>::from_bytes( - &encapped_key.0, - ) - .map_err(|_| Error)?; +impl Decapsulate for X25519PrivateKey { + type Error = HpkeError; - // Now decapsulate - ::decap(self, None, &deserialized_encapped_key) - .map(|ss| SharedSecret::new(ss.0)) - .map_err(|_| Error) - } -} -impl AuthDecapsulator for X25519PrivateKey { - fn try_auth_decap( - &self, - encapped_key: &X25519EncappedKey, - sender_pubkey: &X25519PublicKey, - ) -> Result { - // First parse the encapped key, since it's just bytes right now - let deserialized_encapped_key = - <::EncappedKey as HpkeDeserializable>::from_bytes( - &encapped_key.0, - ) - .map_err(|_| Error)?; - - // Now decapsulate - ::decap( - self, - Some(&sender_pubkey.0), - &deserialized_encapped_key, - ) - .map(|ss| X25519SharedSecret::new(ss.0)) - .map_err(|_| Error) + fn decapsulate(&self, encapped_key: &X25519EncappedKey) -> Result { + ::decap(&self.0, None, &encapped_key) } } // A simple wrapper around the keypair generation function fn gen_keypair(csprng: &mut R) -> (X25519PrivateKey, X25519PublicKey) { let (sk, pk) = X25519HkdfSha256::gen_keypair(csprng); - let wrapped_pk = X25519PublicKey(pk); - - (sk, wrapped_pk) + (X25519PrivateKey(sk), X25519PublicKey(pk)) } #[test] fn test_hpke() { let mut rng = OsRng; - // Make a sender and recipient keypair - let (sk_sender, pk_sender) = gen_keypair(&mut rng); + // Make a recipient's keypair let (sk_recip, pk_recip) = gen_keypair(&mut rng); - // Try an unauthed encap first. Check that the derived shared secrets are equal - let encapper = X25519Encap; - let (ek, ss1) = encapper.try_encap(&mut rng, &pk_recip).unwrap(); - let ss2 = sk_recip.try_decap(&ek).unwrap(); - assert_eq!(ss1.as_bytes(), ss2.as_bytes()); - - // Now do an authenticated encap - let encapper = X25519AuthEncap(sk_sender, pk_sender.clone()); - let (ek, ss1) = encapper.try_encap(&mut rng, &pk_recip).unwrap(); - let ss2 = sk_recip.try_auth_decap(&ek, &pk_sender).unwrap(); - assert_eq!(ss1.as_bytes(), ss2.as_bytes()); - - // Now do an invalid authenticated encap, where the sender uses the wrong private key. This - // should produce unequal shared secrets. - let (rand_sk, _) = gen_keypair(&mut rng); - let encapper = X25519AuthEncap(rand_sk, pk_sender.clone()); - let (ek, ss1) = encapper.try_encap(&mut rng, &pk_recip).unwrap(); - let ss2 = sk_recip.try_auth_decap(&ek, &pk_sender).unwrap(); - assert_ne!(ss1.as_bytes(), ss2.as_bytes()); + // Encapsulate to the recipient. Check that the derived shared secrets are equal + let (ek, ss1) = pk_recip.encapsulate(&mut rng).unwrap(); + let ss2 = sk_recip.decapsulate(&ek).unwrap(); + assert_eq!(ss1.0, ss2.0); } From 6cc36f15c53a18f471b90bf9be849a707278a99f Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Fri, 16 Feb 2024 14:02:24 -0500 Subject: [PATCH 2/6] Updated all the tests --- kem/Cargo.toml | 30 +++++----- kem/src/errors.rs | 17 ------ kem/src/kem.rs | 80 ++++----------------------- kem/src/lib.rs | 6 +- kem/tests/hpke.rs | 27 ++++----- kem/tests/saber.rs | 104 +++++++++++------------------------ kem/tests/x3dh.rs | 134 +++++++++++++++++---------------------------- 7 files changed, 121 insertions(+), 277 deletions(-) delete mode 100644 kem/src/errors.rs diff --git a/kem/Cargo.toml b/kem/Cargo.toml index 23f9fba14..ef05ca05b 100644 --- a/kem/Cargo.toml +++ b/kem/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "kem" -description = "Traits for key encapsulation mechanisms" -version = "0.3.0-pre" -authors = ["RustCrypto Developers"] -license = "Apache-2.0 OR MIT" +name = "kem" +description = "Traits for key encapsulation mechanisms" +version = "0.3.0-pre" +authors = ["RustCrypto Developers"] +license = "Apache-2.0 OR MIT" documentation = "https://docs.rs/kem" -repository = "https://github.com/RustCrypto/traits/tree/master/kem" -readme = "README.md" -edition = "2021" -keywords = ["crypto"] -categories = ["cryptography", "no-std"] -rust-version = "1.66" +repository = "https://github.com/RustCrypto/traits/tree/master/kem" +readme = "README.md" +edition = "2021" +keywords = ["crypto"] +categories = ["cryptography", "no-std"] +rust-version = "1.66" [dependencies] rand_core = "0.6" @@ -19,10 +19,12 @@ zeroize = { version = "1.7", default-features = false } [dev-dependencies] hpke = "0.10" -p256 = { version = "0.9", features = [ "ecdsa" ] } -pqcrypto = { version = "0.15", default-features = false, features = [ "pqcrypto-saber" ] } +p256 = { version = "0.9", features = ["ecdsa"] } +pqcrypto = { version = "0.15", default-features = false, features = [ + "pqcrypto-saber", +] } pqcrypto-traits = "0.3" -rand = { version = "0.8", features = [ "getrandom" ] } +rand = { version = "0.8" } x3dh-ke = "0.1" [features] diff --git a/kem/src/errors.rs b/kem/src/errors.rs deleted file mode 100644 index 1cef35add..000000000 --- a/kem/src/errors.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! KEM error types - -use core::fmt::{Debug, Display}; - -/// Represents KEM errors. This is intentionally opaque to avoid leaking information about private -/// keys through side channels. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Error; - -impl Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "error encapsulating or decapsulating") - } -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} diff --git a/kem/src/kem.rs b/kem/src/kem.rs index 6df31d7cc..8e181e7f6 100644 --- a/kem/src/kem.rs +++ b/kem/src/kem.rs @@ -2,82 +2,24 @@ use rand_core::CryptoRngCore; +/// A value that can be encapsulated to. Often, this will just be a public key. However, +/// it can also be a bundle of public keys, or it can include a sender's private key for +/// authenticated encapsulation. pub trait Encapsulate { + /// Encapsulation error type Error; + /// Encapsulates a fresh shared secret fn encapsulate(&self, rng: impl CryptoRngCore) -> Result<(EK, SS), Self::Error>; } +/// A value that can be used to decapsulate an encapsulated key. Often, this will just +/// be a secret key. But, as with [`Encapsulate`], it can be a bundle of secret keys, +/// or it can include a sender's private key for authenticated encapsulation. pub trait Decapsulate { + /// Decapsulation error type Error; - fn decapsulate(&self, encapped_key: &EK) -> Result; + /// Decapsulates the given encapsulated key + fn decapsulate(&self, encapsulated_key: &EK) -> Result; } - -/* - -/// The shared secret that results from key exchange. -pub struct SharedSecret(GenericArray); - -impl fmt::Debug for SharedSecret { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("SharedSecret { ... }") - } -} - -// Zero the secret on drop -impl Drop for SharedSecret { - fn drop(&mut self) { - self.0.as_mut_slice().zeroize(); - } -} - -impl ZeroizeOnDrop for SharedSecret {} - -impl SharedSecret { - /// Constructs a new `SharedSecret` by wrapping the given bytes - pub fn new(bytes: GenericArray) -> Self { - SharedSecret(bytes) - } - - /// Returns borrowed bytes representing the shared secret of the key exchange - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } -} - -/// Represents the functionality of a key encapsulator. For unauthenticated encapsulation, `Self` -/// can be an empty struct. For authenticated encapsulation, `Self` is a private key. -pub trait Encapsulator { - /// Attempts to encapsulate a fresh shared secret with the given recipient. The resulting - /// shared secret is bound to the identity encoded in `Self` (i.e., authenticated wrt `Self`). - /// If `Self` is empty, then this is equivalent to unauthenticated encapsulation. Returns the - /// shared secret and encapsulated key on success, or an error if something went wrong. - fn try_encap( - &self, - csprng: &mut R, - recip_pubkey: &EK::RecipientPublicKey, - ) -> Result<(EK, SharedSecret), Error>; -} - -/// Represents the functionality of a key decapsulator, where `Self` is a cryptographic key. -pub trait Decapsulator { - /// Attempt to decapsulate the given encapsulated key. Returns the shared secret on success, or - /// an error if something went wrong. - fn try_decap(&self, encapped_key: &EK) -> Result, Error>; -} - -/// Represents the functionality of a authenticated-key decapsulator, where `Self` is a -/// cryptographic key. -pub trait AuthDecapsulator { - /// Attempt to decapsulate the given encapsulated key. The resulting shared secret is bound to - /// the provided sender identity, thus providing authenticity. Returns the shared secret - /// success, or an error if something went wrong. - fn try_auth_decap( - &self, - encapped_key: &EK, - sender_pubkey: &EK::SenderPublicKey, - ) -> Result, Error>; -} - -*/ diff --git a/kem/src/lib.rs b/kem/src/lib.rs index 7faf1d05c..f64c3c675 100644 --- a/kem/src/lib.rs +++ b/kem/src/lib.rs @@ -9,11 +9,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs, unused_qualifications, missing_debug_implementations)] -#[cfg(feature = "std")] -extern crate std; - -mod errors; mod kem; -pub use crate::{errors::*, kem::*}; +pub use crate::kem::*; pub use generic_array; diff --git a/kem/tests/hpke.rs b/kem/tests/hpke.rs index 4726a7a8e..bdabe25f4 100644 --- a/kem/tests/hpke.rs +++ b/kem/tests/hpke.rs @@ -4,47 +4,44 @@ use hpke::{ kem::{Kem as KemTrait, X25519HkdfSha256}, HpkeError, }; -use rand::rngs::OsRng; use rand_core::{CryptoRng, CryptoRngCore, RngCore}; type SharedSecret = hpke::kem::SharedSecret; +type EncappedKey = ::EncappedKey; -// Define the pubkey type. This has no trait bounds required by the library -#[derive(Clone)] -struct X25519PublicKey(::PublicKey); +// We have to define a newtype for the public and private keys because we're gonna impl +// the Encapsulate and Decapsulate traits for them +struct PublicKey(::PublicKey); +struct PrivateKey(::PrivateKey); -struct X25519PrivateKey(::PrivateKey); - -type X25519EncappedKey = ::EncappedKey; - -impl Encapsulate for X25519PublicKey { +impl Encapsulate for PublicKey { type Error = HpkeError; fn encapsulate( &self, mut csprng: impl CryptoRngCore, - ) -> Result<(X25519EncappedKey, SharedSecret), HpkeError> { + ) -> Result<(EncappedKey, SharedSecret), HpkeError> { ::encap(&self.0, None, &mut csprng).map(|(ek, ss)| (ss, ek)) } } -impl Decapsulate for X25519PrivateKey { +impl Decapsulate for PrivateKey { type Error = HpkeError; - fn decapsulate(&self, encapped_key: &X25519EncappedKey) -> Result { + fn decapsulate(&self, encapped_key: &EncappedKey) -> Result { ::decap(&self.0, None, &encapped_key) } } // A simple wrapper around the keypair generation function -fn gen_keypair(csprng: &mut R) -> (X25519PrivateKey, X25519PublicKey) { +fn gen_keypair(csprng: &mut R) -> (PrivateKey, PublicKey) { let (sk, pk) = X25519HkdfSha256::gen_keypair(csprng); - (X25519PrivateKey(sk), X25519PublicKey(pk)) + (PrivateKey(sk), PublicKey(pk)) } #[test] fn test_hpke() { - let mut rng = OsRng; + let mut rng = rand::thread_rng(); // Make a recipient's keypair let (sk_recip, pk_recip) = gen_keypair(&mut rng); diff --git a/kem/tests/saber.rs b/kem/tests/saber.rs index ce70c47d1..2e5a64b76 100644 --- a/kem/tests/saber.rs +++ b/kem/tests/saber.rs @@ -1,93 +1,53 @@ -use kem::{ - generic_array::{ - typenum::{self, U1000, U32, U472}, - GenericArray, - }, - Decapsulator, EncappedKey, Encapsulator, Error, SharedSecret, -}; +use kem::{Decapsulate, Encapsulate}; + use pqcrypto::kem::firesaber::{ - decapsulate, encapsulate, keypair, Ciphertext, PublicKey, SecretKey, + decapsulate, encapsulate, keypair, Ciphertext as SaberEncappedKey, PublicKey, SecretKey, + SharedSecret as SaberSharedSecret, }; -use pqcrypto_traits::kem::{Ciphertext as CiphertextTrait, SharedSecret as SharedSecretTrait}; -use rand::rngs::OsRng; -use rand_core::{CryptoRng, RngCore}; - -// Define the pubkey type. This has no trait bounds required by the library -type SaberPublicKey = PublicKey; +use rand_core::CryptoRngCore; -// The encapped key type is called "Ciphertext" in Rust's pqcrypto. Impl the necessary traits. -struct SaberEncappedKey(Ciphertext); -impl EncappedKey for SaberEncappedKey { - type SharedSecretSize = U32; - // FireSaber encapped keys are 1472 bytes; - type EncappedKeySize = typenum::op!(U1000 + U472); +// We have to define a newtype for the public and private keys because we're gonna impl +// the Encapsulate and Decapsulate traits for them +struct SaberPublicKey(PublicKey); +struct SaberPrivateKey(SecretKey); - // In HPKE the only recipient public key is the identity key - type RecipientPublicKey = SaberPublicKey; - // The sender's pubkey is the identity too - type SenderPublicKey = SaberPrivateKey; +impl Encapsulate for SaberPublicKey { + // TODO: Encapsulation is infallible. Make this the never type once it's available + type Error = (); - fn from_bytes(bytes: &GenericArray) -> Result { - Ciphertext::from_bytes(bytes.as_slice()) - .map(SaberEncappedKey) - .map_err(|_| Error) - } -} -impl AsRef<[u8]> for SaberEncappedKey { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} -impl core::fmt::Debug for SaberEncappedKey { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{:x?}", self.as_ref()) + fn encapsulate( + &self, + _: impl CryptoRngCore, + ) -> Result<(SaberEncappedKey, SaberSharedSecret), ()> { + let (ss, ek) = encapsulate(&self.0); + Ok((ek, ss)) } } -// Define some convenience types -type SaberSharedSecret = SharedSecret; -type SaberPrivateKey = SecretKey; - -// Define an unauthenticated encapsulator. It holds nothing at all -struct SaberEncapper; -impl Encapsulator for SaberEncapper { - fn try_encap( - &self, - _csprng: &mut R, - recip_pubkey: &SaberPublicKey, - ) -> Result<(SaberEncappedKey, SaberSharedSecret), Error> { - let (ss, ek) = encapsulate(recip_pubkey); - let ss_bytes = SaberSharedSecret::new(GenericArray::clone_from_slice(ss.as_bytes())); +impl Decapsulate for SaberPrivateKey { + // TODO: Decapsulation is infallible. Make this the never type once it's available + type Error = (); - Ok((SaberEncappedKey(ek), ss_bytes)) + fn decapsulate(&self, ek: &SaberEncappedKey) -> Result { + Ok(decapsulate(ek, &self.0)) } } -// Define a decapsulator -impl Decapsulator for SaberPrivateKey { - fn try_decap(&self, encapped_key: &SaberEncappedKey) -> Result { - let ss = decapsulate(&encapped_key.0, self); - Ok(SaberSharedSecret::new(GenericArray::clone_from_slice( - ss.as_bytes(), - ))) - } +fn gen_keypair() -> (SaberPublicKey, SaberPrivateKey) { + let (pk, sk) = keypair(); + (SaberPublicKey(pk), SaberPrivateKey(sk)) } #[test] fn test_saber() { - let mut rng = OsRng; + use pqcrypto_traits::kem::SharedSecret as _; + let mut rng = rand::thread_rng(); // Make a recipient keypair - let (pk_recip, sk_recip) = keypair(); + let (pk_recip, sk_recip) = gen_keypair(); - // Do an unauthed encap. Check that the derived shared secrets are equal - let encapper = SaberEncapper; - let (ek, ss1) = encapper.try_encap(&mut rng, &pk_recip).unwrap(); - let ss2 = sk_recip.try_decap(&ek).unwrap(); + // Encapsulate and decapsulate. Assert that the shared secrets are equal + let (ek, ss1) = pk_recip.encapsulate(&mut rng).unwrap(); + let ss2 = sk_recip.decapsulate(&ek).unwrap(); assert_eq!(ss1.as_bytes(), ss2.as_bytes()); - - // Test serialization/deserialization - let ek_bytes = ek.as_bytes(); - let ek2 = SaberEncappedKey::from_bytes(ek_bytes).unwrap(); - assert_eq!(ek.as_bytes(), ek2.as_bytes()); } diff --git a/kem/tests/x3dh.rs b/kem/tests/x3dh.rs index 0081a7197..d3c688fc6 100644 --- a/kem/tests/x3dh.rs +++ b/kem/tests/x3dh.rs @@ -1,20 +1,12 @@ -use kem::{ - generic_array::{ - typenum::{self, Unsigned}, - GenericArray, - }, - AuthDecapsulator, EncappedKey, Encapsulator, Error, SharedSecret, -}; +use kem::{Decapsulate, Encapsulate}; + use p256::ecdsa::Signature; -use rand::rngs::OsRng; -use rand_core::{CryptoRng, RngCore}; +use rand_core::CryptoRngCore; use x3dh_ke::{x3dh_a, x3dh_b, EphemeralKey, IdentityKey, Key, OneTimePreKey, SignedPreKey}; -// The size of an encapped key. This is the number of bytes in an uncompressed P256 point -type NEnc = typenum::U231; +/// The shared secret type defined by x3dh_ke +type SharedSecret = [u8; 32]; -// Define the sender pubkey type. This is an identity key; -type X3DhSenderPublicKey = IdentityKey; // Define the recipient privkey type. This is a bundle of 3 privkeys of different lifespans struct X3DhPrivkeyBundle { ik: IdentityKey, @@ -22,8 +14,10 @@ struct X3DhPrivkeyBundle { sig: Signature, opk: OneTimePreKey, } + impl X3DhPrivkeyBundle { fn gen() -> X3DhPrivkeyBundle { + // The default() method does actual key generation here let ik = IdentityKey::default(); let spk = SignedPreKey::default(); let sig = ik.sign(&spk.pk_to_bytes()); @@ -39,111 +33,81 @@ impl X3DhPrivkeyBundle { } } } + // The pubkeys keys associated with a privkey bundle. In x3dh-ke, all the keys serve as both // pubkeys and privkeys. This seems dangerous but hey this isn't prod. type X3DhPubkeyBundle = X3DhPrivkeyBundle; -// The encapped key is just the byte repr of an ephemeral key. Impl the appropriate traits -#[derive(Debug)] -struct X3DhEncappedKey([u8; NEnc::USIZE]); -impl EncappedKey for X3DhEncappedKey { - type SharedSecretSize = typenum::U32; - type EncappedKeySize = NEnc; - type SenderPublicKey = X3DhSenderPublicKey; - type RecipientPublicKey = X3DhPubkeyBundle; - - fn from_bytes(bytes: &GenericArray) -> Result { - let mut buf = [0u8; NEnc::USIZE]; - buf.copy_from_slice(bytes); - Ok(X3DhEncappedKey(buf)) - } -} -impl AsRef<[u8]> for X3DhEncappedKey { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} +/// To encap, we need the recipient's public keys and the sender's private key +struct EncapContext(X3DhPubkeyBundle, IdentityKey); -// The private key of an authenticated sender is just their identity key. Again, this is the same -// type as the pubkey. -type X3DhSenderPrivateKey = IdentityKey; -type X3DhSharedSecret = SharedSecret; +/// To decap, we need the recipient's private keys and the sender's public key +struct DecapContext(X3DhPrivkeyBundle, IdentityKey); // Define an authenticated encapsulator. To authenticate, we need a full sender keypair. -impl Encapsulator for X3DhSenderPrivateKey { - fn try_encap( +impl Encapsulate for EncapContext { + type Error = &'static str; + + fn encapsulate( &self, - _csprng: &mut R, - recip_pubkey: &X3DhPubkeyBundle, - ) -> Result<(X3DhEncappedKey, X3DhSharedSecret), Error> { + _: impl CryptoRngCore, + ) -> Result<(EphemeralKey, SharedSecret), Self::Error> { // Make a new ephemeral key. This will be the encapped key let ek = EphemeralKey::default(); // Deconstruct the recipient's pubkey bundle - let X3DhPubkeyBundle { ik, spk, sig, opk } = recip_pubkey; + let X3DhPubkeyBundle { + ref ik, + ref spk, + ref sig, + ref opk, + } = self.0; + let my_ik = &self.1; // Do the X3DH operation to get the shared secret - let shared_secret = x3dh_a(sig, self, spk, &ek, ik, opk) - .map(|ss| X3DhSharedSecret::new(ss.into())) - .map_err(|e| { - println!("err {:?}", e); - Error - })?; - // Serialize the ephemeral key - let encapped_key = X3DhEncappedKey::from_bytes(ek.to_bytes().as_slice().into())?; - - Ok((encapped_key, shared_secret)) + let shared_secret = x3dh_a(sig, my_ik, spk, &ek, ik, opk)?; + + Ok((ek, shared_secret)) } } // Define an decapsulator. Since authenticated and unauthenticated encapped keys are represented by // the same type (which, outside of testing, should not be the case), this can do both auth'd and // unauth'd decapsulation. -impl AuthDecapsulator for X3DhPrivkeyBundle { - fn try_auth_decap( - &self, - encapped_key: &X3DhEncappedKey, - sender_pubkey: &X3DhSenderPublicKey, - ) -> Result { - // First parse the encapped key, since it's just bytes right now - let deserialized_ek = EphemeralKey::from_bytes(&encapped_key.0).map_err(|_| Error)?; +impl Decapsulate for DecapContext { + // TODO: Decapsulation is infallible. Make the Error type `!` when it's stable. + type Error = (); + + fn decapsulate(&self, ek: &EphemeralKey) -> Result { // Deconstruct our private keys bundle - let X3DhPubkeyBundle { - ik, - spk, - sig: _, - opk, - } = self; + let X3DhPrivkeyBundle { + ref ik, + ref spk, + ref opk, + .. + } = self.0; + let sender_pubkey = &self.1; // Now decapsulate - let buf = x3dh_b(sender_pubkey, spk, &deserialized_ek, ik, opk); - Ok(X3DhSharedSecret::new(buf.into())) + Ok(x3dh_b(sender_pubkey, spk, &ek, ik, opk)) } } #[test] fn test_x3dh() { - let mut rng = OsRng; + let mut rng = rand::thread_rng(); // We use _a and _b suffixes to denote whether a key belongs to Alice or Bob. Alice is the // sender in this case. - let sk_ident_a = X3DhSenderPrivateKey::default(); + let sk_ident_a = IdentityKey::default(); let pk_ident_a = sk_ident_a.strip(); let sk_bundle_b = X3DhPrivkeyBundle::gen(); let pk_bundle_b = sk_bundle_b.as_pubkeys(); + let encap_context = EncapContext(pk_bundle_b, sk_ident_a); + let decap_context = DecapContext(sk_bundle_b, pk_ident_a); + // Now do an authenticated encap - let (encapped_key, ss1) = sk_ident_a.try_encap(&mut rng, &pk_bundle_b).unwrap(); - let ss2 = sk_bundle_b - .try_auth_decap(&encapped_key, &pk_ident_a) - .unwrap(); - assert_eq!(ss1.as_bytes(), ss2.as_bytes()); - - // Now do an invalid authenticated encap, where the sender uses the wrong private key. This - // should produce unequal shared secrets. - let sk_ident_rando = X3DhSenderPrivateKey::default(); - let (encapped_key, ss1) = sk_ident_rando.try_encap(&mut rng, &pk_bundle_b).unwrap(); - let ss2 = sk_bundle_b - .try_auth_decap(&encapped_key, &pk_ident_a) - .unwrap(); - assert_ne!(ss1.as_bytes(), ss2.as_bytes()); + let (encapped_key, ss1) = encap_context.encapsulate(&mut rng).unwrap(); + let ss2 = decap_context.decapsulate(&encapped_key).unwrap(); + assert_eq!(ss1, ss2); } From 9ae0438e22d56fa4021bdde3d493f43f4f070898 Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Fri, 16 Feb 2024 14:19:23 -0500 Subject: [PATCH 3/6] Deleted cruft; Updated docs --- Cargo.lock | 1 - kem/Cargo.toml | 4 +++- kem/README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++- kem/src/kem.rs | 25 --------------------- kem/src/lib.rs | 23 ++++++++++++++++--- 5 files changed, 83 insertions(+), 31 deletions(-) delete mode 100644 kem/src/kem.rs diff --git a/Cargo.lock b/Cargo.lock index 7b77d7b5d..050fafda2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,6 @@ dependencies = [ name = "kem" version = "0.3.0-pre" dependencies = [ - "generic-array", "hpke", "p256 0.9.0", "pqcrypto", diff --git a/kem/Cargo.toml b/kem/Cargo.toml index ef05ca05b..83ebf4b89 100644 --- a/kem/Cargo.toml +++ b/kem/Cargo.toml @@ -14,7 +14,6 @@ rust-version = "1.66" [dependencies] rand_core = "0.6" -generic-array = "0.14" zeroize = { version = "1.7", default-features = false } [dev-dependencies] @@ -34,3 +33,6 @@ std = [] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lib] +doctest = false diff --git a/kem/README.md b/kem/README.md index 51be87890..8ca46b4ed 100644 --- a/kem/README.md +++ b/kem/README.md @@ -9,7 +9,66 @@ This crate provides a common set of traits for [key encapsulation mechanisms][1]—algorithms for non-interactively establishing secrets between peers. This is intended to be implemented by libraries which produce or contain implementations of key encapsulation mechanisms, and used by libraries which want to produce or consume encapsulated secrets while generically supporting any compatible backend. -The crate exposes four traits, `Encapsulator`, `Decapsulator`, `AuthEncapsulator`, and `AuthDecapsulator`. These traits represent the ability to initiate a key exchange and complete a key exchange, in the case where the sender is authenticated to the receiver and in the case where the sender is not. +The crate exposes two traits, `Encapsulate` and `Decapsulate`, which are both generic over the encapsulated key type and the shared secret type. They are also agnostic about the structure of `Self`. For example, a simple Saber implementation may just impl `Encapsulate` for a single public key: +```rust +// Must make a newtype to implement the trait +struct MyPubkey(SaberPublicKey); + +impl Encapsulate for MyPubkey { + // Encapsulation is infallible + type Error = !; + + fn encapsulate( + &self, + csprng: impl CryptoRngCore, + ) -> Result<(SaberEncappedKey, SaberSharedSecret), !> { + let (ss, ek) = saber_encapsulate(&csprng, &self.0); + Ok((ek, ss)) + } +} +``` +And on the other end of complexity, an [X3DH](https://www.signal.org/docs/specifications/x3dh/) implementation might impl `Encapsulate` for a public key bundle plus a sender identity key: +```rust +struct PubkeyBundle { + ik: IdentityPubkey, + spk: SignedPrePubkey, + sig: Signature, + opk: OneTimePrePubkey, +} + +// Encap context is the recipient's pubkeys and the sender's identity key +struct EncapContext(PubkeyBundle, IdentityPrivkey); + +impl Encapsulate for EncapContext { + // Encapsulation fails if signature verification fails + type Error = SigError; + + fn encapsulate( + &self, + csprng: impl CryptoRngCore, + ) -> Result<(EphemeralKey, SharedSecret), Self::Error> { + // Make a new ephemeral key. This will be the encapped key + let ek = EphemeralKey::gen(&mut csprng); + + // Deconstruct the recipient's pubkey bundle + let PubkeyBundle { + ref ik, + ref spk, + ref sig, + ref opk, + } = self.0; + let my_ik = &self.1; + + // Verify the signature + self.0.verify(&sig, &some_sig_pubkey)?; + + // Do the X3DH operation to get the shared secret + let shared_secret = x3dh_a(sig, my_ik, spk, &ek, ik, opk)?; + + Ok((ek, shared_secret)) + } +} +``` [Documentation][docs-link] diff --git a/kem/src/kem.rs b/kem/src/kem.rs deleted file mode 100644 index 8e181e7f6..000000000 --- a/kem/src/kem.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! KEM Traits - -use rand_core::CryptoRngCore; - -/// A value that can be encapsulated to. Often, this will just be a public key. However, -/// it can also be a bundle of public keys, or it can include a sender's private key for -/// authenticated encapsulation. -pub trait Encapsulate { - /// Encapsulation error - type Error; - - /// Encapsulates a fresh shared secret - fn encapsulate(&self, rng: impl CryptoRngCore) -> Result<(EK, SS), Self::Error>; -} - -/// A value that can be used to decapsulate an encapsulated key. Often, this will just -/// be a secret key. But, as with [`Encapsulate`], it can be a bundle of secret keys, -/// or it can include a sender's private key for authenticated encapsulation. -pub trait Decapsulate { - /// Decapsulation error - type Error; - - /// Decapsulates the given encapsulated key - fn decapsulate(&self, encapsulated_key: &EK) -> Result; -} diff --git a/kem/src/lib.rs b/kem/src/lib.rs index f64c3c675..f12e42de6 100644 --- a/kem/src/lib.rs +++ b/kem/src/lib.rs @@ -9,7 +9,24 @@ #![forbid(unsafe_code)] #![warn(missing_docs, unused_qualifications, missing_debug_implementations)] -mod kem; +/// A value that can be encapsulated to. Often, this will just be a public key. However, it can +/// also be a bundle of public keys, or it can include a sender's private key for authenticated +/// encapsulation. +pub trait Encapsulate { + /// Encapsulation error + type Error; -pub use crate::kem::*; -pub use generic_array; + /// Encapsulates a fresh shared secret + fn encapsulate(&self, rng: impl rand_core::CryptoRngCore) -> Result<(EK, SS), Self::Error>; +} + +/// A value that can be used to decapsulate an encapsulated key. Often, this will just be a secret +/// key. But, as with [`Encapsulate`], it can be a bundle of secret keys, or it can include a +/// sender's private key for authenticated encapsulation. +pub trait Decapsulate { + /// Decapsulation error + type Error; + + /// Decapsulates the given encapsulated key + fn decapsulate(&self, encapsulated_key: &EK) -> Result; +} From 1b8dd29f2b8d679268157c0e5adad663e2462291 Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Fri, 16 Feb 2024 14:21:35 -0500 Subject: [PATCH 4/6] Removed std feature --- kem/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/kem/Cargo.toml b/kem/Cargo.toml index 83ebf4b89..d306ea4c2 100644 --- a/kem/Cargo.toml +++ b/kem/Cargo.toml @@ -26,10 +26,6 @@ pqcrypto-traits = "0.3" rand = { version = "0.8" } x3dh-ke = "0.1" -[features] -default = [] -std = [] - [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] From dd3e5259e8782d6c8ca4f439be5ecd33b9c84ec1 Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Fri, 16 Feb 2024 17:19:26 -0500 Subject: [PATCH 5/6] Made clippy happy --- kem/tests/hpke.rs | 2 +- kem/tests/x3dh.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kem/tests/hpke.rs b/kem/tests/hpke.rs index bdabe25f4..732937e0d 100644 --- a/kem/tests/hpke.rs +++ b/kem/tests/hpke.rs @@ -29,7 +29,7 @@ impl Decapsulate for PrivateKey { type Error = HpkeError; fn decapsulate(&self, encapped_key: &EncappedKey) -> Result { - ::decap(&self.0, None, &encapped_key) + ::decap(&self.0, None, encapped_key) } } diff --git a/kem/tests/x3dh.rs b/kem/tests/x3dh.rs index d3c688fc6..5b3de9f41 100644 --- a/kem/tests/x3dh.rs +++ b/kem/tests/x3dh.rs @@ -88,7 +88,7 @@ impl Decapsulate for DecapContext { let sender_pubkey = &self.1; // Now decapsulate - Ok(x3dh_b(sender_pubkey, spk, &ek, ik, opk)) + Ok(x3dh_b(sender_pubkey, spk, ek, ik, opk)) } } From d24de1062d49d50071b6ed2036f339c6bdd04ca2 Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Thu, 28 Mar 2024 11:11:07 -0400 Subject: [PATCH 6/6] kem: Made rng input a mut ref; made Error assoc type impl Debug --- kem/src/lib.rs | 9 ++++++--- kem/tests/hpke.rs | 2 +- kem/tests/saber.rs | 2 +- kem/tests/x3dh.rs | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/kem/src/lib.rs b/kem/src/lib.rs index f12e42de6..27dc0aa19 100644 --- a/kem/src/lib.rs +++ b/kem/src/lib.rs @@ -9,15 +9,18 @@ #![forbid(unsafe_code)] #![warn(missing_docs, unused_qualifications, missing_debug_implementations)] +use core::fmt::Debug; +use rand_core::CryptoRngCore; + /// A value that can be encapsulated to. Often, this will just be a public key. However, it can /// also be a bundle of public keys, or it can include a sender's private key for authenticated /// encapsulation. pub trait Encapsulate { /// Encapsulation error - type Error; + type Error: Debug; /// Encapsulates a fresh shared secret - fn encapsulate(&self, rng: impl rand_core::CryptoRngCore) -> Result<(EK, SS), Self::Error>; + fn encapsulate(&self, rng: &mut impl CryptoRngCore) -> Result<(EK, SS), Self::Error>; } /// A value that can be used to decapsulate an encapsulated key. Often, this will just be a secret @@ -25,7 +28,7 @@ pub trait Encapsulate { /// sender's private key for authenticated encapsulation. pub trait Decapsulate { /// Decapsulation error - type Error; + type Error: Debug; /// Decapsulates the given encapsulated key fn decapsulate(&self, encapsulated_key: &EK) -> Result; diff --git a/kem/tests/hpke.rs b/kem/tests/hpke.rs index 732937e0d..43ad61c9d 100644 --- a/kem/tests/hpke.rs +++ b/kem/tests/hpke.rs @@ -19,7 +19,7 @@ impl Encapsulate for PublicKey { fn encapsulate( &self, - mut csprng: impl CryptoRngCore, + mut csprng: &mut impl CryptoRngCore, ) -> Result<(EncappedKey, SharedSecret), HpkeError> { ::encap(&self.0, None, &mut csprng).map(|(ek, ss)| (ss, ek)) } diff --git a/kem/tests/saber.rs b/kem/tests/saber.rs index 2e5a64b76..13e85ef6b 100644 --- a/kem/tests/saber.rs +++ b/kem/tests/saber.rs @@ -17,7 +17,7 @@ impl Encapsulate for SaberPublicKey { fn encapsulate( &self, - _: impl CryptoRngCore, + _: &mut impl CryptoRngCore, ) -> Result<(SaberEncappedKey, SaberSharedSecret), ()> { let (ss, ek) = encapsulate(&self.0); Ok((ek, ss)) diff --git a/kem/tests/x3dh.rs b/kem/tests/x3dh.rs index 5b3de9f41..b39a5adf8 100644 --- a/kem/tests/x3dh.rs +++ b/kem/tests/x3dh.rs @@ -50,7 +50,7 @@ impl Encapsulate for EncapContext { fn encapsulate( &self, - _: impl CryptoRngCore, + _: &mut impl CryptoRngCore, ) -> Result<(EphemeralKey, SharedSecret), Self::Error> { // Make a new ephemeral key. This will be the encapped key let ek = EphemeralKey::default();