Skip to content

Commit

Permalink
Support X25519Kyber768Draft00 hybrid post-quantum KEM (#43)
Browse files Browse the repository at this point in the history
See https://datatracker.ietf.org/doc/draft-westerbaan-cfrg-hpke-xyber768d00/

---------

Co-authored-by: Mathieu Amiot <amiot.mathieu@gmail.com>
Co-authored-by: Michael Rosenberg <michael@mrosenberg.pub>
  • Loading branch information
3 people authored Jul 18, 2023
1 parent 2867b0a commit e519560
Show file tree
Hide file tree
Showing 9 changed files with 497 additions and 66 deletions.
21 changes: 19 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ categories = ["cryptography", "no-std"]
# "p256" enables the use of ECDH-NIST-P256 as a KEM
# "p384" enables the use of ECDH-NIST-P384 as a KEM
# "x25519" enables the use of the X25519 as a KEM
# "xyber768d00" enables the use of X25519Kyber768Draft00 as a KEM
default = ["alloc", "p256", "x25519"]
x25519 = ["dep:x25519-dalek"]
p384 = ["dep:p384"]
p256 = ["dep:p256"]
xyber768d00 = ["dep:pqc_kyber", "x25519"]
# Include serde Serialize/Deserialize impls for all relevant types
serde_impls = ["serde", "generic-array/serde"]
serde_impls = ["dep:serde", "generic-array/serde"]
# Include allocating methods like open() and seal()
alloc = []
# Includes an implementation of `std::error::Error` for `HpkeError`. Also does what `alloc` does.
Expand All @@ -39,7 +41,7 @@ rand_core = { version = "0.6", default-features = false }
p256 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdh"], optional = true}
p384 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdh"], optional = true}
sha2 = { version = "0.10", default-features = false }
serde = { version = "1.0", default-features = false, optional = true }
serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] }
subtle = { version = "2.4", default-features = false }
zeroize = { version = "1", default-features = false, features = ["zeroize_derive"] }

Expand All @@ -49,6 +51,21 @@ default-features = false
features = ["u64_backend"]
optional = true

[dependencies.pqc_kyber]
# Be careful when switching to upstream, as the latest version might not have
# been reviewed for implementation mistakes, and might still use explicit
# rejection. Also, enable the avx2 at your own risk, as it's not been reviewed.
# Examples of earlier issues:
#
# https://github.com/Argyle-Software/kyber/issues/73
# https://github.com/Argyle-Software/kyber/issues/75
# https://github.com/Argyle-Software/kyber/issues/77
git = "https://github.com/bwesterb/argyle-kyber"
package = "safe_pqc_kyber"
default-features = false
features = ["kyber768", "std"] # TODO get rid of std dep
optional = true

[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }
hex = "0.4"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# TESTING

