Skip to content

Add RFC 9964 AKP JWK support for ML-DSA keys - #1072

Open
Arpan0995 wants to merge 1 commit into
jwtk:mainfrom
Arpan0995:rfc9964-akp-jwk
Open

Add RFC 9964 AKP JWK support for ML-DSA keys#1072
Arpan0995 wants to merge 1 commit into
jwtk:mainfrom
Arpan0995:rfc9964-akp-jwk

Conversation

@Arpan0995

@Arpan0995 Arpan0995 commented Aug 5, 2026

Copy link
Copy Markdown

This implements the JWK layer for RFC 9964 that I described in #1042: the AKP (Algorithm Key Pair) key type with its pub and priv parameters, and the factory, builder and parser wiring for the ML-DSA parameter sets.

What's included

  • AkpPublicJwk / AkpPrivateJwk API interfaces plus their builder interfaces, mirroring the Octet (OKP) family
  • MlDsaAlgorithm (impl): parameter set identification for keys that have no java.security.interfaces type. Lookup is by JCA algorithm name when the provider reports a specific one (BouncyCastle reports ML-DSA-65), falling back to the algorithm OID in the key's ASN.1 encoding, since the JDK SUN provider reports the generic ML-DSA name for all three sets. This follows the EdwardsCurve.findByKey approach.
  • AkpPublicJwkFactory / AkpPrivateJwkFactory registered in DispatchingJwkFactory, new akpKey / akpKeyPair builder methods, and dispatch from the generic key(PublicKey) / key(PrivateKey) methods
  • JWK thumbprints computed over the RFC 9964 canonical member set (alg, kty, pub)
  • Works natively on JDK 24+ (JEP 497) and via BouncyCastle on earlier JDKs, consistent with how Edwards keys are handled

Design notes

A few decisions worth calling out for review:

  1. priv is the 32-byte seed. RFC 9964 Section 4 requires it ("the priv parameter MUST be the seed and MUST have a length of 32 bytes"). Not every provider retains the seed: JDK 24 through 26 encode ML-DSA private keys as an expanded key only, and the seed cannot be recovered from the expanded form. Creating an AkpPrivateJwk from such a key fails with a clear InvalidKeyException explaining why. Keys parsed from an existing AKP JWK, BouncyCastle keys, and JDK 27+ keys (which switched to the seed encoding) all work. The PKCS#8 parser handles all three ML-DSA-PrivateKey CHOICE alternatives (seed, expandedKey, both); BouncyCastle emits both, JDK 27 emits seed.

  2. Seed import needs an explicit BouncyCastle retry on JDK 24 through 26. Those JDKs have ML-DSA, so the existing NoSuchAlgorithmException-triggered fallback in JcaTemplate never fires, yet their KeyFactory rejects the RFC 9964 seed encoding (InvalidKeySpecException: Cannot parse input). MlDsaAlgorithm retries with BouncyCastle in that case, only when the caller did not specify a provider. I verified the JDK behavior empirically on 24.0.2 and 27-ea, and the BC path on 1.84 (the version this project pins).

  3. Building a private AKP JWK requires the public key. There is no JCA API to derive an ML-DSA public key from a private key, unlike RSA, EC and Edwards keys, so Jwks.builder().key(privateKey).build() alone cannot work. The factory throws an InvalidKeyException directing the caller to Jwks.builder().key(privateKey).publicKey(publicKey).build(), and mismatched parameter sets between the two are rejected.

  4. Strict length validation. Public key material must be exactly 1312, 1952 or 2592 bytes for ML-DSA-44, -65 and -87 respectively, and seeds must be exactly 32 bytes, so malformed JWKs are rejected at parse time rather than surfacing later as provider errors.

Testing

45 new tests across AkpJwksTest and MlDsaAlgorithmTest: round trips for all three parameter sets (build, serialize, parse, and sign with a parsed key), RFC 9964 thumbprint member order, rejection paths (missing or mismatched alg, wrong-length pub and priv, unrecognized kty), and byte-level tests for every PKCS#8 CHOICE alternative and malformed-encoding branch using handcrafted encodings, so they run identically on every JDK. The full suite passes on JDK 17, 21 and 24 locally, and the new and modified files are at 100% method, statement and conditional coverage under the Clover gate.

Scope: this PR is the JWK layer only. Registering ML-DSA-44/65/87 as standard Jwts.SIG algorithms would be the natural follow-up, and I am happy to do that as a separate PR if you want it.

Adds the AKP (Algorithm Key Pair) JWK key type defined by RFC 9964 for
the ML-DSA parameter sets (FIPS 204): API interfaces and builders
mirroring the Octet (OKP) family, family factories registered in
DispatchingJwkFactory, dynamic builder dispatch, and RFC 9964 JWK
thumbprint support (alg, kty, pub).

ML-DSA keys have no java.security.interfaces type, so parameter sets
are identified by JCA algorithm name when the provider reports a
specific one, falling back to the algorithm OID in the key's ASN.1
encoding, following the EdwardsCurve.findByKey approach.

Per RFC 9964, an AKP private JWK's priv value is the 32-byte seed.
PKCS#8 parsing handles all three ML-DSA-PrivateKey CHOICE alternatives
(seed, expandedKey, both).  Seed import falls back to BouncyCastle on
JDK 24 through 26, whose SUN provider accepts only the expandedKey
form.

See issue 1042.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant