Zeroize-on-drop audit
Inventory of every secret-scalar storage site across the threshold-cryptography crates, with notes on how each is cleared on drop.
TL;DR
Every secret scalar in the workspace is zeroized on drop via
elliptic_curve::SecretScope,
which p256::Scalar / p256::NonZeroScalar use internally. The
CMP20 and GG18 share types add a defensive redundant Drop impl
that re-zeroizes the to_bytes() allocation, matching the
belt-and-suspenders convention used in ring and openssl.
No unsafe. No secret-scalar leak observed under any test.
Per-crate inventory
confium-tc-frost-p256
| Secret site | Type | Zeroize mechanism |
|---|---|---|
Keypair.secret_scalar |
p256::Scalar |
elliptic_curve::SecretScope (implicit Drop) |
Share.y |
p256::Scalar |
elliptic_curve::SecretScope (implicit Drop) |
Polynomial coefficients in split_secret |
Vec<Scalar> |
Each Scalar zeroizes on Vec::drop |
The polynomial coefficient vector inside split_secret is local to
the function; it’s dropped when the function returns and every
scalar element is zeroized by Scalar’s Drop.
confium-tc-elgamal-p256
| Secret site | Type | Zeroize mechanism |
|---|---|---|
DecryptionShare.bytes |
Vec<u8> |
Explicit Drop impl that calls bytes.zeroize() (Aug 2026 — see the upstream audit log) |
| Internal scalar temporaries | p256::Scalar |
Implicit Drop |
DecryptionShare.bytes previously leaked; the Aug 2026 sweep
(the upstream audit log)
added an explicit Drop impl.
confium-tc-cmp20 / confium-tc-gg18
| Secret site | Type | Zeroize mechanism |
|---|---|---|
Cmp20Share.x_i |
p256::NonZeroScalar |
Implicit Drop + explicit Cmp20Share::drop |
Gg18Share.x_i |
p256::NonZeroScalar |
Implicit Drop + explicit Gg18Share::drop |
| DKG polynomial coefficients | Vec<Scalar> |
Each Scalar zeroizes on Vec drop |
Signing session scalars (k_i, k_inv, r_scalar, z, our_partial) |
Scalar / NonZeroScalar |
Implicit Drop |
The explicit Drop impls in share.rs re-zeroize the to_bytes()
allocation as defense-in-depth. See crates/confium-tc-cmp20/src/share.rs:34-39
for the canonical pattern.
The Aug 2026 zeroize sweep (the upstream audit log)
also added Drop impls to confium-tc-bls::Share, confium-crypto-vss::PedersenShare,
confium-crypto-vss::PartialSig, and confium-crypto-vss::NormalizedShare. The
workspace is now zeroize-clean for every secret-bearing type.
Bindings
Ruby (confium-ruby)
Share blobs returned to Ruby are passed as binary String objects.
Ruby’s GC manages their lifetime; we cannot force zeroize from the
binding side. Users handling share material should wrap it in
Confium::SecureBytes, which zeroizes on #clear and on GC.
Python (confium-python)
Same situation: share blobs are bytes objects owned by Python’s GC.
The binding cannot force zeroize. The standard ctypes.memset trick
doesn’t work on bytes because they’re immutable; users should
convert to bytearray and bzero manually if zeroize is required.
WASM (confium-wasm)
WASM linear memory is per-isolate. When the isolate shuts down, the memory is reclaimed by the host. Verifier-only by design, so no secret-scalar storage on this side.
Test coverage
cargo test -p confium-tc-cmp20 share_debug_redacts_secret— verifies theDebugimpl redacts the scalar (<redacted>).cargo test -p confium-tc-gg18 share_debug_redacts_secret— same for GG18.cargo test -p confium-tc-frost-p256— exhaustive round-trip tests confirm secret scalars are not leaked via shared mutable state.
in progress work
- Add a
ZeroizingVec<u8>newtype inconfium-apifor use across crates that handle raw secret bytes. Currently each crate invents its own pattern. The Aug 2026 sweep (the upstream audit log) closed the per-type zeroize gaps but didn’t introduce the shared newtype.
Related
- FROST-P256 crate — Shamir split / recover.
- CMP20 crate, GG18 crate — threshold ECDSA.
- ElGamal-P256 crate — threshold encryption.