This is a testing branch for [draft v2](https://www.ietf.org/archive/id/draft-westerbaan-cfrg-hpke-xyber768d00-02.html) of an HPKE _hybrid post-quantum_ ciphersuite. In short, this ciphersuite, X25519Kyber768Draft00, does both X25519 and [Kyber](https://pq-crystals.org/kyber/) encapsulation/decapsulation, and uses _both_ shared secrets to establish a secure session. This construction is secure so long as at least one of its components, X25519 or Kyber, is secure.

**Do NOT use this branch for anything other than testing**

rust-hpke
=========
[![Version](https://img.shields.io/crates/v/hpke.svg)](https://crates.io/crates/hpke)
Expand Down
145 changes: 87 additions & 58 deletions src/kat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use crate::{
kdf::{HkdfSha256, HkdfSha384, HkdfSha512, Kdf as KdfTrait},
kem::{
self, DhP256HkdfSha256, DhP384HkdfSha384, Kem as KemTrait, SharedSecret, X25519HkdfSha256,
X25519Kyber768Draft00,
},
op_mode::{OpModeR, PskBundle},
setup::setup_receiver,
test_util::PromptedRng,
Deserializable, HpkeError, Serializable,
};

Expand All @@ -20,59 +22,74 @@ use serde_json;
trait TestableKem: KemTrait {
/// The ephemeral key used in encapsulation. This is the same thing as a private key in the
/// case of DHKEM, but this is not always true
type EphemeralKey: Deserializable;

// Encap with fixed randomness
#[doc(hidden)]
fn encap_with_eph(
fn encaps_det(
tv: &MainTestVector,
pk_recip: &Self::PublicKey,
sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>,
sk_eph: Self::EphemeralKey,
) -> Result<(SharedSecret<Self>, Self::EncappedKey), HpkeError>;
}

// Now implement TestableKem for all the KEMs in the KAT
// Now implement TestableDhKem for all the KEMs in the KAT
impl TestableKem for X25519HkdfSha256 {
// In DHKEM, ephemeral keys and private keys are both scalars
type EphemeralKey = <X25519HkdfSha256 as KemTrait>::PrivateKey;

// Call the x25519 deterministic encap function we defined in dhkem.rs
fn encap_with_eph(
fn encaps_det(
tv: &MainTestVector,
pk_recip: &Self::PublicKey,
sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>,
sk_eph: Self::EphemeralKey,
) -> Result<(SharedSecret<Self>, Self::EncappedKey), HpkeError> {
// In DHKEM, ephemeral keys and private keys are both scalars
type EphemeralKey = <X25519HkdfSha256 as KemTrait>::PrivateKey;

let sk_eph = EphemeralKey::from_bytes(tv.sk_eph.as_ref().unwrap()).unwrap();
kem::x25519_hkdfsha256::encap_with_eph(pk_recip, sender_id_keypair, sk_eph)
}
}
impl TestableKem for DhP256HkdfSha256 {
// In DHKEM, ephemeral keys and private keys are both scalars
type EphemeralKey = <DhP256HkdfSha256 as KemTrait>::PrivateKey;

// Call the p256 deterministic encap function we defined in dhkem.rs
fn encap_with_eph(
fn encaps_det(
tv: &MainTestVector,
pk_recip: &Self::PublicKey,
sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>,
sk_eph: Self::EphemeralKey,
) -> Result<(SharedSecret<Self>, Self::EncappedKey), HpkeError> {
// In DHKEM, ephemeral keys and private keys are both scalars
type EphemeralKey = <DhP256HkdfSha256 as KemTrait>::PrivateKey;

let sk_eph = EphemeralKey::from_bytes(tv.sk_eph.as_ref().unwrap()).unwrap();
kem::dhp256_hkdfsha256::encap_with_eph(pk_recip, sender_id_keypair, sk_eph)
}
}

impl TestableKem for DhP384HkdfSha384 {
// In DHKEM, ephemeral keys and private keys are both scalars
type EphemeralKey = <DhP384HkdfSha384 as KemTrait>::PrivateKey;

// Call the p384 deterministic encap function we defined in dhkem.rs
fn encap_with_eph(
fn encaps_det(
tv: &MainTestVector,
pk_recip: &Self::PublicKey,
sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>,
sk_eph: Self::EphemeralKey,
) -> Result<(SharedSecret<Self>, Self::EncappedKey), HpkeError> {
// In DHKEM, ephemeral keys and private keys are both scalars
type EphemeralKey = <DhP384HkdfSha384 as KemTrait>::PrivateKey;

let sk_eph = EphemeralKey::from_bytes(tv.sk_eph.as_ref().unwrap()).unwrap();
kem::dhp384_hkdfsha384::encap_with_eph(pk_recip, sender_id_keypair, sk_eph)
}
}

impl TestableKem for X25519Kyber768Draft00 {
fn encaps_det(
tv: &MainTestVector,
pk_recip: &Self::PublicKey,
sender_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>,
) -> Result<(SharedSecret<Self>, Self::EncappedKey), HpkeError> {
// The encap randomness is given in the TV. Rig a PRNG to output exactly that sequence, and
// run the encapsulation process.
let seed = tv.encap_randomness.as_ref().unwrap();
let mut prng = PromptedRng::new(&seed);
let ret = X25519Kyber768Draft00::encap(&pk_recip, sender_keypair, &mut prng);
prng.assert_done();
ret
}
}

/// Asserts that the given serializable values are equal
macro_rules! assert_serializable_eq {
($a:expr, $b:expr, $args:tt) => {
Expand Down Expand Up @@ -117,16 +134,18 @@ struct MainTestVector {
ikm_recip: Vec<u8>,
#[serde(default, rename = "ikmS", deserialize_with = "bytes_from_hex_opt")]
ikm_sender: Option<Vec<u8>>,
#[serde(rename = "ikmE", deserialize_with = "bytes_from_hex")]
_ikm_eph: Vec<u8>,
#[serde(default, rename = "ikmE", deserialize_with = "bytes_from_hex_opt")]
_ikm_eph: Option<Vec<u8>>,
#[serde(default, rename = "ier", deserialize_with = "bytes_from_hex_opt")]
encap_randomness: Option<Vec<u8>>,

// Private keys
#[serde(rename = "skRm", deserialize_with = "bytes_from_hex")]
sk_recip: Vec<u8>,
#[serde(default, rename = "skSm", deserialize_with = "bytes_from_hex_opt")]
sk_sender: Option<Vec<u8>>,
#[serde(rename = "skEm", deserialize_with = "bytes_from_hex")]
sk_eph: Vec<u8>,
#[serde(default, rename = "skEm", deserialize_with = "bytes_from_hex_opt")]
sk_eph: Option<Vec<u8>>,

// Preshared Key Bundle
#[serde(default, deserialize_with = "bytes_from_hex_opt")]
Expand All @@ -139,8 +158,8 @@ struct MainTestVector {
pk_recip: Vec<u8>,
#[serde(default, rename = "pkSm", deserialize_with = "bytes_from_hex_opt")]
pk_sender: Option<Vec<u8>>,
#[serde(rename = "pkEm", deserialize_with = "bytes_from_hex")]
_pk_eph: Vec<u8>,
#[serde(default, rename = "pkEm", deserialize_with = "bytes_from_hex_opt")]
_pk_eph: Option<Vec<u8>>,

// Key schedule inputs and computations
#[serde(rename = "enc", deserialize_with = "bytes_from_hex")]
Expand Down Expand Up @@ -226,7 +245,6 @@ fn make_op_mode_r<'a, Kem: KemTrait>(
fn test_case<A: Aead, Kdf: KdfTrait, Kem: TestableKem>(tv: MainTestVector) {
// First, deserialize all the relevant keys so we can reconstruct the encapped key
let recip_keypair = deser_keypair::<Kem>(&tv.sk_recip, &tv.pk_recip);
let sk_eph = <Kem as TestableKem>::EphemeralKey::from_bytes(&tv.sk_eph).unwrap();
let sender_keypair = {
let pk_sender = &tv.pk_sender.as_ref();
tv.sk_sender
Expand All @@ -241,7 +259,7 @@ fn test_case<A: Aead, Kdf: KdfTrait, Kem: TestableKem>(tv: MainTestVector) {
assert_serializable_eq!(recip_keypair.1, derived_kp.1, "pk recip doesn't match");
}
if let Some(sks) = sender_keypair.as_ref() {
let derived_kp = Kem::derive_keypair(&tv.ikm_sender.unwrap());
let derived_kp = Kem::derive_keypair(tv.ikm_sender.as_ref().unwrap());
assert_serializable_eq!(sks.0, derived_kp.0, "sk sender doesn't match");
assert_serializable_eq!(sks.1, derived_kp.1, "pk sender doesn't match");
}
Expand All @@ -250,9 +268,9 @@ fn test_case<A: Aead, Kdf: KdfTrait, Kem: TestableKem>(tv: MainTestVector) {

// Now derive the encapped key with the deterministic encap function, using all the inputs
// above
let (shared_secret, encapped_key) = {
let (shared_secret, encapped_key): (kem::SharedSecret<Kem>, _) = {
let sender_keypair_ref = sender_keypair.as_ref().map(|&(ref sk, ref pk)| (sk, pk));
Kem::encap_with_eph(&pk_recip, sender_keypair_ref, sk_eph).expect("encap failed")
TestableKem::encaps_det(&tv, &pk_recip, sender_keypair_ref).expect("encap failed")
};

// Assert that the derived shared secret key is identical to the one provided
Expand Down Expand Up @@ -361,32 +379,43 @@ macro_rules! dispatch_testcase {

#[test]
fn kat_test() {
let file = File::open("test-vectors-5f503c5.json").unwrap();
let tvs: Vec<MainTestVector> = serde_json::from_reader(file).unwrap();

for tv in tvs.into_iter() {
// Ignore everything that doesn't use X25519, P256, or P384, since that's all we support
// right now
if tv.kem_id != X25519HkdfSha256::KEM_ID
&& tv.kem_id != DhP256HkdfSha256::KEM_ID
&& tv.kem_id != DhP384HkdfSha384::KEM_ID
{
continue;
}

// This unrolls into 36 `if let` statements
dispatch_testcase!(
tv,
(AesGcm128, AesGcm256, ChaCha20Poly1305, ExportOnlyAead),
(HkdfSha256, HkdfSha384, HkdfSha512),
(X25519HkdfSha256, DhP256HkdfSha256, DhP384HkdfSha384)
);
for file_name in [
"test-vectors-5f503c5.json",
"test-vectors-xyber768d00-02.json",
] {
let file = File::open(file_name).unwrap();
let tvs: Vec<MainTestVector> = serde_json::from_reader(file).unwrap();

for tv in tvs.into_iter() {
// Ignore everything that doesn't use X25519, P256, P384,
// or X25519Kyber768Draft00 since that's all we support right now
if tv.kem_id != X25519HkdfSha256::KEM_ID
&& tv.kem_id != DhP256HkdfSha256::KEM_ID
&& tv.kem_id != DhP384HkdfSha384::KEM_ID
&& tv.kem_id != X25519Kyber768Draft00::KEM_ID
{
continue;
}

// This unrolls into 36 `if let` statements
dispatch_testcase!(
tv,
(AesGcm128, AesGcm256, ChaCha20Poly1305, ExportOnlyAead),
(HkdfSha256, HkdfSha384, HkdfSha512),
(
X25519HkdfSha256,
DhP256HkdfSha256,
DhP384HkdfSha384,
X25519Kyber768Draft00
)
);

// The above macro has a `continue` in every branch. We only get to this line if it failed
// to match every combination of the above primitives.
panic!(
"Unrecognized (AEAD ID, KDF ID, KEM ID) combo: ({}, {}, {})",
tv.aead_id, tv.kdf_id, tv.kem_id
);
// The above macro has a `continue` in every branch. We only get to this line if it failed
// to match every combination of the above primitives.
panic!(
"Unrecognized (AEAD ID, KDF ID, KEM ID) combo: ({}, {}, {})",
tv.aead_id, tv.kdf_id, tv.kem_id
);
}
}
}
9 changes: 9 additions & 0 deletions src/kem.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Traits and structs for key encapsulation mechanisms
// We allow ambiguous ::* exports because every KEM exports a doc(hidden) type called EncappedKey.
// The user never sees it, but the compiler still thinks it's ambiguous.
#![allow(ambiguous_glob_reexports)]

use crate::{Deserializable, HpkeError, Serializable};

use core::fmt::Debug;
Expand All @@ -11,6 +15,11 @@ use zeroize::Zeroize;
mod dhkem;
pub use dhkem::*;

#[cfg(feature = "xyber768d00")]
pub(crate) mod xyber768d00;
#[cfg(feature = "xyber768d00")]
pub use xyber768d00::*;

#[cfg(feature = "serde_impls")]
use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};

Expand Down
Loading

0 comments on commit e519560

Please sign in to comment.