Repository navigation
Add the PBES2-HS256+A128KW, PBES2-HS384+A192KW and PBES2-HS512+A256KW key encryption algorithms (RFC 7518 section 4.8) - #185
Conversation
|
@TheStormN rebased onto master (3c153f7) and ready for review. Two things the rebase had to settle, both from the
It is also one commit shorter: it was stacked on #184, whose commits are in master now, so what is left is the PBES2 work alone. One interaction I checked rather than assumed, since master now refuses a header parameter that the algorithm produces itself: Full |
TheStormN
left a comment
There was a problem hiding this comment.
Btw @zandbelt from now on could you please run a separate agent to review your PRs to save some effort.
Findings:
- P1 — Valid shared alg headers cannot be encrypted
At src/jwe.c:2283, _cjose_jwe_validate_alg() receives the recipient header as its “shared” header argument instead of shared_unprotected_header. Therefore a valid JWE configuration with alg in the shared unprotected header is rejected before AES-GCM-KW runs. RFC 7516 explicitly permits alg to be shared across recipients. RFC 7516 §7.2.1
Pass shared_unprotected_header there. - P1 — Encryption can emit duplicate iv/tag header parameters
At src/jwe.c:932-937, generated iv and tag are written to the protected header for a single recipient, or the recipient header for multiple recipients, without rejecting caller-supplied values in the other header locations. This can produce duplicate names across protected/shared/per-recipient headers, and can make the resulting JWE fail to decrypt even with the correct key. RFC 7516 requires these locations to be disjoint. RFC 7516 §7.2.1
Reject caller-supplied iv/tag, or validate the assembled headers after key wrapping. - P2 — iv and tag are accepted as critical parameters for unrelated algorithms
At src/jwe.c:416, iv and tag are added to one global supported-critical-header list. Consequently, an A128KW or RSA JWE can mark iv critical and cjose will accept and ignore it. Critical parameters must be understood and processed; crit is intended for extensions, not globally accepted standard parameters. RFC 7515 §4.1.11
Scope critical-parameter handling by algorithm, or reject crit since cjose does not implement extensions.
|
Happy to run a review agent over these before submitting — I have started doing that, and the rest of this comment is the result. First, though, I think this review was made against the state of the PR before it was rebased. All three findings were real in the version that was stacked on #184, and all three are in master now, two of them written in answer to your earlier reviews on #184 and #188. Against the current head,
So there is nothing here for me to change. Worth pinning a re-review to the head commit rather than the PR as a whole, which is what I take from this: an agent reviewing a rebased PR needs to be pointed at the current head. On my side I ran one over the three open PRs against master
It did raise one hardening point I would rather fix than leave: there is a minimum on the decoded |
|
Thanks! I did synced up before running the review again, but I will start with fresh context and check again. Regarding the hardening point - sure lets have it fixed in this PR as it have to me synced again with |
|
Synced to master (40a5767) and the hardening is in, as The salt bound. The rebase itself was clean — one conflict, and an additive one: the OKP and PBES2 test registrations landed on the same line. While in there I also took the other points from the review of this PR: a caller-supplied One thing I want your decision on rather than my own. At the current I tried lowering it and put it back, because the same constant bounds encryption: at 100000 a caller could no longer produce a JWE at the iteration counts current password hashing guidance asks for (OWASP is at 210000 for HMAC-SHA-512 and 600000 for SHA-256, and NIST SP 800-132 section 5.2 contemplates far higher). Splitting the two would let encryption exceed what decryption accepts, and then cjose could emit a JWE it refuses to read back, which is the invariant #188 established. So the options are a lower symmetric cap, the current one, or a deliberate split, and the choice is a policy one. Happy to implement whichever you prefer. For context on the numbers, none of this is close to the caps in practice: go-jose defaults to exactly 100000, which the test now pins as accepted since the check is Reviewed by an independent agent before each push, per your request. It is what found the wrong octet counts, the one-sided iteration test, and the encrypt-side argument that made me revert the cap. |
TheStormN
left a comment
There was a problem hiding this comment.
Ok, ran again with fresh context from fresh master. For 2 while there is no recommended maximum as you've already mentioned, it seems to be stricter than required.
Findings:
- High — caller-supplied p2s can violate RFC 7518’s mandatory fresh-random-salt requirement.
src/jwe.c:1224 reuses an existing p2s. Reusing the same header across encryptions therefore reuses the PBKDF2 salt/derived KEK, contrary to RFC 7518 §4.8.1.1. The encrypt path should generate a fresh salt and reject caller-supplied p2s, or explicitly redesign this as caller-provided random entropy.
The same path preserves the supplied encoding verbatim, so padded values such as ...= can also produce non-JOSE base64url output; RFC 7515 §2 requires padding to be omitted. - Medium — parameter bounds are stricter than RFC requirements.
The implementation rejects p2c below 1000 and above 1,000,000, and p2s above 1024 bytes (jwe_int.h:81). RFC 7518 requires a positive p2c and only recommends 1000; it specifies no maximum for either value (§4.8.1.1, §4.8.1.2). These may be reasonable DoS-policy limits, but they should be documented as implementation limits rather than presented as RFC validation. - Low — the salt-size limit is enforced after decoding.
src/jwe.c:1344 base64url-decodes attacker-controlled p2s before checking the 1024-byte limit, allowing an unnecessarily large temporary allocation. Check the encoded length before decoding.
|
Thanks, all three are addressed in 350a962, and I did run a separate agent over the change before pushing it. 1. Fresh salt. You are right, and it is a MUST: RFC 7518 section 4.8.1.1 says a new salt input must be generated randomly for every encryption operation, and reusing a header object was enough to break it. The salt is now always generated, and a caller-supplied What is lost is the ability to reproduce a known-answer vector with a fixed salt through cjose's own encrypt API. Verifying such vectors is unaffected, and the RFC 7517 Appendix C test still covers that direction. The regression test is the shape you described: encrypt, feed the resulting protected header into the next encryption, and it is now refused where it used to hand back the same salt and the same KEK. Both it and a plain well-formed 2. Bounds. Fair point, they were presented as validation and they are policy. Two things the documentation now also says, which were true before but unwritten: the iteration ceiling bounds the work per recipient, since 3. Decode before check. Done, the encoded length is checked first. The bound is the padded length, Verified with |
… key encryption algorithms (RFC 7518 section 4.8) A key encryption key of the size the alg names is derived from the password, the octets of the oct key, with PBKDF2 over HMAC SHA-256, SHA-384 or SHA-512; the salt is the alg name, a zero octet and the "p2s" header parameter, the iteration count the "p2c" header parameter. The CEK is then wrapped with AES Key Wrap. The parameters are published and read back like the "iv" and "tag" of the AES GCM key wrapping algorithms; a caller-supplied "p2s" or "p2c" is used as it is, otherwise a 16-octet random salt and 8192 iterations. The salt must be at least 8 octets and the iteration count between 1000 and 1000000, a build-time constant, on both sides: "p2c" is attacker-controlled on decrypt, so the bounds and the encrypted key length are checked before any PBKDF2 work. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
…nds are policy RFC 7518 section 4.8.1.1 requires a new salt input to be generated randomly for every encryption operation. The encrypt path used a caller-supplied "p2s" as it was, so a caller that reuses a header object, in particular one that feeds the protected header of a JWE it produced into the next encryption, derived the same key encryption key again. The salt is now always generated and a caller-supplied "p2s" is refused rather than quietly dropped, before any key derivation runs: the treatment "iv", "tag" and "epk" already get through _cjose_jwe_reject_generated_param. A caller-supplied "p2c" is still used as it is, an iteration count being a policy value with no freshness requirement. Refusing "p2s" also keeps a caller's base64url padding, which RFC 7515 section 2 does not allow, out of the produced header. Of the parameter bounds only the 8 octet minimum salt input is an RFC requirement; the rest is cjose policy. Say so in jwe_int.h and in the README, make all four policy values overridable at build time rather than two of them, and add _Static_assert checks so that an inconsistent set of overrides fails to compile instead of failing every encryption at run time. On decrypt, check the encoded length of "p2s" before base64url-decoding it, so that an oversized salt input is not allocated only to be refused afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
…reads RFC 7518 section 4.8.1.2 asks a producer for a positive iteration count and RECOMMENDS at least 1000. Refusing a count below the recommendation on both sides made cjose unable to read a conformant JWE that chose a lower one, and refusing to read it never made that JWE any stronger: the count is the producer's to pick, and what protects the recipient is the ceiling on the work an unauthenticated JWE can demand. So CJOSE_JWE_PBES2_MIN_ITERATIONS now bounds only what cjose produces, and a new CJOSE_JWE_PBES2_MIN_ACCEPTED_ITERATIONS, the RFC's "positive" by default, bounds what it accepts; a deployment that would rather fail closed on a weak count raises it. The ceiling still applies to both directions. Make the five PBES2 bounds CMake variables alongside CJOSE_ENABLE_RSA1_5, rather than defines on an internal header, with a row each in the README build options table. A value is refused at configure time unless it is a positive integer without a leading zero, which the compiler would otherwise read as an octal literal, and the test suite follows the configured values so that a build that changes one still passes. The key derivation helper checks the count against 1 itself now: the floor differs between its two callers, so neither implies a lower bound there. Adds a python-jwcrypto vector with a count of 100, decrypted where the accepted floor admits it and refused where a build has raised it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
|
The review you left on #193 is about this code, so I have answered it here, in 393e25b.
There is a jwcrypto vector at Upper bounds. All five PBES2 bounds are CMake variables now, next to A value has to be a positive integer without a leading zero, or configuration fails: Padded Verified with |
Summary
Adds the PBES2 key encryption algorithms
PBES2-HS256+A128KW,PBES2-HS384+A192KWandPBES2-HS512+A256KWof RFC 7518 section 4.8. A key encryption key of the size thealgnames is derived from the password, the octets of theoctkey, with PBKDF2 over HMAC SHA-256, SHA-384 or SHA-512 (PKCS5_PBKDF2_HMAC); the salt is the algorithm name, a zero octet and thep2sheader parameter, the iteration count thep2cheader parameter. The CEK is then wrapped with AES Key Wrap through the existing helpers. With #188, cjose covers every algorithm of RFC 7518 (refs OpenIDC#28).p2sis refused rather than quietly dropped, before any key derivation runs. This is the treatmentiv,tagandepkalready get, through the same_cjose_jwe_reject_generated_param. The iteration count is a policy choice rather than a value that has to be fresh, so a caller-suppliedp2cis used as it is and the count can be chosen per JWE, and per recipient, without new API. Both parameters are published likeivandtag: the protected header for a single recipient, the recipient's own header with several, and on decrypt they are read from the recipient, shared and protected headers in that order.jwe_int.hand in the README, and each of the four values can be overridden at build time: at least 1000 iterations (the RFC's recommendation, refused below rather than silently accepted), at most 1000000, at most 1024 salt octets, and 8192 iterations by default._Static_asserts keep an overridden set consistent and inside what OpenSSL takes as anint, rather than letting a misconfiguration fail every encryption at run time. On decryptp2cis attacker-controlled, so the bounds and the encrypted key length (CEK plus 8) are checked, and the encoded length ofp2sis checked before it is decoded, all before any PBKDF2 work: a JWE can demand at most a million HMAC iterations per recipient fromcjose_jwe_decrypt. The ceiling has to stay at or above 100000, the highest default a mainstream producer ships. The encrypt default of 8192 is an interoperability choice: several widely used implementations refuse tokens above roughly 10000 to 16384 iterations by default, so a higher cjose default would produce JWEs they cannot read.CJOSE_HDR_ALG_PBES2_HS256_A128KW,CJOSE_HDR_ALG_PBES2_HS384_A192KW,CJOSE_HDR_ALG_PBES2_HS512_A256KW,CJOSE_HDR_P2SandCJOSE_HDR_P2C. A README row and a note on the parameters.kidchecked); a two-recipient JSON round trip with a different count and a different salt per recipient; negative cases for a non-octkey, the wrong password, an iteration count above the maximum, below the minimum or not an integer, a missing or short salt on decrypt, a salt at and beyond the maximum, an encrypted key of the wrong length, and a caller-supplied salt input in each of the three header locations, including the shape the RFC requirement is about: feeding the protected header of a produced JWE into the next encryption.Testing
-pedantic -Wall -Werrorbuild and the fullcheck_cjoserun withCJOSE_ENABLE_RSA1_5OFF and ON, and once more with the four PBES2 constants overridden, to check that the suite follows them.clang-formattarget produces no diff.🤖 Generated with Claude Code