Repository navigation
Add X25519 and X448 to the ECDH-ES key agreement (RFC 8037 section 3.2) - #186
Conversation
ECDH-ES and ECDH-ES+A128KW, +A192KW and +A256KW accept OKP keys on X25519 and X448 next to EC keys. The ephemeral key is generated with the type and curve of the recipient's key and published as an OKP JWK in "epk"; the Concat KDF and the apu and apv handling stay as they are. The recipient key and the ephemeral key must be of the same type on the same curve, which the key agreement now checks itself as well, and the Ed25519 and Ed448 signature keys are refused for key agreement. The four ECDH-ES paths share the ephemeral-key and curve-match helpers instead of calling the EC-only functions, and the public cjose_jwk_derive_ecdh_ephemeral_key gains the two curves with them. The RFC 8037 Appendix A.6 and A.7 examples are verified in both directions with Bob's private keys from RFC 7748 section 6, whose public keys the examples use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
7e96d1e to
5cee18c
Compare
|
@TheStormN rebased onto master (3c153f7) and ready for review. Two conflicts, both resolved by combining rather than choosing a side:
The interaction worth checking is that master now refuses a header parameter the algorithm produces itself, Full |
TheStormN
left a comment
There was a problem hiding this comment.
The review found a P3 issue. Please check if it is worth fixing in this PR or in a separate one.
Minor P3 conformance note: the new OKP decryption path accepts an epk containing private d, since the check at jwe.c:1364 validates only type and curve. RFC 7518 §4.6.1.1 requires epk to contain only public parameters. This behavior already exists for EC keys and does not affect the derived public-key operation, but strict validation should reject private parameters.
RFC 7518 section 4.6.1.1 gives the "epk" header the public key parameters of the ephemeral key and nothing else. The ECDH-ES decryption paths checked only that the ephemeral key had the recipient key's type and curve, so one carrying its private part was accepted and the key agreement ran on it, which failed later with a crypto error rather than up front with an invalid argument. That holds for EC keys as it does for the OKP curves this branch adds, and both go through the same check, so both are refused now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
|
Good catch, and fixed here rather than separately (
One detail worth recording, since it sharpens the "does not affect the derived operation" part of your note: the private Full |
TheStormN
left a comment
There was a problem hiding this comment.
I found one remaining P3 issue.
The fix checks the internal has_private flag:
jwe.c:1364-1369
However, EC JWK import treats a present but empty or null "d" member as if it were absent (jwk.c:1979-1999). Therefore an epk such as:
{"kty":"EC","crv":"P-256","x":"...","y":"...","d":""}is imported as a public key, has_private remains false, and it is accepted. This still violates RFC 7518 §4.6.1.1, which requires epk to contain only public-key parameters.
The added test covers a non-empty private key, but not empty/null "d". The fix should preserve or inspect parameter presence rather than relying only on has_private.
The previous commit read the has_private flag of the imported ephemeral key, which an EC "epk" carrying an empty or null "d" slips past: the EC import took a present but valueless "d" for an absent one and produced a public key, so the JWE was accepted although RFC 7518 section 4.6.1.1 allows public parameters only. Reported by TheStormN. Two changes. The "epk" header is now inspected before the import decides what its members are worth, and a "d" member is refused whatever it holds. And the EC import treats a present but valueless "d" as the malformed key it is rather than as a public key, which is the distinction the OKP import already made. Tests cover a private, an empty and a null "d" for EC and for X25519; the EC ones fail without this commit, while the OKP ones were already caught by the import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
|
You are right, and the Confirmed first, with the suite's P-256 key, that an Two changes, matching your "preserve or inspect parameter presence":
Tests cover a private, an empty and a null One thing I did not touch, since it is outside this PR's subject: Full |
|
@zandbelt Yes, please open other PR to address the issue which you noticed. |
#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 #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>
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>
Summary
Adds the X25519 and X448 curves of RFC 8037 section 3.2 to the ECDH-ES key agreement:
ECDH-ES,ECDH-ES+A128KW,ECDH-ES+A192KWandECDH-ES+A256KWnow accept OKP keys on those curves next to EC keys. cjose has had the OKP key type with X25519 and X448 since #169 but could only sign with OKP keys; this uses them for what RFC 8037 registered them for.epk; the Concat KDF,apu,apvand the key sizes are exactly those of RFC 7518, so the four ECDH-ES paths only swap their EC-only calls for two shared helpers, one that makes the ephemeral key and one that checks that the recipient key andepkare of the same type on the same curve. The key agreement in jwk.c performs that check itself as well, so the publiccjose_jwk_derive_ecdh_ephemeral_keygains the two curves and refuses mismatched keys withCJOSE_ERR_INVALID_ARG. The Ed25519 and Ed448 signature keys are refused for key agreement everywhere. OpenSSL's exchange already rejects a small-order peer key (the all-zero shared secret of RFC 7748 section 6.1), so no extra check is needed there.epkon any other curve is rejected before the key agreement runs.jwk.hcomments of the derive functions, which said "EC key pair".jwksuite, the RFC 8037 Appendix A.6 and A.7 examples: the ephemeral key pair and Bob's public key are the RFC's, Bob's private keys come from RFC 7748 section 6 (the examples use its public keys), and the shared secret Z is checked in both directions; plus mismatched-key cases. In thejwesuite, round trips over both curves and all four algorithms with GCM and CBC content encryption, a check thatepkis a public OKP key on the recipient's curve, python-jwcrypto vectors for both curves, and negative cases: an Ed25519 key offered for ECDH-ES on encrypt and on decrypt, decrypting an X25519 JWE with an X448 or EC key, and anepkof another type or curve or of the wrong size.Independent of #184 and #185; whichever lands later needs at most a trivial rebase around adjacent README rows, which I will do.
Testing
-pedantic -Wall -Werrorbuild and the fullcheck_cjoserun (122 checks) withCJOSE_ENABLE_RSA1_5OFF and ON against OpenSSL 3.5.5.jwkandjwesuites: no leaks, no errors.clang-formattarget produces no diff.🤖 Generated with Claude Code