From 9d37c085eae2466bdb1587d0a0a7dbd99b49dad1 Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:21:31 +0000 Subject: [PATCH 1/2] feat: optional zeroize feature for secret types (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `zeroize` cargo feature that derives `zeroize::Zeroize` on the secret-bearing structs across every scheme: - `kem::cgw_kv::SecretKey` / `UserSecretKey` - `kem::cgw_fo::UserSecretKey` - `kem::kiltz_vahlis_one::SecretKey` / `UserSecretKey` - `ibe::cgw::SecretKey` / `UserSecretKey` - `ibe::waters::SecretKey` / `UserSecretKey` - `ibe::waters_naccache::SecretKey` / `UserSecretKey` - `ibe::boyen_waters::SecretKey` / `UserSecretKey` - `kem::SharedSecret` - `util::Identity` (used as `Id` in cgw_fo) The feature chains through to `pg-curve/zeroize` so the leaf `Scalar`, `G1Affine`, `G2Affine` and `Gt` types get their zeroize impls from the upstream crate. As noted in issue #7, these types are all `Copy` so `ZeroizeOnDrop` is not possible; users must call `.zeroize()` explicitly. The feature gives them the machinery to do so. Added a round-trip test under `cgwkv` (the scheme PostGuard uses): derives an MSK/USK/SharedSecret, zeroizes each, asserts the bytes change and that the shared-secret bytes are fully zero. Does NOT add `Zeroize` as a bound on the `IBKEM` / `IBE` traits themselves — that's a larger API decision and would retroactively require every external impl to opt in. Deferring. --- Cargo.toml | 2 ++ src/ibe/boyen_waters.rs | 2 ++ src/ibe/cgw.rs | 2 ++ src/ibe/waters.rs | 2 ++ src/ibe/waters_naccache.rs | 2 ++ src/kem/cgw_fo.rs | 1 + src/kem/cgw_kv.rs | 27 +++++++++++++++++++++++++++ src/kem/kiltz_vahlis_one.rs | 2 ++ src/kem/mod.rs | 1 + src/util.rs | 1 + 10 files changed, 42 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f8520f9..16a210a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ pg-curve = { version = "0.2.0", features = [ subtle = { version = "2.4.1", default-features = false } tiny-keccak = { version = "2.0.2", features = ["sha3", "shake"] } aes-gcm = { version = "0.10", optional = true } +zeroize = { version = "1.7", default-features = false, features = ["derive"], optional = true } [target.wasm32-unknown-unknown.dependencies] getrandom = { version = "0.2", features = ["js"] } @@ -42,6 +43,7 @@ kv1 = [] waters = [] waters_naccache = [] mkem = ["aes-gcm"] +zeroize = ["dep:zeroize", "pg-curve/zeroize"] [lib] bench = false diff --git a/src/ibe/boyen_waters.rs b/src/ibe/boyen_waters.rs index 74ed157..212aa11 100644 --- a/src/ibe/boyen_waters.rs +++ b/src/ibe/boyen_waters.rs @@ -50,6 +50,7 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { alpha: Scalar, t1: Scalar, @@ -60,6 +61,7 @@ pub struct SecretKey { /// Points on the paired curves that form the user secret key. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { d: [G2Affine; 5], } diff --git a/src/ibe/cgw.rs b/src/ibe/cgw.rs index 3ed899e..7315089 100644 --- a/src/ibe/cgw.rs +++ b/src/ibe/cgw.rs @@ -49,6 +49,7 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. /// Also known as MSK. #[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { b: [Scalar; 2], k: [Scalar; 2], @@ -59,6 +60,7 @@ pub struct SecretKey { /// User secret key. Can be used to decrypt the corresponding ciphertext. /// Also known as USK_{id}. #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { d0: [G2Affine; 2], d1: [G2Affine; 2], diff --git a/src/ibe/waters.rs b/src/ibe/waters.rs index 83cf1c8..c73ba9d 100644 --- a/src/ibe/waters.rs +++ b/src/ibe/waters.rs @@ -49,12 +49,14 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { g1prime: G1Affine, } /// Points on the paired curves that form the user secret key. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { d1: G1Affine, d2: G2Affine, diff --git a/src/ibe/waters_naccache.rs b/src/ibe/waters_naccache.rs index 4bd6fa6..16b7325 100644 --- a/src/ibe/waters_naccache.rs +++ b/src/ibe/waters_naccache.rs @@ -59,12 +59,14 @@ pub struct Identity([Scalar; CHUNKS]); /// Secret key parameter generated by the PKG used to extract user secret keys. #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { g2prime: G2Affine, } /// Points on the paired curves that form the user secret key. #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { d1: G2Affine, d2: G1Affine, diff --git a/src/kem/cgw_fo.rs b/src/kem/cgw_fo.rs index 6876ce6..3593122 100644 --- a/src/kem/cgw_fo.rs +++ b/src/kem/cgw_fo.rs @@ -30,6 +30,7 @@ pub const USK_BYTES: usize = CPA_USK_BYTES + ID_BYTES; /// User secret key. Can be used to decaps the corresponding ciphertext. /// Also known as USK_{id}. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { usk: crate::ibe::cgw::UserSecretKey, id: Identity, diff --git a/src/kem/cgw_kv.rs b/src/kem/cgw_kv.rs index 87a75dc..992cb12 100644 --- a/src/kem/cgw_kv.rs +++ b/src/kem/cgw_kv.rs @@ -48,6 +48,7 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. /// Also known as MSK. #[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { b: [Scalar; 2], k: [Scalar; 2], @@ -59,6 +60,7 @@ pub struct SecretKey { /// User secret key. Can be used to decaps the corresponding ciphertext. /// Also known as USK_{id}. #[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { d0: [G2Affine; 2], d1: [G2Affine; 2], @@ -516,4 +518,29 @@ mod tests { #[cfg(feature = "mkem")] test_multi_kem!(CGWKV); + + #[cfg(feature = "zeroize")] + #[test] + fn secret_types_zeroize() { + use zeroize::Zeroize; + let mut rng = rand::thread_rng(); + let id = ::Id::derive_str("alice@example.com"); + let (pk, mut sk) = CGWKV::setup(&mut rng); + let mut usk = CGWKV::extract_usk(Some(&pk), &sk, &id, &mut rng); + let (_ct, mut ss) = CGWKV::encaps(&pk, &id, &mut rng); + + // Snapshot serialized form so we can assert zeroization actually blanks it. + let sk_bytes_before = sk.to_bytes(); + let usk_bytes_before = usk.to_bytes(); + let ss_bytes_before = ss.0; + + sk.zeroize(); + usk.zeroize(); + ss.zeroize(); + + assert_ne!(sk.to_bytes(), sk_bytes_before, "SecretKey did not zeroize"); + assert_ne!(usk.to_bytes(), usk_bytes_before, "UserSecretKey did not zeroize"); + assert_ne!(ss.0, ss_bytes_before, "SharedSecret did not zeroize"); + assert_eq!(ss.0, [0u8; crate::kem::SS_BYTES], "SharedSecret bytes should be zero"); + } } diff --git a/src/kem/kiltz_vahlis_one.rs b/src/kem/kiltz_vahlis_one.rs index 5e1af1c..0d557d6 100644 --- a/src/kem/kiltz_vahlis_one.rs +++ b/src/kem/kiltz_vahlis_one.rs @@ -45,12 +45,14 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { alpha: G1Affine, } /// Points on the paired curves that form the user secret key. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { d1: G1Affine, d2: G2Affine, diff --git a/src/kem/mod.rs b/src/kem/mod.rs index 472c12a..9b3018c 100644 --- a/src/kem/mod.rs +++ b/src/kem/mod.rs @@ -35,6 +35,7 @@ pub const SS_BYTES: usize = 32; /// This shared secret has roughly a 127 bits of security. /// This is due to the fact that BLS12-381 targets this security level (optimistically). #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SharedSecret(pub [u8; SS_BYTES]); /// Uses SHAKE256 to derive a 32-byte shared secret from a target group element. diff --git a/src/util.rs b/src/util.rs index 0aae16f..8b7c7ee 100644 --- a/src/util.rs +++ b/src/util.rs @@ -99,6 +99,7 @@ pub fn rpc(k: &[u8; 32], gs: &[Gr]) -> Scalar { /// /// This identity is obtained by hashing using sha3_512. #[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct Identity(pub [u8; ID_BYTES]); impl Default for Identity { From b5e762588d34bf0771a49e60ed24a3e576fa26f0 Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:03:24 +0000 Subject: [PATCH 2/2] style: cargo fmt --- src/kem/cgw_kv.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/kem/cgw_kv.rs b/src/kem/cgw_kv.rs index 992cb12..998e83e 100644 --- a/src/kem/cgw_kv.rs +++ b/src/kem/cgw_kv.rs @@ -539,8 +539,16 @@ mod tests { ss.zeroize(); assert_ne!(sk.to_bytes(), sk_bytes_before, "SecretKey did not zeroize"); - assert_ne!(usk.to_bytes(), usk_bytes_before, "UserSecretKey did not zeroize"); + assert_ne!( + usk.to_bytes(), + usk_bytes_before, + "UserSecretKey did not zeroize" + ); assert_ne!(ss.0, ss_bytes_before, "SharedSecret did not zeroize"); - assert_eq!(ss.0, [0u8; crate::kem::SS_BYTES], "SharedSecret bytes should be zero"); + assert_eq!( + ss.0, + [0u8; crate::kem::SS_BYTES], + "SharedSecret bytes should be zero" + ); } }