Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(signature): ML-DSA-44/65/87 and the AKP key type via OpenSSL 3.5 (RFC 9964) - #735

Merged
Spomky merged 1 commit into
4.3.xfrom
feat/ml-dsa
Sep 13, 2026
Merged

feat(signature): ML-DSA-44/65/87 and the AKP key type via OpenSSL 3.5 (RFC 9964)#735
Spomky merged 1 commit into
4.3.xfrom
feat/ml-dsa

Conversation

@Spomky

@Spomky Spomky commented Sep 13, 2026

Copy link
Copy Markdown
Member

Closes #723

What

RFC 9964 registers ML-DSA (FIPS 204) for JOSE — ML-DSA-44, ML-DSA-65, ML-DSA-87, the AKP key type and its pub/priv parameters. First post-quantum signature in the registries; the library had nothing for it. The design mirrors the COSE side implemented in web-auth/cose-lib#223.

Plumbing — Jose\Component\Core\Util\AKPKey (@internal)

  • FIPS 204 sizes, RFC 9881 OIDs.
  • checkKey() — RFC 9964 §7.3 before OpenSSL sees the key: kty, alg present and equal, pub of the parameter set (1312/1952/2592), priv exactly the 32-byte seed (the expanded key is rejected), and pub = expansion of the seed (hash_equals, §7.4).
  • generate($alg, ?$seed), publicKeyFromSeed(), sign()/verify() with digest 0.
  • PEM forms of RFC 9881: seed-only PrivateKeyInfo (seed [0]), SubjectPublicKeyInfo. loadFromPEM() also reads the both [2] choice and refuses expandedKey [1] alone.
  • Platform gate: supportsOpenSSL() = PHP ≥ 8.4 and a runtime probe (loading the all-zero-seed key of appendix A, cached for the process). OPENSSL_VERSION_TEXT is the compile-time header — the machine this was developed on reports 3.0.13 while running 3.5.5, so the probe is not theoretical.

Algorithms — MLDSA + MLDSA44/65/87

isSupported(); the constructor throws MissingDependencyException naming the missing piece (PHP version or OpenSSL library); key check before OpenSSL; signature length checked before any computation (false, never an exception); sign() refuses a key without priv.

Keys, converter, analyzer, console

  • JWK::thumbprint(): AKP case over alg, kty, pub (§6); an AKP key without alg throws. toPublic() strips priv.
  • JWKFactory::mldsa($alg, $values) (+ JWKFactoryInterface, unreleased): random seed, or rebuilt from values['priv']. No createMLDSAKey() static: the static creators are all deprecated in 4.3.
  • KeyConverter: an ML-DSA PEM is intercepted before OpenSSL (a public key loads on any platform); a certificate whose key OpenSSL refuses has its SPKI read by ASN.1. A certificate signed with ML-DSA is not loadable (pki-framework 1.6), documented.
  • MLDSAKeyAnalyzer; key:generate:mldsa, keyset:generate:mldsa.

Bundle

signature_mldsa.php loaded when MLDSA44::isSupported() (aliases ML-DSA-44/65/87); analyzer and commands wired. Container compiles on both sides.

CI

ml-dsa: yes/no per matrix entry → JOSE_ML_DSA_EXPECTED, asserted by MLDSASignatureTest; new tests_without_ml_dsa job (pinned ubuntu-24.04 + setup-php 8.4 on OpenSSL 3.0, expects no) so a runner drifting to 3.5 fails instead of silently losing the coverage.

Acceptance criteria

  • Round-trip for the three parameter sets, compact/flattened/general ✔
  • The three JWS of appendix A.1 verify with the printed JWK; the all-zero seed expands to the printed pub; each kid is thumbprint('sha256')
  • NIST ACVP: 30 keyGen seed → pk cases, 8 pure-mode empty-context sigGen cases ✔
  • Malformed keys (priv ≠ 32, wrong pub length, missing/other alg, pub ≠ seed) rejected before OpenSSL; 2419-byte or tampered signature is false
  • PHP 8.3: isSupported() false, constructor throws, bundle compiles, suite green with JOSE_ML_DSA_EXPECTED=no (112 skips) ✔ — the OpenSSL 3.0 side runs in the new job.

Fixtures vendored from cose-lib with provenance (tests/fixtures/rfc9964/, tests/fixtures/nist-acvp/ml-dsa/); appendix-a.json trimmed to its JOSE half.

Backward compatibility

