Repository navigation
Refuse a private member whose value is base64url padding only - #192
Merged
Merged
Conversation
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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
TheStormN
approved these changes
Sep 12, 2026
This was referenced Sep 13, 2026
TheStormN
pushed a commit
to OpenIDC/cjose
that referenced
this pull request
Sep 13, 2026
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>
TheStormN
pushed a commit
that referenced
this pull request
Sep 13, 2026
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. #189 tested the decoded buffer pointer, #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. Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
TheStormN
pushed a commit
that referenced
this pull request
Sep 15, 2026
The 1.0.0 section carried only the Breaking list that #187 wrote, so nothing merged after it was recorded: the A*GCMKW, PBES2, X25519/X448 and ML-DSA algorithms, the "crit" refusal and the JWE header disjointness, the JWK import refusals of #189, #190, #192 and #193, the NULL cjose_err crash of #191 and the EVP_Q_mac change of #196. The entries reference pull requests, as the rest of that section does, rather than the commit links the released sections use. The "crit" refusal is listed as breaking because it refuses a JWE or JWS that 0.8.0 accepted. Three more rules do the same without changing the API, so they stay under Fix and a Compatibility paragraph names them, the way the 0.8.1 notes do: the disjointness of the header locations, the refusal of a header parameter the algorithm generates, and the refusal of a valueless private member in an RSA or EC key. 0.8.1 was released from the 0.8.x branch on 2026-09-14 and its section only ever existed there, so this file jumped from the unreleased 1.0.0 straight to 0.8.0 and the release was invisible here. It is copied over unchanged. The README already describes the ML-DSA algorithms, the AKP key type and the CJOSE_ENABLE_ML_DSA option; the only thing missing was the OpenSSL requirement in the prerequisites, which named 3.0.0 alone. Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
TheStormN
pushed a commit
that referenced
this pull request
Sep 15, 2026
Three sentences #199 added describe the 0.8.x line or an earlier commit rather than what main does, found by a review of the merged text against the source at 0.8.0 and at each pull request. The generated-parameter entry said the check for a caller-supplied "epk" had read it as a string and so never saw one. That defect existed only on the 0.8.x backport, whose 0.8.1 notes the sentence was taken from; on main the helper has looked the parameter up as JSON since #188 introduced it. What 0.8.0 did on main was silently replace a protected "epk" with the generated one, and leave one in the shared or a per-recipient header beside it. The RSA private-member entry gave "imported as a public key" as the outcome for every valueless form. That is true of an empty, null or padding-only member (#189, #192) but not of a zero one (#193), which imported as a private key whose export cjose could not read back, as the 0.8.1 section below it already says. The Compatibility paragraph counted three rules that refuse input 0.8.0 accepted and missed two: the "oth" refusal of #190, since 0.8.0 knew no such member and imported a multi-prime key as a two-prime one, and the refusal of an "epk" naming a private member of #186. It also listed "iv", "tag" and "p2s" as if 0.8.0 had accepted them, when the algorithms that use them are new in this release. The paragraph now names the rules without counting them. Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #189, which landed while this was in flight. The guard it added tests the decoded buffer pointer:
but base64url padding on its own decodes to zero octets with a non-NULL buffer:
cjose_base64url_decodebreaks out of its loop on the first=, publishes the allocated buffer and reports a length of 0. So a private member of"=="or"===="walks past the check and{"kty":"RSA","n":"...","e":"AQAB","d":"=="}still imports as a public RSA key, which is exactly what #189 set out to prevent.
"="alone was already refused, by theinlen % 4 == 1gate in the decoder, which is why the hole is easy to miss. The same applies top,q,dp,dqandqi.The guard now tests the decoded length as well. EC and OKP keys were never affected: their importers pass a non-zero expected length, so the decoder's length check rejects the padding first. RSA passes 0, meaning "any length", so there is nothing else to catch it.
Found by an independent review agent over the open PRs, which is the practice you asked for in #185 — this is the first thing it turned up.
Testing
test_cjose_jwk_import_empty_private_membernow covers"=="and"===="beside the empty string andnull, so 24 negative cases over the six private members, plus the public and private serializations of the same key still importing. It fails on master with "a d of "==" was accepted".check_cjose,clang-formatproduces no diff.🤖 Generated with Claude Code