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 |
…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.