Skip to content

pkcs12: zeroize KDF output and intermediates#2283

Open
MarkAtwood wants to merge 1 commit intoRustCrypto:masterfrom
MarkAtwood:pkcs12-kdf-zeroize
Open

pkcs12: zeroize KDF output and intermediates#2283
MarkAtwood wants to merge 1 commit intoRustCrypto:masterfrom
MarkAtwood:pkcs12-kdf-zeroize

Conversation

@MarkAtwood
Copy link
Copy Markdown
Contributor

Summary

  • Return Zeroizing<Vec<u8>> from derive_key, derive_key_bmp, and derive_key_utf8 so all derived key material is erased on drop.
  • Three buffers now zeroized: the output key (out), each per-round intermediate hash (result), and the stretched S||P password buffer (init_key). init_key uses Zeroizing for panic-safe erasure on unwind in addition to an explicit .zeroize() on the normal path.
  • Enable digest/zeroize (zeroes the digest block buffer holding input fragments of id||salt||password) and add hybrid-array as a direct optional dep with features = ["zeroize"]. The direct dep is required because digest/zeroize does not propagate hybrid-array/zeroize, leaving Array<u8, OutputSize>: Zeroize unsatisfied. If digest ever adds hybrid-array/zeroize to its own zeroize feature, the direct dep can be removed. Both are gated behind the kdf feature.
  • New tests verified against openssl kdf PKCS12KDF: empty UTF-8 password (BMP null-terminates to [0x00, 0x00]) and a 72-byte salt spanning two SHA-256 blocks.

Breaking change

derive_key, derive_key_bmp, and derive_key_utf8 now return Zeroizing<Vec<u8>> instead of Vec<u8>. This is intentional and is the point of the PR. Because Zeroizing<Vec<u8>> implements Deref<Target = Vec<u8>>, most callers that use &result[..] or iterate are unaffected. Callers that stored the result as Vec<u8> or compared directly with [u8; N] will need to call .as_slice() or deref-coerce. The crate is 0.2.0-pre.0 so this break is within the pre-release versioning policy.

Dependency notes

  • cms = "=0.3.0-pre.2" pin is intentional coordinated monorepo pre-release.
  • The hybrid-array dep is version = "0.4", matching the version already pulled in transitively by digest.

Pre-existing issue (out of scope)

rounds: i32 — passing 0 or a negative value silently behaves as rounds = 1 (one hash iteration). This is pre-existing behavior and is not introduced or changed by this PR.

Test plan

  • cargo test --features kdf -p pkcs12 — 6 KDF tests pass, including new empty-password and long-salt cases
  • cargo clippy --all-features -p pkcs12 -- -D warnings — clean
  • cargo hack check --feature-powerset --no-dev-deps -p pkcs12 — all 7 feature combinations compile
  • RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc --no-deps --all-features -p pkcs12 — clean
  • cargo +1.85 test --features kdf -p pkcs12 — passes at MSRV

Return `Zeroizing<Vec<u8>>` from `derive_key`, `derive_key_bmp`, and
`derive_key_utf8` so all key material is erased from memory on drop.
The returned key being wrapped in `Zeroizing` is documented on each
public function.

Three buffers are now zeroized:
- `out` (the derived key) — wrapped in `Zeroizing`, erased when the
  caller drops the return value.
- `result` (per-round intermediate hash) — wrapped in `Zeroizing`;
  each reassignment in the inner loop drops and erases the previous
  value before the next hash is computed.
- `init_key` (the S||P buffer holding stretched password bytes) —
  wrapped in `Zeroizing` for panic-safe erasure on unwind, plus an
  explicit `.zeroize()` call on the normal return path for eager
  erasure.

Enable `digest/zeroize` (zeroes the digest's internal block buffer,
which holds input fragments of id||salt||password between Update
calls) and add `hybrid-array` as a direct optional dep with
`features = ["zeroize"]`.  The latter is required because
`digest/zeroize` does not propagate `hybrid-array/zeroize`, leaving
`Array<u8, OutputSize>: Zeroize` unsatisfied without the direct dep.
Both are gated behind the existing `kdf` feature.

`(*result)[..]` explicit dereferences are necessary because
`Zeroizing<T>` implements `Deref` but not `Index`.

New test functions verified against `openssl kdf PKCS12KDF`:
- `pkcs12_key_derive_empty_password`: empty UTF-8 password
  (BMP null-terminates to [0x00,0x00]); covers all three key types
  plus a key_len=1 prefix-consistency check.
- `pkcs12_key_derive_long_salt`: 72-byte salt spanning two 64-byte
  SHA-256 blocks (slen=128), exercising the S-padding loop beyond
  one diversifier block.

Breaking change: `derive_key`, `derive_key_bmp`, and `derive_key_utf8`
now return `Zeroizing<Vec<u8>>` instead of `Vec<u8>`.  Callers that
stored the result as `Vec<u8>` or compared it directly with `[u8; N]`
will need to call `.as_slice()` or deref-coerce.  Most read-only
callers are unaffected due to `Deref<Target = Vec<u8>>`.

Pre-existing issue (not introduced here): `rounds: i32` — passing 0
or a negative value silently behaves as `rounds = 1` (one hash
iteration).
MarkAtwood added a commit to MarkAtwood/formats that referenced this pull request Apr 11, 2026
Add two explanatory comments in decrypt_rc2:
- Zeroizing::new() wrappers on derive_key_utf8 return values are
  intentional on this branch (kdf returns Vec<u8>); note they will
  be removed once kdf PR RustCrypto#2283 (Zeroizing<Vec<u8>> return) lands.
- Salt length is bounded by DER input size; no separate allocation
  cap is needed.
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.

1 participant