Add ML-DSA (FIPS 204) post-quantum signature support - #651
Conversation
Adds ML-DSA-44/65/87 XML digital signature support via the JSR-105 API (DOM) and the STAX signature path, wired through JCEMapper and the JSR-105 provider's algorithm URI registrations. Part of the post-quantum work tracked under SANTUARIO-634 (originally proposed in SANTUARIO-633 / apache#645), split out here as the signature-only half per community request. - The ML-DSA test keystore is generated on the fly per test run instead of a committed PKCS12 binary, avoiding the maintenance burden of binary test fixtures. Uses SelfSignedCertGenerator, originally authored by Joze Rihtarsic (unmerged PR apache#617), copied in and extended here with ML-DSA-44/65/87 AlgorithmIdentifier support per his suggestion on apache#645. - Adds negative-test coverage on both the DOM/JSR-105 and STAX paths: a tampered SignatureValue is rejected, and verification against the wrong public key fails. Added per Arpan0995's review feedback on apache#645.
| if (provider == null) { | ||
| String providerId = JCEMapper.getProviderId(); | ||
| if (providerId == null) { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID); |
| if (provider == null) { | ||
| String providerId = JCEMapper.getProviderId(); | ||
| if (providerId == null) { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID); |
| if (providerId == null) { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID); | ||
| } else { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID, providerId); |
| if (providerId == null) { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID); | ||
| } else { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID, providerId); |
| this.signatureAlgorithm = Signature.getInstance(algorithmID, providerId); | ||
| } | ||
| } else { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID, provider); |
| this.signatureAlgorithm = Signature.getInstance(algorithmID, providerId); | ||
| } | ||
| } else { | ||
| this.signatureAlgorithm = Signature.getInstance(algorithmID, provider); |
… sets Converts the tampered-SignatureValue and wrong-public-key rejection tests on both the DOM/JSR-105 and StAX paths from single hardcoded ML-DSA-65 cases to @ParameterizedTest/@CsvSource across ML-DSA-44/65/87, matching the style of the existing sign-and-verify tests. The StAX helper takes the signature and key algorithms as parameters instead of hardcoding ML-DSA-65.
Parameterize the negative signature tests across all ML-DSA parameter sets
|
While adding inbound-path coverage after the rebase, I found that StAX verification of an ML-DSA signature is not exercised by the current tests, and driving it revealed a round-trip issue with the KeyValue key identifier. Everything below was reproduced on this branch. The issue. With <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Id="..."><dsig:KeyValue/></dsig:KeyInfo>Feeding that document back through the library's own inbound side ( Root cause. The inbound side of this PR is otherwise sound. Repeating the same round trip with Why the tests do not catch it: Two possible fixes, in order of preference:
Happy to contribute the inbound-path tests either way (valid round trip, tampered signature, wrong key, all through |
Hi @Arpan0995 , Agree that "Emit DEREncodedKeyValue " is better and please go this way.
|
… form StAX signing with the KeyValue key identifier dispatched on the public key algorithm with branches for RSA, DSA and EC only. For any other key type (ML-DSA, EdDSA) it fell through and emitted an empty <dsig:KeyValue/>, which the inbound processor then rejects at schema validation before reaching signature verification. Emit a dsig11:DEREncodedKeyValue holding the DER SubjectPublicKeyInfo for those key types, which is schema-valid inside dsig:KeyValue via its ##other wildcard and mirrors the DOM DEREncodedKeyValue support. Adds a parameterized test across ML-DSA-44/65/87 asserting the KeyValue carries a DEREncodedKeyValue that round-trips to the signer's public key and verifies the signature. Inbound extraction of a public key from DEREncodedKeyValue is not added here; the StAX inbound path has no DEREncodedKeyValue support for any key type yet.
|
@ffang Done: the DEREncodedKeyValue fix is up as ffang#4 into this branch, with a parameterized test across ML-DSA-44/65/87. As noted there, it makes the KeyValue schema-valid and the embedded key recoverable; full StAX-inbound round-trip additionally needs inbound DEREncodedKeyValue extraction, which I am happy to follow up with separately. |
Emit dsig11:DEREncodedKeyValue for keys without a structured KeyValue form
The StAX inbound processor resolved a KeyValue only through its RSA, DSA and EC forms, so a dsig11:DEREncodedKeyValue (the KeyValue form for key types without a structured element, such as ML-DSA) failed at key resolution with "No or unsupported key in KeyValue" even though the outbound side now emits it. Add DEREncodedKeyValueSecurityToken, which rebuilds the public key from the DER SubjectPublicKeyInfo by trying the same key types as the DOM DEREncodedKeyValue, and resolve it from both placements: nested inside ds:KeyValue (as emitted) and as a direct ds:KeyInfo child (the XML Signature 1.1 placement). With this a StAX-signed ML-DSA document verifies through the StAX inbound path with no out-of-band key. Adds StaxMLDSAKeyValueInboundTest covering both placements (asserting the resolved key is the signer's) and tampered signature rejection, parameterized across ML-DSA-44/65/87; all nine cases fail without the factory changes.
buildPublicKey() caught only NoSuchAlgorithmException and InvalidKeySpecException, but some providers (BouncyCastle's XDH/EdDSA KeyFactorySpi) throw an unchecked ArrayIndexOutOfBoundsException for malformed or short input rather than InvalidKeySpecException. Because the DEREncodedKeyValue content is untrusted, attacker-controlled inbound data, that exception propagated out of processInMessage instead of being rejected cleanly. Also catch RuntimeException in the key-type loop so a malformed encoding falls through to a clean stax.unsupportedKeyValue rejection, and add a garbage-content case to StaxMLDSAKeyValueInboundTest (parameterized across ML-DSA-44/65/87) that fails without this change.
The DOM DEREncodedKeyValue#getPublicKey() has the same narrow exception handling as the StAX token fixed in the previous commit: it caught only NoSuchAlgorithmException and InvalidKeySpecException while iterating the supported key types, so an unchecked exception from a KeyFactorySpi (BouncyCastle 1.85's XDH/EdDSA throw ArrayIndexOutOfBoundsException for malformed or short input) propagated out instead of a clean rejection. Because DEREncodedKeyValueResolver is a default KeyResolver and a DEREncodedKeyValue in an inbound document is untrusted, attacker-controlled content, this could crash key resolution reached via KeyInfo#getPublicKey() with an uncontrolled runtime exception. Catch RuntimeException in the loop so a malformed encoding falls through to the declared XMLSecurityException. Adds a test that fails without the fix (BouncyCastle at first provider position, skipped otherwise).
Resolve dsig11:DEREncodedKeyValue on the StAX inbound path
Adds ML-DSA-44/65/87 XML digital signature support via the JSR-105 API (DOM) and the STAX signature path, wired through JCEMapper and the JSR-105 provider's algorithm URI registrations. Part of the post-quantum work tracked under SANTUARIO-634 (originally proposed in SANTUARIO-633 / #645), split out here as the signature-only half per community request.