Skip to content

Zeroize output buffers at the start of all _out functions (part of #16)#18

Merged
hubot merged 2 commits into
bcgit:release/0.1.2alphafrom
Quant-TheodoreFelix:qtfelix/0.1.2alpha-zeroize-out-buffers
Jun 10, 2026
Merged

Zeroize output buffers at the start of all _out functions (part of #16)#18
hubot merged 2 commits into
bcgit:release/0.1.2alphafrom
Quant-TheodoreFelix:qtfelix/0.1.2alpha-zeroize-out-buffers

Conversation

@Quant-TheodoreFelix

@Quant-TheodoreFelix Quant-TheodoreFelix commented Jun 9, 2026

Copy link
Copy Markdown

This zeroizes the caller-provided output buffer at the entry of every concrete function that writes into an output byte slice (the *_out family, the dispatch wrappers in the factory crate, and the constant-time copy helper in utils). Each such function now starts with out.fill(0) (or output.fill(0)) before it produces its result. 84 write sites in total.

Motivation (from the 0.1.2alpha TODO)

The TODO states: anywhere you have an _out(.. out: &mut [u8]), start by zeroizing it with .fill(0). Pre-zeroizing closes two leak paths:

  • An oversized output buffer keeps stale bytes past the digest/MAC length.
  • An early error return leaves partially written or stale data in the caller's buffer.

A representative change:

fn do_final_out(mut self, output: &mut [u8]) -> usize {
    output.fill(0);
    ...
}

Scope

sha2, sha3 (sha3 / shake / keccak), hmac, rng (hash_drbg), hex, base64, factory (hash / xof / mac / rng), utils/ct, and the PQ crates mlkem, mlkem_lowmemory, mldsa, mldsa_lowmemory (key-encode and sign paths).

Behavior change (please note)

Digest, MAC, XOF and RNG output functions now zero the tail of an oversized output buffer instead of leaving it untouched. One existing sha3 unit test (test_static_hash) asserted the old "does not touch the extra bytes" contract and was updated to assert the new zeroed-tail contract. The shared core-test-framework hash and mac tests already use zero-initialized buffers and expect zeroed tails, so they were unaffected.

Deliberately excluded

The row-wise partial writers in mlkem_lowmemory (pack_t_hat_row, pack_s_hat_row, compress_u_row) were NOT changed. They write a sub-slice of a shared buffer that already holds other rows, so a blanket fill(0) would erase previously written data and corrupt the output. Trait declarations (no body), unimplemented!() stubs, and KeyMaterialTrait-typed outputs (zeroized transitively through the byte-slice writers) were left as-is.

Merge conflict warning

This PR modifies crypto/rng/src/hash_drbg80090a.rs (generate_out, next_bytes_out, hash_df, hashgen). PR #16 also touches that file, so expect a conflict when folding this into the roundup branch.

Verification

cargo build            # clean across the workspace
cargo test --workspace # 326 passed, 0 failed

The test run includes the ML-KEM / ML-DSA wycheproof KATs, which exercise the modified key-encode and sign paths.

Relationship

Part of #16. Intended to be folded into the 0.1.2alpha TODO roundup.

- 출력 슬라이스를 받는 _out 계열 함수 진입부에서 out fill(0) 선-초기화
- 오버사이즈 버퍼 뒤쪽이나 조기 반환 시 남는 이전 데이터 노출 방지
- digest 출력 버퍼 뒤쪽까지 0이 되도록 sha3 테스트 1건 갱신
- 행 단위 부분 기록 함수는 다른 행을 지우므로 제외

@ounsworth ounsworth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is great, thanks!

I have left a few requests for changes.

Can you please also update the docstring in core/traits.rs for each *_out function to indicate that the entire output buffer will be zeroized.

This PR is marked Draft. Please let me know when you think it is ready to be merged.

If you have done a task from the todo list, can you please delete that line and move it to the changelog section at the bottom of that file?

Comment thread crypto/mlkem/src/matrix.rs Outdated
// let mut s = self.clone();
// s.conditional_sub_q();

out.fill(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This one should be removed -- this is not a public function, and this will have a performance impact on the mlkem algorithm.

Comment thread crypto/mlkem/src/polynomial.rs Outdated
// each of the N i16's will take dv bits
debug_assert_eq!(out.len(), N * (dv as usize) / 8);

out.fill(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This one should be removed -- this is not a public function, and this will have a performance impact on the mlkem algorithm.

// each of the N i16's will take dv bits
debug_assert_eq!(out.len(), N * (dv as usize) / 8);

out.fill(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This one should be removed -- this is not a public function, and this will have a performance impact on the mlkem algorithm.

Comment thread crypto/sha3/tests/sha3_tests.rs Outdated
// Q. T. Felix NOTE: With the application of zeroize, the entire output buffer is pre-initialized to 0,
// so the bytes after the digest length are now also 0.
// Previously, the contract was to leave the trailing bytes untouched,
// but this has been changed to fill them with zeros to prevent exposure of stale data.

@ounsworth ounsworth Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A comment to explain what is being tested by this line is good, but can you please remove your name, and can you please remove the description of what changed from one version to the next? (the change history will very quickly become meaningless)

Comment thread crypto/utils/src/ct.rs Outdated
out: &mut [u8; LEN],
take_a: bool,
) {
out.fill(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs to be removed as it may violate the constant-time assumption since over-writing a zero with a zero may take less time than overwriting a different value.

@Quant-TheodoreFelix

Copy link
Copy Markdown
Author

Yes, confirmed. Im working on it.

- 비공개 mlkem compress 함수 fill(0) 제거 (성능)
- ct conditional_copy_bytes fill(0) 제거 (constant-time 가정 위반 가능)
- core traits의 _out 함수 docstring에 출력 버퍼 zeroize 명시
- sha3 테스트 주석에서 이름과 변경 이력 제거
- 릴리즈 노트의 완료된 TODO 항목을 changelog로 이동
@Quant-TheodoreFelix Quant-TheodoreFelix marked this pull request as ready for review June 10, 2026 04:43
@Quant-TheodoreFelix

Copy link
Copy Markdown
Author

Thanks for the review! All requested changes are in (commit c642ef6):

  • Removed the fill(0) from the three internal mlkem compress functions (perf) and from ct::conditional_copy_bytes (constant-time concern).
  • Cleaned up the sha3 test comment (no name, no change history).
  • Added "the entire output buffer is zeroized" to the docstrings of the *_out functions in core/traits.rs.
    One deliberate exception: derive_key_out, derive_key_from_multiple_out and fill_keymaterial_out take a KeyMaterialTrait output rather than a byte slice, and HKDF only writes the first L bytes of the key's internal buffer, so a blanket zeroize claim there would be inaccurate. Happy to extend it if you prefer.
  • Moved the TODO line to the changelog section of the release notes.

cargo test --workspace passes (326 tests). Marked ready for review.

@ounsworth

Copy link
Copy Markdown
Contributor

Amazing! Thank you so much! I will merge now.

@hubot hubot merged commit b3a22ae into bcgit:release/0.1.2alpha Jun 10, 2026
hubot pushed a commit that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants