Repository navigation
Add the ML-DSA-44, ML-DSA-65 and ML-DSA-87 signature algorithms and the AKP key type (RFC 9964) - #198
Conversation
RFC 9964 registers "AKP" with the key itself in "pub" and "priv" and the algorithm in "alg", which unlike every other key type is REQUIRED there: the key type alone does not say which algorithm the key belongs to. For the ML-DSA algorithms of US NIST FIPS 204 "priv" is the 32 octet seed rather than the expanded private key, and section 7.3 makes that length check a MUST. OpenSSL grows the expanded key from the seed itself, so a JWK round trips through the seed and the expanded form never leaves the EVP_PKEY. Section 7.4 warns that a "pub" which does not belong to the private key is a tampered or mismatched key. A supplied seed therefore has its public key derived and compared, in constant time, rather than being accepted on the caller's word, which is what the OKP import already does for "x" and "d". The algorithms arrived in OpenSSL 3.5, well above the 3.0 the rest of cjose builds on, and RFC 9964 registers them as OPTIONAL. So the implementation sits behind the CJOSE_ENABLE_ML_DSA build option, off by default and refused at configure time on an older OpenSSL, and cjose_jwk_create_AKP_random, cjose_jwk_create_AKP_spec and cjose_jwk_AKP_get_alg keep their place in the ABI either way, failing with CJOSE_ERR_INVALID_ARG when the option is off. That is the treatment RSA1_5 already gets. The tests are the three JOSE examples of RFC 9964 Appendix A.1, whose seed is all zeros and whose public key is therefore reproducible: each must import, export back to exactly the public key the RFC prints, and keep "priv" out of a public export. Beside them a random round trip per algorithm and the negative cases: a missing, unknown or foreign "alg", a missing, empty or wrongly sized "pub" including a well formed one of another ML-DSA algorithm, a seed of 31 or 33 octets, and a seed that does not match "pub". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
…9964) RFC 9964 registers the three ML-DSA algorithms of US NIST FIPS 204 for JOSE, signing with the AKP key type the previous commit added. They are pure ML-DSA, Algorithm 2 of FIPS 204, over the JWS signing input with the empty context string the RFC requires, which is what OpenSSL does when no signature parameter is set; HashML-DSA is outside the specification and is not offered. Like PureEdDSA there is no pre-hash, so the two share the step that prepares the signing input, renamed from _cjose_jws_build_dig_eddsa now that it serves more than one algorithm family. The signature is the fixed size of the algorithm, which is what EVP_PKEY_get_size reports for ML-DSA rather than an upper bound, so it is also what an attacker-supplied signature is measured against before OpenSSL sees it. An AKP key carries its own algorithm, so the "alg" header has to name that same one: signing an ML-DSA-44 key under an ML-DSA-65 header is refused, in _cjose_jws_validate_verify_key with every other family and again at the point of use. A public-only key is refused before OpenSSL because EVP_DigestSignInit succeeds on one and only the signing itself fails, which is the shape of the EdDSA crash this library has already had once. The tests are the three JOSE examples of RFC 9964 Appendix A.1: each JWS must verify under the public key the RFC prints and yield the RFC's payload, which makes them known-answer tests over signatures this library did not produce. Beside them a sign and verify round trip per algorithm and the negative cases: a public-only signing key, an "alg" naming another ML-DSA algorithm, an ML-DSA header over a key of another type, a JWS verified under the wrong algorithm's key, and a signature of the wrong length or with a byte changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Hans Zandbelt <hans.zandbelt@openidc.com>
|
Do we have anything else already implemented here that is part of a 'proposed' RFC? In general I would like to stick to already approved RFCs than implementing drafts, unless there is some real business need for it. |
|
that's just IETF wording, RFC 7518 is also a "proposed standard", see also https://www.ietf.org/process/rfcs/#statuses |
|
@zandbelt After I merged the PR I noticed that you haven't updated the README file to mention the new algorithms and their OpenSSL requirements. Also an update on the CHANGELOG would be nice. Could you please submit another PR to update them? |
|
Done in #199. On the README: #198 did update it — the JWS The CHANGELOG turned out to need more than ML-DSA. The I also noticed |
Summary
Adds the ML-DSA signature algorithms
ML-DSA-44,ML-DSA-65andML-DSA-87of RFC 9964, and theAKPkey type they sign with. Two commits: the key type, then the algorithms.This is the first thing I have proposed here that is not RFC 7518, so the first question is whether you want it at all. RFC 9964 is a Proposed Standard and all four registrations are live in the IANA JOSE registries, but with an implementation requirement of Optional, and the underlying FIPS 204 needs OpenSSL 3.5 where this project's floor is 3.0. So it is off by default and costs nothing to anyone who does not ask for it. If you would rather cjose stayed on RFC 7518 for now, say so and I will close this without argument.
CJOSE_ENABLE_ML_DSA, OFF by default, and configuration fails with a clear message on OpenSSL below 3.5 rather than leaving it to a link error. With the option off the code is compiled out and the three new functions refuse withCJOSE_ERR_INVALID_ARG, which is howCJOSE_ENABLE_RSA1_5already behaves. They keep their place in the ABI either way, so the export lists do not depend on the option.AKPkey type.kty: "AKP"with the key inpubandpriv, andalgREQUIRED — unlike every other key type, since the key type alone does not say which algorithm the key belongs to (RFC 9964 section 3). For ML-DSA theprivmember is the 32 octet seed, not the expanded private key (section 4), and the 32 octet length check is a MUST (section 7.3). OpenSSL grows the expanded key from the seed, so a JWK round trips through the seed and the expanded form never leaves theEVP_PKEY.pubwhich does not belong to the private key is tampered or mismatched. A supplied seed therefore has its public key derived and compared in constant time rather than being taken on the caller's word, which is what the OKP import already does forxandd. With only a seed,pubis derived, ascjose_jwk_create_OKP_specderivesxfromd._cjose_jws_build_dig_eddsanow that it serves more than one family.CJOSE_JWK_KTY_AKP,cjose_jwk_akp_alg,cjose_jwk_akp_keyspec,cjose_jwk_create_AKP_random,cjose_jwk_create_AKP_spec,cjose_jwk_AKP_get_alg, and the macro-onlyCJOSE_HDR_ALG_ML_DSA_44/65/87.CJOSE_JWK_KTY_AKPis appended tocjose_jwk_kty_t, which is an ABI change and so belongs in the 1.0.x cycle rather than a patch release.Things worth a second opinion
ml-dsa.retain_seed=nowill not hand the seed back, and a key that can sign but can never be serialised is worse.rsa_keydataalready keeps its BIGNUMs for the same round-tripping reason.cjose_jwk_create_AKP_randomgenerates the seed itself rather than asking the provider for it afterwards, for the same reason._cjose_jwk_decode_private_attribute. That helper refuses a private member whose octets are all zero, which is right for RSA and EC where the member is an integer and zero is not a key. An ML-DSAprivis 32 opaque octets of seed, and the all-zeros seed is a good one — it is what RFC 9964 Appendix A.1 uses for all three of its examples. The import does its own presence and length check instead, with a comment saying why, since the obvious tidy-up would break the RFC's own vectors.Testing
The tests are the three JOSE examples of RFC 9964 Appendix A.1. Their seed is all zeros, so the public key is reproducible: each JWK must import and export back to exactly the public key the RFC prints, and each of the RFC's JWSs must verify and yield the RFC's payload. Those are known-answer tests over signatures this library did not produce, not round trips.
Beside them: a random round trip per algorithm; a seed-only spec that must derive the RFC's public key; and the negative cases — a missing, unknown or foreign
alg, a missing, empty or wrongly sizedpubincluding a well formed one of another ML-DSA algorithm, a seed of 31 or 33 octets, a seed that does not matchpub, a public-only signing key, analgnaming another ML-DSA algorithm, an ML-DSA header over a key of another type, a JWS verified under the wrong algorithm's key, and a signature of the wrong length or with a byte changed.-pedantic -Wall -Werrorclean and the fullcheck_cjoserun in three configurations: ML-DSA on (145 checks), off (139) and on together withCJOSE_ENABLE_RSA1_5(144). valgrind over the whole suite: no leaks, no errors. Theclang-formattarget produces no diff. Each commit builds and passes on its own.openssl.ymlturns the option on for the three matrix entries that have ML-DSA (3.5.8, 3.6.4, 4.0.2) and leaves it off below that, so CI actually compiles and exercises the new code. Without that it would not:ubuntu-latestis on OpenSSL 3.0.13, sobuild.ymlcannot enable it and a green run would only prove the OFF path still works. Move it elsewhere if you would rather it lived in another workflow.🤖 Generated with Claude Code