Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Changed multi-user key encapsulation, see [this PR](https://github.com/encryption4all/ibe/pull/10).
- Changed `irmaseal-curve 0.1.4` to `pg-curve 0.2.0` (includes `bls12_381 0.8`).
- Derive the `mkem` AES-128-GCM key from the KEM shared secret via HKDF-SHA256 with domain separation instead of raw truncation, see [this PR](https://github.com/encryption4all/ibe/pull/47). This is a wire-incompatible change: pre-fix and post-fix peers can no longer decrypt each other's MKEM ciphertexts (session keys are ephemeral, so there is no stored-ciphertext migration).

## 0.2.3

Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ subtle = { version = "2.6.1", default-features = false }
tiny-keccak = { version = "2.0.2", features = ["sha3", "shake"] }
# aes-gcm >= 0.10.3 fixes RUSTSEC-2023-0096 / CVE-2023-42811.
aes-gcm = { version = "0.10.3", optional = true }
hkdf = { version = "0.12", default-features = false, optional = true }
sha2 = { version = "0.10", default-features = false, optional = true }
zeroize = { version = "1.7", default-features = false, features = ["derive"], optional = true }

[target.wasm32-unknown-unknown.dependencies]
Expand All @@ -43,7 +45,7 @@ cgwkv = []
kv1 = []
waters = []
waters_naccache = []
mkem = ["aes-gcm"]
mkem = ["aes-gcm", "hkdf", "sha2"]
zeroize = ["dep:zeroize", "pg-curve/zeroize"]

[lib]
Expand Down
66 changes: 64 additions & 2 deletions src/kem/mkem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ use subtle::CtOption;

use aes_gcm::aead::{Nonce, Tag};
use aes_gcm::{AeadInPlace, Aes128Gcm, KeyInit};
use hkdf::Hkdf;
use sha2::Sha256;

#[cfg(feature = "cgwfo")]
use crate::kem::cgw_fo::CGWFO;
Expand All @@ -56,6 +58,27 @@ const TAG_SIZE: usize = 16;
const NONCE_SIZE: usize = 12;
const KEY_SIZE: usize = 16;

/// Domain-separation label for the HKDF-SHA256 expansion that turns the KEM
/// shared secret into the AES-128-GCM key.
const HKDF_INFO: &[u8] = b"ibe-mkem-aes128gcm";

/// Derives the AES-128-GCM key bytes from a KEM shared secret using
/// HKDF-SHA256 with domain separation, rather than raw truncation of the
/// shared secret.
fn derive_aead_key(kek: &SharedSecret) -> [u8; KEY_SIZE] {
Comment thread
rubenhensen marked this conversation as resolved.
let mut aes_key = [0u8; KEY_SIZE];
Hkdf::<Sha256>::new(None, &kek.0)
.expand(HKDF_INFO, &mut aes_key)
.expect("KEY_SIZE is a valid HKDF-SHA256 output length");

aes_key
}

/// Builds the AES-128-GCM instance keyed with the HKDF-derived key.
fn derive_aead(kek: &SharedSecret) -> Aes128Gcm {
Aes128Gcm::new_from_slice(&derive_aead_key(kek)).expect("aes_key has the correct length")
}

impl SharedSecret {
/// Sample random shared secret.
fn random<R: Rng + CryptoRng>(r: &mut R) -> Self {
Expand Down Expand Up @@ -96,7 +119,7 @@ where

let (ct_asymm, kek) = <K as IBKEM>::encaps(self.pk, id, self.rng);

let aead = Aes128Gcm::new_from_slice(&kek.0[..KEY_SIZE]).unwrap();
let aead = derive_aead(&kek);
let nonce_bytes = self.rng.gen::<[u8; NONCE_SIZE]>();
let nonce = Nonce::<Aes128Gcm>::from_slice(&nonce_bytes);

Expand Down Expand Up @@ -147,7 +170,7 @@ pub trait MultiRecipient: IBKEM {
ct: &Ciphertext<Self>,
) -> Result<SharedSecret, Error> {
let kek = <Self as IBKEM>::decaps(mpk, usk, &ct.ct_asymm)?;
let aead = Aes128Gcm::new_from_slice(&kek.0[..KEY_SIZE]).unwrap();
let aead = derive_aead(&kek);
let mut shared_key = ct.ct_symm;
aead.decrypt_in_place_detached(&ct.nonce, b"", &mut shared_key, &ct.tag)
.map_err(|_e| Error)?;
Expand Down Expand Up @@ -206,3 +229,42 @@ impl_mkemct_compress!(CGWFO);

#[cfg(feature = "kv1")]
impl_mkemct_compress!(KV1);

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn derive_aead_key_is_deterministic() {
let kek = SharedSecret([7u8; SS_BYTES]);
assert_eq!(derive_aead_key(&kek), derive_aead_key(&kek));
}

#[test]
fn derive_aead_key_differs_from_raw_truncation() {
// The vulnerability being fixed: the key used to be `&kek.0[..KEY_SIZE]`.
// HKDF-SHA256 must mix the whole secret, so the derived key should not
// equal the raw prefix of the shared secret.
let kek = SharedSecret([0xABu8; SS_BYTES]);
assert_ne!(&derive_aead_key(&kek)[..], &kek.0[..KEY_SIZE]);
}

#[test]
fn derive_aead_key_depends_on_full_secret() {
// Two secrets sharing the same first KEY_SIZE bytes but differing in the
// tail would collide under raw truncation; HKDF must keep them distinct.
let mut a = [0u8; SS_BYTES];
let mut b = [0u8; SS_BYTES];
for (i, (x, y)) in a.iter_mut().zip(b.iter_mut()).enumerate() {
*x = i as u8;
*y = i as u8;
}
b[SS_BYTES - 1] ^= 0xFF;

assert_eq!(&a[..KEY_SIZE], &b[..KEY_SIZE]);
assert_ne!(
derive_aead_key(&SharedSecret(a)),
derive_aead_key(&SharedSecret(b))
);
}
}
Loading