Repository navigation
A 0.8.x maintenance release: six fixes backported from the 1.0 line - #195
Conversation
…ing algorithm (cisco#191) The trailing cjose_err of the public API may be NULL, and cjose_jwe_decrypt passes the caller's pointer straight to the key decryption function, so _cjose_jwe_decrypt_ek_ecdh_es_kw() dereferenced it in its first statement: decrypting any ECDH-ES+A128KW, ECDH-ES+A192KW or ECDH-ES+A256KW JWE with a NULL err segfaulted. Its sibling for plain ECDH-ES was given a local error object to fall back on when that path was hardened, and the key wrapping variant was missed. The regression test extends the existing NULL-err test over the three key wrapping algorithms; the test binary crashes without the fix. Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
cisco#189) _cjose_jwk_decode_json_object_base64url_attribute() reports an attribute that is present but empty or null the same way it reports an absent one, with a NULL buffer and a true return, so the RSA import read "d", "p", "q", "dp", "dq" and "qi" of that shape as absent and produced a public key from a malformed one. RFC 7517 gives each of those members a base64url value, so the key is invalid and must be refused rather than silently reinterpreted. The six decodes go through a helper that keeps the distinction, and the required public members need no such guard: a valueless "n" or "e" already fails when the key is built. The OKP import makes the same distinction for its "d" already; the EC import gets it in cisco#186, which can share this helper once both have landed. Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Refuse a private member whose value decodes to nothing Base64url padding on its own decodes to zero octets and publishes a non-NULL buffer, so "d": "==" slipped past a check that only looked at the pointer and the key still imported as a public one. The guard tests the decoded length as well, and the test covers "==" and "====" beside the empty string and null. Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third turn at one rule: an RSA private member that is present but carries no usable value must fail the import instead of being read as absent. cisco#189 tested the decoded buffer pointer, cisco#192 added its length, and both let a value of "AA" through: it decodes to octets that are all zero, which is the integer 0, with a length of one. The predicate is now the thing it was always meant to be, that the decoded value is zero. What that costs a caller today: a key whose "d", "p", "q", "dp", "dq" or "qi" is zero imports, and cjose_jwk_to_json() then writes the member back as an empty string, so cjose cannot read its own export in again. The test covers that directly, on a complete private key rather than on a public one with a member appended, and asserts the round trip for the key that is accepted. A value that merely contains a zero octet is a number like any other and keeps importing; "AQA" and "AAE" cover both ends of the scan. This does not make an accepted key a usable one: any other wrong "d" still imports, signs, and produces a JWS that does not verify. Refusing that needs a consistency check on the key rather than on one member, which is a change of a different size. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
… header locations disjoint Two fixes from the 1.0 line that are not tied to any of its new algorithms or to its break with OpenSSL 1.x, ported here. RFC 7518 section 4.6.1.1 gives the "epk" header the public key parameters of the ephemeral key and nothing else, but both ECDH-ES decryption paths took whatever the import made of it, so an ephemeral key carrying the recipient's own private part was accepted and the key agreement ran on it. The header is now inspected before the import decides what its members are worth, and a "d" member is refused whatever it holds. RFC 7516 section 7.2.1 requires the member names of the protected, the shared unprotected and a per-recipient unprotected header to be disjoint. They were not checked, and _cjose_jwe_get_from_headers() resolves a name per-recipient first, then shared, then protected, so a JWE that repeated a name was accepted and the copy that the content encryption does not authenticate won: a protected "alg" could be shadowed by a per-recipient one. For the encryption side to see the shared header at all, cjose_jwe_encrypt_multi_iv() passes it to _cjose_jwe_validate_alg() where it passed the recipient's own header, which also lets "alg" be resolved from it as the import path already did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
477cbee to
a36b9ec
Compare
|
Pushed three corrections after running an independent review over the branch. Two of them were things I had claimed in the description above that were not true, so they are worth calling out rather than leaving in the diff:
I have also added three behaviour changes to the description and the CHANGELOG that I had not disclosed. One of them widens what cjose accepts rather than narrowing it, which deserves your attention in a fixes-only release: Nine commits now. Each builds and passes standalone; |
TheStormN
left a comment
There was a problem hiding this comment.
The PR review is good, but please remove all the versions bumps from both CMake and autotools as I'm doing that as part of the release process.
RFC 7516 section 7.2.1 requires the member names of the protected header, the shared unprotected header and each per-recipient unprotected header to be disjoint. Import checks that already; encryption did not, so cjose could produce a JWE that it would then refuse to read back. Validate the assembled headers at the end of the encryption as well, and hand _cjose_jwe_validate_alg the shared unprotected header rather than the per-recipient one it was given twice, which is what the disjointness check needs to see all three locations. Also fix the out-of-tree autotools test build, which passed only the build directory include path and so compiled the test against an installed copy of the headers; test/Makefile.in is tracked on this line, so it is updated alongside test/Makefile.am. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
The guard that keeps a caller from supplying the parameter the ECDH-ES key agreement produces itself looked it up with _cjose_jwe_get_from_headers, which ends in json_string_value and so returns NULL for any value that is not a string. An "epk" is a JSON object, so the guard never saw one and the caller's header was silently replaced instead of refused. Add the JSON-valued lookup beside the string one and use it there, which is what upstream does. The disjointness rule already caught the shared and per-recipient cases after the fact; this is the protected header, and it now fails before the key agreement runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
The same defect the RSA import was fixed for: an EC JWK whose "d" is there but holds an empty string, a JSON null or base64url padding on its own imported as a public key rather than failing, so a caller who believed they had a signing key got one that cannot sign. RFC 7518 section 6.2.2 makes "d" the private key, present for a private key and absent for a public one; there is no third state in which it is present and means nothing. Route it through _cjose_jwk_decode_private_attribute, which the RSA import already uses, and record the two behaviour changes that came with validating the shared unprotected header at encryption time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
a36b9ec to
f18ae38
Compare
|
Done, in f18ae38. Two related things I did along with it, both easy to undo if you would rather:
I also dropped "and release as 0.8.1" from that commit's subject, since it no longer does that. Rebased rather than reverted, so no version bump is left anywhere in the history. Nine commits, each still building and passing on its own, 122 checks with |
Closes #194, now that
0.8.xexists to target. Nine commits off the0.8.0tag, which is where this branch points, so it applies as a fast-forward.Fixes only. No algorithm, no API, no ABI change; the OpenSSL 1.0.1 floor, the autotools build and
libcjose.so.0all stay.cjose_jwe_decryptdereferenced a NULLcjose_erron anyECDH-ES+A*KWJWE. A crash on a documented-optional argument, reachable by any application that passesNULLmaster— if it changes in review I will match this commit to itoth) was silently used as if it had two primesepkcarrying a private member is refused (RFC 7518 section 4.6.1.1); the three JWE header locations must be disjoint (RFC 7516 section 7.2.1)epkis refused in the protected header too. The guard looked the parameter up as a string and anepkis an object, so it never saw one theredis present but valueless is refused, the same defect #189 fixed for RSA and left for ECThe last two needed hand-porting rather than a cherry-pick: upstream squash-merged both together with things this line must not take — X25519/X448,
A*GCMKW, and thecritrefusal.Deliberately left behind: the
critrefusal, since rejecting input that 0.8.0 accepts is not patch-release behaviour, and every feature and build change.What it costs a user, since a maintenance line should not surprise anyone. All of these are in the CHANGELOG:
alg, where the unauthenticated per-recipient copy won, but it also refuses harmless repetitions such askid. Every JSON vector intest/and the python-jwcrypto ones are disjoint, so I know of no producer that does this.epkto an ECDH-ES encryption is refused. The key agreement overwrote it anyway, so nothing that worked stops working; a caller doing it now gets an error instead of silence.cjose_jwe_encrypt_multinow refuses acritthat header carries and cannot process, which import already refused on 0.8.0; it now accepts analgsupplied only there, where 0.8.0 required it in the protected or per-recipient header; and an EC JWK with a present-but-valuelessdno longer imports.I would rather state that second one plainly than have it found in review: it is an acceptance-widening in a fixes-only release. It falls out of passing the right header to the disjointness check, and matches what merged upstream. Say the word and I will restrict it.
No version is bumped anywhere:
CMakeLists.txt,configure.acand the generatedconfigureall still say 0.8.0, and the CHANGELOG section is headedUnreleasedfor you to number at release time.Testing
Each of the nine commits builds and passes on its own. On the tip: the full
check_cjoserun withCJOSE_ENABLE_RSA1_5off (122 checks) and on (121), noclang-formatdiff, valgrind clean, and every fix carries a regression test verified to fail on the0.8.0tag — including the headline crash, which I confirmed SIGSEGVs on a build of the tag itself.SONAMEislibcjose.so.0and the exported symbol list is byte-identical to 0.8.0. The autotools build works in tree and out of tree;test/Makefile.inis tracked on this line, so the out-of-tree include fix is applied there alongsidetest/Makefile.am.🤖 Generated with Claude Code