Skip to content

Feature request: implement Zeroize for MLDSASigningKey (and related key types) #1370

Description

@dhgatjeye

I'm building a peer-to-peer encrypted file transfer tool over LAN(for now). It uses a custom
handshake protocol (X25519 + ML-KEM-1024 hybrid key exchange, ML-DSA-87 for peer
identity signatures) no X.509, no PKIX, no certificate authorities. I'm using
libcrux-ml-dsa as the signing implementation.

What I ran into

My signing code looks roughly like this:

pub fn sign(&self, message: &[u8]) -> Result<[u8; SIGNATURE_LEN]> {
    // self.signing_key is a Box<[u8; 4896]> that I zeroize on Drop.
    let typed_sk = libcrux_ml_dsa::MLDSASigningKey::new(*self.signing_key); // <- 4896-byte stack copy

    let mut randomness = Zeroizing::new([0u8; 32]);
    crate::crypto::csprng().fill_bytes(&mut *randomness);

    let sig = ml_dsa_87::sign(&typed_sk, message, ctx, *randomness)?;
    // typed_sk drops here but without a Zeroize impl
    // sit on the stack until the next frame overwrites them.
    Ok(*sig.as_ref())
}

I zeroize my own Box<[u8; 4896]> on drop via the zeroize crate, so the
long-lived copy is taken care of. But MLDSASigningKey doesn't implement Zeroize,
which means the short-lived stack copy that gets created for the duration of the
sign() call isn't explicitly cleared when it drops. It'll eventually be overwritten
by normal stack reuse, but there's no guarantee on when.

Why I care

RFC 9881 §9 specifically calls out side-channel risk in ML-DSA signing:

"Some amount of side-channel leakage has been demonstrated in parts of the signing
algorithm (specifically the bit-unpacking function), from which a demonstration of
key recovery has been made over a large sample of signatures."

It also recommends randomized signing to harden against fault and hardware side-channel
attacks and I'm already doing that (hedged signing with a 256-bit RBG string). But the
complementary defense and then not leaving key material in memory longer than necessary is
something I can't currently achieve without Zeroize support on MLDSASigningKey.

In practice the window is tiny: it's the time between drop(typed_sk) and the stack
frame getting reused. I'm not claiming this is an imminent catastrophe. But in a
security-sensitive context, "tiny window" isn't the same as "no window", and I'd like
to be able to document that I've done everything possible on my side.

What would help me

Would it be feasible to add Zeroize (and ideally ZeroizeOnDrop) for
MLDSASigningKey and the equivalent types for ML-DSA-44 and ML-DSA-65 while
we're at it?

Something like:

impl zeroize::Zeroize for MLDSASigningKey {
    fn zeroize(&mut self) {
        self.0.zeroize(); // assuming the inner type is a plain byte array
    }
}

impl zeroize::ZeroizeOnDrop for MLDSASigningKey {}

I just don't want to touch the internals blindly without knowing whether there are constraints I'm
not aware of (inner representation, etc.).

A few questions

  • Is this something that's already on your radar, or intentionally left out for a
    reason I'm missing?
  • Does the formally verified internals story complicate adding a Zeroize impl, or
    is it straightforward at the boundary?
  • Is there anything I should know for this context?

Environment

  • libcrux-ml-dsa: 0.0.8 (feature mldsa87)
  • libcrux-secrets: 0.0.5
  • Rust: 1.85.1

Thanks for your time and again.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions