From 59c34d2459214dca25abc8ca252a834acc2c654f Mon Sep 17 00:00:00 2001 From: Michael Rosenberg Date: Tue, 2 Apr 2024 13:01:24 -0400 Subject: [PATCH] kem: simplify API (#1509) * Simplified API significantly; updated HPKE test * Updated all the tests * Deleted cruft; Updated docs * Removed std feature * Made clippy happy * kem: Made rng input a mut ref; made Error assoc type impl Debug --- Cargo.lock | 1 - kem/Cargo.toml | 38 +++++------ kem/README.md | 61 ++++++++++++++++- kem/src/errors.rs | 17 ----- kem/src/kem.rs | 100 --------------------------- kem/src/lib.rs | 28 ++++++-- kem/tests/hpke.rs | 166 ++++++++------------------------------------- kem/tests/saber.rs | 104 +++++++++------------------- kem/tests/x3dh.rs | 134 +++++++++++++----------------------- 9 files changed, 210 insertions(+), 439 deletions(-) delete mode 100644 kem/src/errors.rs delete mode 100644 kem/src/kem.rs diff --git a/Cargo.lock b/Cargo.lock index 31812c324..8f099192e 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 23f9fba14..d306ea4c2 100644 --- a/kem/Cargo.toml +++ b/kem/Cargo.toml @@ -1,34 +1,34 @@ [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" -generic-array = "0.14" 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] -default = [] -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/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 deleted file mode 100644 index 1963883ef..000000000 --- a/kem/src/kem.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! KEM Traits - -use crate::errors::Error; - -use core::fmt; - -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; - - /// Parses an encapsulated key from its byte representation. - fn from_bytes(bytes: &GenericArray) -> Result; - - /// 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() - } -} - -/// 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..27dc0aa19 100644 --- a/kem/src/lib.rs +++ b/kem/src/lib.rs @@ -9,11 +9,27 @@ #![forbid(unsafe_code)] #![warn(missing_docs, unused_qualifications, missing_debug_implementations)] -#[cfg(feature = "std")] -extern crate std; +use core::fmt::Debug; +use rand_core::CryptoRngCore; -mod errors; -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: Debug; -pub use crate::{errors::*, kem::*}; -pub use generic_array; + /// Encapsulates a fresh shared secret + 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 +/// 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: 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 43df15cd2..43ad61c9d 100644 --- a/kem/tests/hpke.rs +++ b/kem/tests/hpke.rs @@ -1,163 +1,53 @@ +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}; -// Define the pubkey type. This has no trait bounds required by the library -#[derive(Clone)] -struct X25519PublicKey(::PublicKey); +type SharedSecret = hpke::kem::SharedSecret; +type EncappedKey = ::EncappedKey; -// 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; +// 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); - fn from_bytes(bytes: &GenericArray) -> Result { - Ok(X25519EncappedKey(*bytes)) - } -} -impl AsRef<[u8]> for X25519EncappedKey { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -// 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 PublicKey { + 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: &mut impl CryptoRngCore, + ) -> Result<(EncappedKey, 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 PrivateKey { + 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: &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); - let wrapped_pk = X25519PublicKey(pk); - - (sk, wrapped_pk) + (PrivateKey(sk), PublicKey(pk)) } #[test] fn test_hpke() { - let mut rng = OsRng; + let mut rng = rand::thread_rng(); - // 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); } diff --git a/kem/tests/saber.rs b/kem/tests/saber.rs index ce70c47d1..13e85ef6b 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, + _: &mut 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..b39a5adf8 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> { + _: &mut 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); }