Reflection dump of the public API vs 4.3.x: 44 additions, no removal or change. 48 behavioural probes (thumbprints of the four existing key types, toPublic(), every key and certificate of the test fixtures, error paths): identical, except that toPublic() now strips a priv member on any key type — deliberate (priv is a registered private parameter), flagged for review.

Also in this PR: RFC citations removed from runtime messages (exception/deprecation/analyzer/console texts), including three merged in #732/#734; references stay in the docblocks.

QA

PHPUnit (8.3, 8.5), ECS, Rector, Deptrac, PHPStan green; PHPStan baseline unchanged.

Out of scope

HashML-DSA, non-empty ctx, the expanded private key as priv, ML-KEM/HPKE, SLH-DSA.

Documentation: web-token/jwt-doc.

… (RFC 9964)

RFC 9964 registers ML-DSA (FIPS 204) for JOSE: the "ML-DSA-44",
"ML-DSA-65" and "ML-DSA-87" algorithms, the "AKP" key type and its
"pub" and "priv" parameters. It is the first post-quantum signature in
the JOSE registries and the library had nothing for it.

AKPKey carries the plumbing, as OKPKey does for the Edwards curves: the
sizes of FIPS 204, the checks of RFC 9964 section 7.3 run before OpenSSL
sees a key ("alg" present and equal, "pub" of the parameter set, "priv"
the 32-byte seed and nothing else, "pub" the key the seed expands to),
key generation from a fresh or a stored seed, the PEM forms of RFC 9881
(seed-only PrivateKeyInfo, SubjectPublicKeyInfo) and their loading, the
digest-less openssl_sign()/openssl_verify(). MLDSA and its three
subclasses implement SignatureAlgorithm on top of it; a signature of
the wrong length is false before any computation.

The computation is OpenSSL's and needs two things, both checked at
runtime: PHP 8.4, the first version whose openssl_sign() accepts a null
digest, and an OpenSSL library that provides ML-DSA (3.5). The second
cannot be read from OPENSSL_VERSION_TEXT, which reports the headers PHP
was built against and not the library it loaded - this very machine
reports 3.0.13 while running 3.5.5 - so the gate loads the all-zero-seed
key of RFC 9964 appendix A once per process. isSupported() tells, the
constructor throws a MissingDependencyException naming the missing
piece, and the bundle registers the algorithms only when they can run.

JWK::thumbprint() hashes "alg", "kty" and "pub" for an AKP key, which
the RFC 7638 member list would have silently dropped, and refuses a key
without "alg"; toPublic() strips "priv". JWKFactory::mldsa() generates
or rebuilds a key from its seed. KeyConverter intercepts an ML-DSA PEM
before OpenSSL, so that a public key loads on any platform, and reads
the SubjectPublicKeyInfo of a certificate whose key OpenSSL refuses.
MLDSAKeyAnalyzer checks the key structure; key:generate:mldsa and
keyset:generate:mldsa generate keys.

The fixtures of RFC 9964 appendix A.1, of the NIST ACVP FIPS 204 files
and of the OpenSSL command line come from web-auth/cose-lib, where the
COSE side of the same RFC was implemented. The CI matrix says which
side of the gate each job expects through JOSE_ML_DSA_EXPECTED, and a
new job runs PHP 8.4 on the OpenSSL 3.0 of ubuntu-24.04 for the other
side.

Backward compatibility was verified: the reflection dump of the public
API only shows additions, and the behavioural probes run against 4.3.x
are identical, except that toPublic() now strips a "priv" member on any
key type. Verified on PHP 8.3 (no ML-DSA) and 8.5.

Closes #723
Spomky added a commit to web-token/jwt-doc that referenced this pull request Sep 13, 2026
web-token/jwt-framework#735 adds the ML-DSA-44/65/87 algorithms and the
AKP key type. The key type and algorithm tables gain the entries, the
signature algorithms page gets a section with the key shape (seed as
"priv", "alg" required), the checks and the runtime platform
requirement behind isSupported(), the key factory page the AKP
generation, the seed form and the PEM forms the loader reads, the
console page the two generator commands, the prerequisites and the
README the requirement, and the 4.2 → 4.3 guide the feature entry.
@Spomky Spomky added this to the 4.3.0 milestone Sep 13, 2026
@Spomky Spomky self-assigned this Sep 13, 2026
@Spomky
Spomky merged commit a65c67b into 4.3.x Sep 13, 2026
18 checks passed
@Spomky
Spomky deleted the feat/ml-dsa branch September 13, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(signature): ML-DSA-44/65/87 and the AKP key type via OpenSSL 3.5 (RFC 9964)

1 participant