Skip to content

feat(pkcs12): add RC2-CBC PBE decryption support#2284

Open
MarkAtwood wants to merge 5 commits intoRustCrypto:masterfrom
MarkAtwood:shrouded-key-bag-rc2-decrypt
Open

feat(pkcs12): add RC2-CBC PBE decryption support#2284
MarkAtwood wants to merge 5 commits intoRustCrypto:masterfrom
MarkAtwood:shrouded-key-bag-rc2-decrypt

Conversation

@MarkAtwood
Copy link
Copy Markdown

Summary

  • Implements decrypt_rc2_128_cbc and decrypt_rc2_40_cbc on
    EncryptedPrivateKeyInfo, covering:
    • pbeWithSHAAnd128BitRC2-CBC (OID 1.2.840.113549.1.12.1.5)
    • pbeWithSHAAnd40BitRC2-CBC (OID 1.2.840.113549.1.12.1.6)
  • Both schemes are deprecated; this implementation exists solely to
    support reading legacy .p12 files produced by OpenSSL's -legacy
    flag and similar tools.
  • New encryption feature (mirrors the convention in pkcs8).

Public API change

PKCS_12_PBEWITH_SHAAND40_BIT_RC2_CBC (missing underscore) is renamed
to PKCS_12_PBE_WITH_SHAAND40_BIT_RC2_CBC. The old name is kept as a
#[deprecated(since = "0.2.0")] alias for backwards compatibility. Happy
to remove the alias if you prefer a clean break at 0.2.0.

Dependency note

cbc = "0.2.0-rc.4" is an exact pre-release pin — intentional
coordinated monorepo release, same as the existing cms pin.

Zeroizing note

decrypt_rc2 wraps the derive_key_utf8 return values in
Zeroizing::new() because on this branch kdf::derive_key_utf8 still
returns Vec<u8>. Once PR #2283 lands (derive_key_utf8 returning
Zeroizing<Vec<u8>>), those wrappers will be removed in a follow-up.

Pre-existing doc bugs fixed (in scope)

  • OID constant doc comments in lib.rs were all copy-pasted as
    `pbeWithSHAAnd128BitRC4`; corrected to match each constant.
  • Pkcs12PbeParams::salt doc was /// the MAC digest info (wrong);
    corrected to describe the salt.
  • derive_key_bmp doc was the stub /// Derive; expanded to full
    description with RFC reference.
  • TODO stubs at the bottom of lib.rs removed (tracked as separate
    issues).

Pre-existing issue spotted but left out of scope

EncryptedPrivateKeyInfo carries #[allow(missing_docs)] on its
fields. Not addressed here.

Test coverage

  • tests/rc2_cipher.rs: RFC 2268 §5 ECB known-answer tests (vectors
    1–8) + EKB sensitivity proof — contractually pins the rc2 crate's
    effective-key-bits behaviour before any pkcs12 code is exercised.
  • tests/kdf.rs: 10 drh-consultancy / Bouncy Castle SHA-1 vectors
    (passwords "smeg" and "queeg"), confirmed against OpenSSL 3.0.13
    openssl kdf PKCS12KDF; plus RC2-specific key sizes (16 B, 5 B).
  • tests/decrypt_rc2.rs: full integration tests —
    happy-path (iter=1, 2048, 100 000), wrong-password, non-BMP password,
    OID-swap rejection, cipher-layer isolation vectors (both OIDs),
    pyca/cryptography cross-vendor fixture, SHA-256 fingerprint oracle
    (independently confirmed by OpenSSL and pyca/cryptography).
  • Test fixtures and oracle values documented in tests/data/README.md.

Test plan

  • cargo test --features encryption,kdf -p pkcs12 — 36 tests + 2 doc-tests
  • cargo hack check --feature-powerset --no-dev-deps -p pkcs12 — all 10 combinations
  • RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc --no-deps --all-features -p pkcs12
  • cargo +1.85 test --features encryption,kdf -p pkcs12 (MSRV)
  • typos pkcs12/

All pass locally.

Implements pbeWithSHAAnd128BitRC2-CBC (OID 1.2.840.113549.1.12.1.5)
and pbeWithSHAAnd40BitRC2-CBC (OID 1.2.840.113549.1.12.1.6) as
defined in RFC 7292 Appendix C.

New public API (behind feature "encryption"):
  EncryptedPrivateKeyInfo::decrypt_rc2_128_cbc(&self, password: &str)
  EncryptedPrivateKeyInfo::decrypt_rc2_40_cbc(&self, password: &str)

Key design decisions:
- OID verified before any KDF work (fail cheap on wrong method)
- MAX_ITERATIONS = 1_000_000 cap prevents DoS from crafted .p12 files
- EKB is set implicitly by key length via Rc2::new_from_slice:
  16-byte key -> EKB=128, 5-byte key -> EKB=40
- Zeroizing<Vec<u8>> used for key, IV, working buffer, and return value

New dependencies (optional, behind "encryption" feature):
  sha1 = "0.11.0"
  cbc = "0.2.0-rc.4"   <- pre-release pin, intentional monorepo release
  rc2 = "0.9.0"

Test coverage:
- RFC 2268 §5 ECB KATs 1-8 (normative RC2 spec vectors)
- EKB sensitivity proof (same key, EKB=64 vs 128 -> different output)
- drh-consultancy / Bouncy Castle PKCS12KDF SHA-1 vectors 1-10
- OpenSSL CBC interop vectors for both RC2-128 and RC2-40
- Cipher-layer isolation tests (KDF + cipher, no PFX parsing)
- Full-stack fingerprint oracle (SHA-256 of decrypted DER vs OpenSSL + pyca)
- OID mismatch rejection and cross-vendor (pyca) parse test

Pre-existing issues spotted but left out of scope (tracked separately):
- Pkcs12PbeParams.salt field doc says "MAC digest info" (wrong)
- derive_key_bmp has truncated doc comment "Derive"
- derive_key() intermediate hash state not zeroized
- derive_key() returns plain Vec<u8> not Zeroizing<Vec<u8>>
- derive_key_bmp: replace truncated stub with full doc
- Pkcs12PbeParams.salt: fix wrong "MAC digest info" label
- lib.rs: remove four TODO comments (tracked in beads)
- rustfmt: reorder imports alphabetically
- add #[deprecated] alias for renamed PKCS_12_PBEWITH_SHAAND40_BIT_RC2_CBC
- clarify Zeroizing spare-capacity behavior in decrypt.rs comment
- add non-BMP password rejection tests for both RC2 decrypt methods
- remove redundant rc2 dev-dependency (covered by optional main dep)
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.
The SHA-256 recorded for the pyca EC PrivateKeyInfo DER was wrong
(c5eacb73...).  Correct value confirmed by three independent sources:
OpenSSL, pyca/cryptography private_bytes(DER, PKCS8, NoEncryption),
and carl-wallace/pkcs12_builder — all agree on 956890dd...
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