Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support X25519Kyber768Draft00 hybrid post-quantum KEM #43

Merged
merged 16 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 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", "dep:serde-big-array", "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,8 @@ 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"] }
serde-big-array = { version = "=0.5.1", optional = true }
subtle = { version = "2.4", default-features = false }
zeroize = { version = "1", default-features = false, features = ["zeroize_derive"] }

Expand All @@ -49,6 +52,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"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for future: we should probably not rely on this code or forks of it. I agree with your commits @bwesterb but I think that library needs quite a bit of work to get to a good state.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some good people working on a better Rust implementation of Kyber. Let's promptly switch when it's out.

I did very carefully check every bit of the code in that fork, and I feel confident in deploying it. (Not so much upstream though.)

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
);
}
}
}
7 changes: 7 additions & 0 deletions src/kem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ use rand_core::{CryptoRng, RngCore};
use zeroize::Zeroize;

mod dhkem;
// The allow here is because every KEM exports a doc(hidden) type called EncappedKey. The user
// never sees it, but the compiler thinks it's ambiguous.
pub use dhkem::*;

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

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

Expand Down
Loading
Loading