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 the Debug impl 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 in confium-api for 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.