Repository navigation
fix: bound attacker-controlled work factors and break issuer cycles during PKCS#12 import - #11
Conversation
51ff74c to
317422d
Compare
317422d to
10c8a49
Compare
|
Good call — done, pushed as an update to the first commit. One thing I double-checked while switching: the loop skips pushing an issuer whose |
| /// Returns `true` when attacker-controlled scrypt parameters stay within the | ||
| /// memory and parallelism budget. The effective memory cost is `128 * N * r` | ||
| /// bytes; `checked_mul` treats an overflowing product as over budget, since | ||
| /// such parameters are by definition far past the limit. |
There was a problem hiding this comment.
This AI-generated comment looks completely redundant because the function name already explains it.
|
|
||
| /// Upper bound for attacker-controlled PBES1/PBES2 KDF iteration counts read | ||
| /// from container DER; deliberately far above counts produced by mainstream | ||
| /// tooling, so no interoperable container is rejected. |
There was a problem hiding this comment.
Rustdoc-style comments (///) are not needed for non-public members. Replace with normal comments (and below as well).
| // Stop if this issuer is already in the chain: an issuer | ||
| // cycle (A issued by B, B issued by A) would otherwise loop | ||
| // forever. The chain is tiny (typically <= 3), so a linear | ||
| // scan is cheaper than a separate visited set. |
There was a problem hiding this comment.
The comment is too verbose, we don't want every change to be documented like this. It could be skipped completely or be just // prevent infinite loop in case of cycle certificate chains
| } | ||
|
|
||
| /// Builds a PBES2/scrypt AlgorithmIdentifier, letting the caller tamper | ||
| /// with the decoded scrypt parameters before re-encoding. |
KeyStore::from_pkcs12 follows issuer names without checking whether a certificate has already been seen. A container whose certificates form an issuer cycle (A issued by B, B issued by A) makes the resolution loop alternate between them forever, growing the chain until memory is exhausted. Stop when an issuer already present in the collected chain reappears. The chain is typically <= 3 elements, so a linear scan is cheaper than a separate visited set. Acyclic chains keep their previous shape.
The MAC iteration count, the PBES1 and PBES2/PBKDF2 iteration counts, and the scrypt parameters are all read from container DER without bounds. A crafted PKCS#12 file can request up to i32::MAX/u32::MAX KDF rounds (CPU exhaustion), or scrypt parameters whose 128*N*r memory cost is unbounded — note that N, the block size r, and the parallelism p all come from the DER, so bounding N alone is insufficient. Reject iteration counts above 1,000,000, scrypt effective memory (128*N*r) above 1 GiB via checked_mul, and scrypt parallelism above 16, all with Error::InvalidParameters before any derivation runs. Covered by rejection tests for each path.
|
Done — addressed all four: |
10c8a49 to
da721a6
Compare
Summary
Importing an untrusted PKCS#12 container currently allows two denial-of-service vectors, both reachable before password verification completes — so an attacker only needs to get a crafted
.p12/.pfxfile parsed, not decrypted. I hit both while integratingp12-keystoreas the PKCS#12 backend of an offline PDF-signing application, where the PFX file is untrusted user input.Two focused commits, each with a dedicated rejection test. The full existing suite still passes (42 tests, unchanged).
1. Issuer-cycle hang → memory exhaustion (
src/keystore.rs)KeyStore::from_pkcs12builds each certificate chain by repeatedly looking up the certificate whosesubjectmatches the currentissuer, with no record of which certificates it has already visited:If two certificates name each other as issuer (cert A issued by B, cert B issued by A — trivial to craft, and also producible by accident with cross-signed CAs), the lookup alternates between them forever. The
certsvector grows on every iteration until the process is OOM-killed. This is reachable purely from parsing an untrusted container.Fix: track visited certificates by their DER identity and stop when an issuer repeats. Acyclic chains keep exactly their previous shape — the change only adds a terminating condition to a loop that could previously run unbounded.
2. Unbounded KDF work factors → CPU / memory exhaustion (
src/codec.rs)The MAC iteration count, the PBES1 iterations, the PBES2/PBKDF2
iterationCount, and all three scrypt parameters are read straight from the container DER with no upper bound. A crafted file can therefore request:i32::MAX/u32::MAXKDF rounds → the key-derivation runs for effectively forever (CPU exhaustion), or128 * N * rbytes is unbounded → immediate memory exhaustion / abort. Note this is not justN: the block sizerand parallelismpareu16values that also come straight from the DER, so boundingNalone leaves the cost unbounded via a larger(e.g. a modestNwithr = 65535).Worth flagging explicitly, because it's easy to assume
pkcs5already covers this:pkcs5only enforces itsMAX_ITERATION_COUNTinside thePbkdf2Params::hmac_sha256builder. The DER decode path (TryFrom<AnyRef>) fills the parameters without any check, so values parsed from an untrusted container are never bounded unless the consumer bounds them. That's why the guard has to live here.Fix: before any derivation runs, reject with
Error::InvalidParameters:128 * N * rabove 1 GiB, computed withchecked_mulso an overflowing product is rejected rather than wrapping;pabove 16.The limits are deliberately generous — well above what mainstream tooling produces (OpenSSL's PKCS#12 default is 2048 iterations) — so no interoperable, honestly-produced container is rejected.
Testing
cargo test— 44 passing, including a rejection test per guard: cyclic-chain import terminates; over-limit MAC, PBES1, and PBES2/PBKDF2 iterations rejected; and scrypt rejected for an oversizedN, for a large block sizerwith an otherwise-validN, and for excessive parallelismp— each before the KDF runs.Happy to adjust the limits, split the commits differently, or add more test vectors if you'd prefer.