Add PQC (ML-DSA and ML-KEM) support for XML Digital Signature and Enc… - #645
Add PQC (ML-DSA and ML-KEM) support for XML Digital Signature and Enc…#645ffang wants to merge 1 commit into
Conversation
…ryption - ML-DSA (FIPS 204): signature algorithm constants for ML-DSA-44/65/87, JCA mappings and whitelist URIs in XMLSignature, DOM and JSR-105 support. - ML-KEM (FIPS 203): key transport support using the W3C "XML Security: Generic Hybrid Cipher" structure (https://www.w3.org/TR/xmlsec-generic-hybrid/, closing SANTUARIO-633): KeyUtils.kemEncapsulate/kemDecapsulate perform real javax.crypto.KEM (JEP 452) encapsulation/decapsulation plus HKDF key derivation, with the AES-KeyWrap output and KEM encapsulation concatenated into CipherValue. New ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID top-level algorithm and GenericHybridCipherMethod/KeyEncapsulationMethod/ DataEncapsulationMethod XML structure, wired into both the DOM XMLCipher and STAX (XMLEncryptOutputProcessor/XMLEncryptedKeyInputHandler) paths. - Both features require BouncyCastle 1.84+ and skip gracefully without it; ML-KEM additionally requires Java 21+ for javax.crypto.KEM, accessed via reflection so the module still compiles under its Java 11 target. Algorithm URIs follow the RFC 9231 provisional naming pattern under http://www.w3.org/tbd# per draft-eastlake-rfc9231bis-xmlsec-uris, pending final URI assignment.
| 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); |
| KeyUtils.KemDecapsulation kemResult = KeyUtils.kemDecapsulate( | ||
| (PrivateKey) wrapKeyToken, kemAlgorithm, encryptedBytes, kdp); | ||
| String jceWrapId = JCEMapper.translateURItoJCEID(dataEncapsulationAlgorithm); | ||
| Cipher cipher = Cipher.getInstance(jceWrapId); |
| KeyUtils.KemDecapsulation kemResult = KeyUtils.kemDecapsulate( | ||
| (PrivateKey) wrapKeyToken, kemAlgorithm, encryptedBytes, kdp); | ||
| String jceWrapId = JCEMapper.translateURItoJCEID(dataEncapsulationAlgorithm); | ||
| Cipher cipher = Cipher.getInstance(jceWrapId); |
| KeyUtils.KemEncapsulation kemResult = KeyUtils.kemEncapsulate(pubKey, kemAlgorithm, kdfParams); | ||
| try { | ||
| String jceWrapId = JCEMapper.translateURItoJCEID(dataEncapsulationAlgorithm); | ||
| Cipher wrapCipher = Cipher.getInstance(jceWrapId); |
| KeyUtils.KemEncapsulation kemResult = KeyUtils.kemEncapsulate(pubKey, kemAlgorithm, kdfParams); | ||
| try { | ||
| String jceWrapId = JCEMapper.translateURItoJCEID(dataEncapsulationAlgorithm); | ||
| Cipher wrapCipher = Cipher.getInstance(jceWrapId); |
|
Thank you for this contribution! I plan on reviewing this but will need a couple of weeks. |
Thanks @seanjmullan ! No rush, take your time. Freeman |
|
I like the idea of splitting the PR into 2, one for XML Signature and one for XML Encryption. This will make it easier to focus on one specification at a time. And I think the XML Signature changes should be more straightforward. |
Sure, I will separate the PR and resend. Cheers |
|
I have been looking at post-quantum support across a number of Java crypto libraries and read through this PR with interest. Two choices in the KEM path stood out as the right ones: it takes the encapsulation length from the KEM API (Decapsulator.encapsulationSize()) rather than a hardcoded per-algorithm table, and it derives the key-wrap key through a KDF rather than using the raw shared secret. One gap I noticed while reading the tests. The new PQC tests (XMLSignatureMLDSATest, XMLEncryptionMLKEMTest, StaxMLDSASignatureTest, StaxMLKEMEncryptionTest) all exercise the happy path only, sign-then-verify and encrypt-then-decrypt. For a signature and encryption feature it would be worth adding a few negative cases that lock in the security-relevant behaviour, for example:
These are the cases that would catch a regression in the verify or decapsulate path later, and they are quick to add on top of the round-trip tests already there. Since you are planning to split this into separate Signature and Encryption PRs, the ML-DSA cases would go with the signature side and the ML-KEM cases with the encryption side. Happy to help with any of them if useful. |
Hi @Arpan0995 , Thanks for the review, and glad the KEM length/KDF choices came through as intended. Good catch on the negative test coverage — you're right that the current tests only exercise the happy path. I'll send the split PRs (Signature and Encryption) soon and make sure to include the negative cases you listed: tampered signature rejection, wrong-key verification failure, wrong-key decryption failure, and truncated/corrupted encapsulation rejection. And yes, any help/contribution is welcome — feel free to jump in on either PR once they're up. Cheers |
| "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"; | ||
|
|
||
| // Provisional URIs for ML-DSA (FIPS 204) per draft-eastlake-rfc9231bis-xmlsec-uris | ||
| // section 3.3.15. These use the draft's "tbd" placeholder namespace and will need |
There was a problem hiding this comment.
There's been some talk related to w3c/strategy#484 about getting the namespace sorted so maybe coming soon.
There was a problem hiding this comment.
https://www.w3.org/2026/08/xmldsig-more# is looking to be the clubhouse leader
Covers the four cases discussed on apache#645: tampered signature rejection, wrong-key verification failure, wrong-recipient-key decryption failure, and corrupted KEM encapsulation rejection. All parameterized across ML-DSA-44/65/87 and ML-KEM-512/768/1024.
|
Following up on the negative-test cases: since the split PRs are not up yet, I implemented the four cases against the current PQC branch and opened ffang#1 into it, so they are ready before the split. All four are parameterized across the three parameter sets in the same style as the existing tests, pass locally with |
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.
Adds ML-KEM-512/768/1024 key transport via the W3C "XML Security: Generic Hybrid Cipher" structure, on both the DOM and STAX encryption paths. Part of the post-quantum work tracked under SANTUARIO-634 (originally proposed in SANTUARIO-633 / apache#645), split out here as the encryption-only half per community request. Adds negative-test coverage on both the DOM and STAX paths: decryption with the wrong recipient's ML-KEM private key fails cleanly, and a truncated EncryptedKey CipherValue is rejected via the existing length check in KeyUtils#kemDecapsulate. Added per Arpan0995's review feedback on apache#645.
Hi @Arpan0995, Thanks for going ahead and implementing these — nice catch keeping them parameterized across all three parameter sets, that's cleaner than what I had. FYI, I'd already been working on adding negative-test coverage, and mine covers both the DOM and STAX paths . But your @ParameterizedTest/@CsvSource approach, parameterized across all three parameter sets, is cleaner than the single hardcoded case I used — I'd like to bring that in. The split PRs are up now:
Would you mind rebasing your parameterization against #651/#652 (ML-DSA cases → #651, ML-KEM cases → #652)? Happy to help however's easiest — a PR against my branch, or a patch pasted here. Cheers |
…ryption
ML-DSA (FIPS 204): signature algorithm constants for ML-DSA-44/65/87, JCA mappings and whitelist URIs in XMLSignature, DOM and JSR-105 support.
ML-KEM (FIPS 203): key transport support using the W3C "XML Security: Generic Hybrid Cipher" structure (https://www.w3.org/TR/xmlsec-generic-hybrid/, closing SANTUARIO-633): KeyUtils.kemEncapsulate/kemDecapsulate perform real javax.crypto.KEM (JEP 452) encapsulation/decapsulation plus HKDF key derivation, with the AES-KeyWrap output and KEM encapsulation concatenated into CipherValue. New ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID top-level algorithm and GenericHybridCipherMethod/KeyEncapsulationMethod/ DataEncapsulationMethod XML structure, wired into both the DOM XMLCipher and STAX (XMLEncryptOutputProcessor/XMLEncryptedKeyInputHandler) paths.
Both features require BouncyCastle 1.84+ and skip gracefully without it; ML-KEM additionally requires Java 21+ for javax.crypto.KEM, accessed via reflection so the module still compiles under its Java 11 target.
Algorithm URIs follow the RFC 9231 provisional naming pattern under http://www.w3.org/tbd# per draft-eastlake-rfc9231bis-xmlsec-uris, pending final URI assignment.