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/4] 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/4] 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" + ); } } From 7b7266d11d24885f2efbb7c07c0f8cb5ebd1f83f Mon Sep 17 00:00:00 2001 From: "dobby-coder[bot]" <275734547+dobby-coder[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:34:56 +0200 Subject: [PATCH 3/4] docs: document mandatory .zeroize() call for secret types (#49) The zeroize feature (#14) derives Zeroize but not ZeroizeOnDrop, because the secret types are Copy. Secret material is therefore not wiped on drop and callers MUST call .zeroize() explicitly. Document this contract in the crate-level docs and add a MUST-call annotation to every SharedSecret, SecretKey and UserSecretKey across all KEM and IBE schemes. Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 --- src/ibe/boyen_waters.rs | 14 ++++++++++++++ src/ibe/cgw.rs | 14 ++++++++++++++ src/ibe/waters.rs | 14 ++++++++++++++ src/ibe/waters_naccache.rs | 14 ++++++++++++++ src/kem/cgw_fo.rs | 7 +++++++ src/kem/cgw_kv.rs | 14 ++++++++++++++ src/kem/kiltz_vahlis_one.rs | 14 ++++++++++++++ src/kem/mod.rs | 7 +++++++ src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ 9 files changed, 135 insertions(+) diff --git a/src/ibe/boyen_waters.rs b/src/ibe/boyen_waters.rs index 212aa11..4065e77 100644 --- a/src/ibe/boyen_waters.rs +++ b/src/ibe/boyen_waters.rs @@ -49,6 +49,13 @@ pub struct PublicKey { } /// Secret key parameter generated by the PKG used to extract user secret keys. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { @@ -60,6 +67,13 @@ pub struct SecretKey { } /// Points on the paired curves that form the user secret key. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/ibe/cgw.rs b/src/ibe/cgw.rs index 7315089..0a1547c 100644 --- a/src/ibe/cgw.rs +++ b/src/ibe/cgw.rs @@ -48,6 +48,13 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. /// Also known as MSK. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { @@ -59,6 +66,13 @@ pub struct SecretKey { /// User secret key. Can be used to decrypt the corresponding ciphertext. /// Also known as USK_{id}. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/ibe/waters.rs b/src/ibe/waters.rs index c73ba9d..df3d393 100644 --- a/src/ibe/waters.rs +++ b/src/ibe/waters.rs @@ -48,6 +48,13 @@ pub struct PublicKey { } /// Secret key parameter generated by the PKG used to extract user secret keys. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { @@ -55,6 +62,13 @@ pub struct SecretKey { } /// Points on the paired curves that form the user secret key. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/ibe/waters_naccache.rs b/src/ibe/waters_naccache.rs index 16b7325..21eee27 100644 --- a/src/ibe/waters_naccache.rs +++ b/src/ibe/waters_naccache.rs @@ -58,6 +58,13 @@ pub struct PublicKey { pub struct Identity([Scalar; CHUNKS]); /// Secret key parameter generated by the PKG used to extract user secret keys. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { @@ -65,6 +72,13 @@ pub struct SecretKey { } /// Points on the paired curves that form the user secret key. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/kem/cgw_fo.rs b/src/kem/cgw_fo.rs index 3593122..b34f25a 100644 --- a/src/kem/cgw_fo.rs +++ b/src/kem/cgw_fo.rs @@ -29,6 +29,13 @@ 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}. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/kem/cgw_kv.rs b/src/kem/cgw_kv.rs index 998e83e..3a1b880 100644 --- a/src/kem/cgw_kv.rs +++ b/src/kem/cgw_kv.rs @@ -47,6 +47,13 @@ pub struct PublicKey { /// Secret key parameter generated by the PKG used to extract user secret keys. /// Also known as MSK. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { @@ -59,6 +66,13 @@ pub struct SecretKey { /// User secret key. Can be used to decaps the corresponding ciphertext. /// Also known as USK_{id}. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/kem/kiltz_vahlis_one.rs b/src/kem/kiltz_vahlis_one.rs index 0d557d6..d3390ef 100644 --- a/src/kem/kiltz_vahlis_one.rs +++ b/src/kem/kiltz_vahlis_one.rs @@ -44,6 +44,13 @@ pub struct PublicKey { } /// Secret key parameter generated by the PKG used to extract user secret keys. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SecretKey { @@ -51,6 +58,13 @@ pub struct SecretKey { } /// Points on the paired curves that form the user secret key. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct UserSecretKey { diff --git a/src/kem/mod.rs b/src/kem/mod.rs index 9b3018c..c0123a4 100644 --- a/src/kem/mod.rs +++ b/src/kem/mod.rs @@ -34,6 +34,13 @@ 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). +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SharedSecret(pub [u8; SS_BYTES]); diff --git a/src/lib.rs b/src/lib.rs index 1a34908..850dd85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,43 @@ //! //! assert_eq!(k, k2); //! ``` +//! +//! # Zeroizing secret material +//! +//! When the `zeroize` feature is enabled, the secret types in this crate — the +//! [`SharedSecret`](crate::kem::SharedSecret) produced by the KEMs, and the +//! `SecretKey` and `UserSecretKey` of every scheme — derive +//! [`Zeroize`](https://docs.rs/zeroize), but **not** `ZeroizeOnDrop`. +//! +//! These types are `Copy`, and a `Copy` type cannot implement `Drop` (Rust +//! forbids `Copy` and `Drop` on the same type), so `ZeroizeOnDrop` cannot be +//! derived for them. As a consequence **secret key material is not wiped from +//! memory automatically when a value goes out of scope**. If you care about +//! clearing secrets, you **MUST** call `.zeroize()` explicitly once you are +//! done with each secret value: +//! +//! ```ignore +//! use ibe::kem::{IBKEM, cgw_kv::CGWKV}; +//! use ibe::Derive; +//! 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); +//! +//! // ... use sk / usk / ss ... +//! +//! // Wipe the secret material once you are done with it. +//! sk.zeroize(); +//! usk.zeroize(); +//! ss.zeroize(); +//! ``` +//! +//! Making these types `!Copy` so that `ZeroizeOnDrop` can be derived (and the +//! wiping happens automatically) is a breaking API change; it is deferred to a +//! future major release. #![no_std] #![deny(missing_debug_implementations, rust_2018_idioms, missing_docs)] From e960c516f39eca05df246290ec55bb8ac2e51333 Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:13:31 +0000 Subject: [PATCH 4/4] fix: zeroize coverage for boyen_waters SharedSecret (#14) The PKE SharedSecret(Gt) was the one public secret-bearing type left without the zeroize derive or the MUST-call note. Its inner Gt field is private, so callers could neither .zeroize() it nor reach .0 to wipe it manually, leaving the crate's zeroization contract unachievable for this type. Add the same derive + # Zeroization note used on every other secret type, plus a shared_secret_zeroize test. Also correct the decrypt doc comment (returns Msg, not SharedSecret). Co-Authored-By: Claude Opus 4.8 --- src/ibe/boyen_waters.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/ibe/boyen_waters.rs b/src/ibe/boyen_waters.rs index 4065e77..e9542cb 100644 --- a/src/ibe/boyen_waters.rs +++ b/src/ibe/boyen_waters.rs @@ -90,7 +90,15 @@ pub struct CipherText { /// A point on the paired curve that can be encrypted and decrypted. /// /// You can use the byte representation to derive an AES key. +/// +/// # Zeroization +/// +/// With the `zeroize` feature enabled this type derives `Zeroize` but **not** +/// `ZeroizeOnDrop` (it is `Copy`). Secret material is **not** cleared on drop — +/// you **MUST** call `.zeroize()` explicitly once done. See the +/// [crate-level docs](crate#zeroizing-secret-material). #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] pub struct SharedSecret(Gt); /// A message that can be encrypted using the PKE. @@ -215,7 +223,7 @@ impl IBE for BoyenWaters { } } - /// Decrypt ciphertext to a SharedSecret using a user secret key. + /// Decrypt ciphertext to a message using a user secret key. fn decrypt(usk: &UserSecretKey, ct: &CipherText) -> Msg { ct.cprime + multi_miller_loop(&[ @@ -454,4 +462,23 @@ impl Compress for CipherText { #[cfg(test)] mod tests { test_ibe!(BoyenWaters); + + #[cfg(feature = "zeroize")] + #[test] + fn shared_secret_zeroize() { + use group::Group; + use zeroize::Zeroize; + + let mut rng = rand::thread_rng(); + let mut ss = SharedSecret(Gt::random(&mut rng)); + let before = ss; + + ss.zeroize(); + assert_ne!(ss, before, "SharedSecret did not zeroize"); + + // Zeroization is deterministic: two independently-wiped values match. + let mut ss2 = SharedSecret(Gt::random(&mut rng)); + ss2.zeroize(); + assert_eq!(ss, ss2, "zeroized SharedSecrets should be identical"); + } }