Skip to content
This repository was archived by the owner on Aug 7, 2026. It is now read-only.

fix: zeroize Falcon secret polynomial temporaries and encoded key buffers - #1061

Open
Jr-kenny wants to merge 4 commits into
0xMiden:nextfrom
Jr-kenny:zeroize-falcon-polynomial-temporaries
Open

fix: zeroize Falcon secret polynomial temporaries and encoded key buffers#1061
Jr-kenny wants to merge 4 commits into
0xMiden:nextfrom
Jr-kenny:zeroize-falcon-polynomial-temporaries

Conversation

@Jr-kenny

Copy link
Copy Markdown
Contributor

Follow-up to #1057, closes 0xMiden/miden-vm#3533.

#1057 wiped the byte-level read buffers, but the secret material doesn't stop there. On the Falcon path the decoded coefficients get turned into Polynomial temporaries that nothing wipes, Polynomial carries a ZeroizeOnDrop marker with no Drop behind it so it never actually runs, and both write_into and generate_seed leave encoded copies of the secret key on the heap, the second one on every signature. 0xMiden/miden-vm#3533 has the full walkthrough.

What's in here:

  • Zeroize for FalconFelt, so Polynomial<FalconFelt> becomes wipeable through the existing generic impl.
  • Dropped the unbacked ZeroizeOnDrop marker on Polynomial. It's unimplementable as declared: a Drop bounded on F: Zeroize fails E0367, and bounding the struct itself breaks Polynomial<Complex64> on the signing path. Made wipe-on-drop explicit at the use sites with Zeroizing instead, the same pattern feat: add zeroizing read helper for sensitive deserialization #1057 uses for the byte buffers.
  • read_from: wrapped f/g/big_f/big_g and bound the fft chain stepwise so the intermediates get wiped too, inlining hadamard_div as hadamard_inv + hadamard_mul so the inverse lands in a wrapped value instead of an internal allocation. Negated the basis polynomials through references so the un-negated copies stay wrapped.
  • write_into: wiped the encode buffer and the encoded chunks, and fixed the stale comment that claimed write_bytes consumes the buffer (it borrows it).
  • generate_seed: bound the serialized key in Zeroizing so the per-signature copies are covered.
  • Swept the rest of the secret path: compute_pub_key_poly clones f/g on every signature and ntru_gen drops rejected candidates on every retry, both wiped now.

The BigInt arithmetic in ntru_solve and the Complex64 fft domain are left as documented residue. Wiping those needs the volatile-write treatment LdlTree already does rather than Zeroize, and that's called out in 0xMiden/miden-vm#3533.

This sits on top of #1057, so it should land after that one. The first commits here are #1057's.

Test plan: cargo test -p miden-crypto dsa (49 pass, including the reference-impl signature determinism check), cargo fmt --check, cargo clippy -p miden-crypto --all-features, and the no-std build.

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown

Automated check (CONTRIBUTING.md)

Findings:

  • Add a short Rationale explaining why the change is needed.

Next steps:

buffer.push(LOG_N);
buffer.extend_from_slice(&self.to_bytes());
// Bind the serialized key so the temporary holding it is wiped, not just `buffer`.
let sk_bytes = Zeroizing::new(self.to_bytes());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handles the generate_seed copy, but the same full-key serialization still happens in PartialEq just below: self.to_bytes().ct_eq(&other.to_bytes()).

Both calls allocate encoded secret keys and drop them without Zeroizing, so comparing two Falcon secret keys can still leave the same kind of buffer this PR is trying to wipe. Could those two serialized values be bound with Zeroizing too, or compared without serializing?

@Jr-kenny Jr-kenny Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that path slips the wipe too. PartialEq serializes both keys and drops them in the clear on every compare. Bound both to_bytes() in Zeroizing in 53a0f03 so they get wiped after the compare instead. ct_eq still runs over the bound bytes, so the comparison is unchanged and stays constant time.

Swept the rest while I was in there: this was the last unwiped to_bytes() path in Falcon, write_into and generate_seed were already covered. The same pattern showed up in the sibling secret keys though. The k256 ECDSA and ed25519 EdDSA SecretKeys are both ZeroizeOnDrop and their PartialEq had the identical un-wiped to_bytes().ct_eq(), so I folded the same fix into those two as well. Falcon, ecdsa and eddsa suites all pass locally.

@Jr-kenny
Jr-kenny requested a review from huitseeker June 20, 2026 01:53

@huitseeker huitseeker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, a rebase would be great!

@Jr-kenny
Jr-kenny force-pushed the zeroize-falcon-polynomial-temporaries branch from 53a0f03 to fc12005 Compare June 23, 2026 14:55
@Jr-kenny

Jr-kenny commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

just did ser

Comment thread miden-crypto/src/dsa/falcon512_poseidon2/keys/secret_key.rs
Comment thread miden-crypto/src/dsa/falcon512_poseidon2/math/mod.rs
@huitseeker
huitseeker force-pushed the zeroize-falcon-polynomial-temporaries branch from fc12005 to 733ff85 Compare July 10, 2026 21:07
Jr-kenny and others added 4 commits August 2, 2026 07:31
FalconFelt gets a Zeroize impl so Polynomial<FalconFelt> is wipeable through
the existing generic impl. The ZeroizeOnDrop marker on Polynomial is removed:
it had no Drop behind it (a conditional Drop impl is rejected by E0367 and
bounding the struct would exclude Polynomial<Complex64>), so it promised a
wipe that never ran. Wipe-on-drop is now explicit at the call sites.

SecretKey::read_from wraps the decoded polynomials and every FFT-domain
intermediate of the big_g computation in Zeroizing, inlining hadamard_div as
hadamard_inv + hadamard_mul so the inverse is reachable, and negates the
basis polynomials through references so the un-negated copies are wiped.

SecretKey::write_into wipes its encode buffer and the encoded chunks, fixing
the stale note that claimed write_bytes consumes the buffer. generate_seed
binds the serialized key in Zeroizing so the per-signature copy is wiped.
compute_pub_key_poly cloned f and g into FalconFelt polynomials on every
signature and dropped them intact; they and their FFT forms are now bound
in Zeroizing, with hadamard_div inlined so the inverse is wiped too. The
h = g/f result is the public key and stays unwrapped.

ntru_gen wraps the sampled f and g and the converted F and G so rejected
candidates are wiped on every retry, moves the kept polynomials out of
their wrappers, and negates through references so the un-negated copies
are wiped. gen_poly wipes its raw 4096-sample buffer.

Out of reach without new machinery and left as is: the BigInt arithmetic
inside ntru_solve, and the Complex64 FFT domain (gram_schmidt_norm_squared,
to_complex_fft, sign_helper, ffsampling), which would need the volatile
treatment LdlTree uses.
@Jr-kenny
Jr-kenny force-pushed the zeroize-falcon-polynomial-temporaries branch from 733ff85 to 6a39e31 Compare August 2, 2026 07:31

@Al-Kindi-0 Al-Kindi-0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@bobbinth

bobbinth commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This should be ported to the miden-vm repo now, right?

@Jr-kenny

Jr-kenny commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, this needs to live in miden-vm now. I ported the fix onto the current next code instead of carrying the old files over, reran the crypto checks, and opened 0xMiden/miden-vm#3456.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

falcon512_poseidon2: Polynomial zeroization is an unbacked promise, secret polynomial temporaries and encoded key buffers are never wiped

4 participants