Skip to content

Add PQC (ML-DSA and ML-KEM) support for XML Digital Signature and Enc… - #645

Open
ffang wants to merge 1 commit into
apache:mainfrom
ffang:PQC
Open

Add PQC (ML-DSA and ML-KEM) support for XML Digital Signature and Enc…#645
ffang wants to merge 1 commit into
apache:mainfrom
ffang:PQC

Conversation

@ffang

@ffang ffang commented Aug 7, 2026

Copy link
Copy Markdown

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

…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);
@seanjmullan

Copy link
Copy Markdown
Member

Thank you for this contribution! I plan on reviewing this but will need a couple of weeks.

@ffang

ffang commented Aug 11, 2026

Copy link
Copy Markdown
Author

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

@seanjmullan

Copy link
Copy Markdown
Member

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.

@ffang

ffang commented Aug 12, 2026

Copy link
Copy Markdown
Author

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
Freeman

@Arpan0995

Copy link
Copy Markdown

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:

  • a tampered ML-DSA signature (a flipped byte of SignatureValue) is rejected by verification;
  • an ML-DSA signature checked against the wrong public key fails;
  • ML-KEM decryption with the wrong recipient private key fails cleanly rather than returning wrong plaintext;
  • a truncated or corrupted KEM encapsulation (shorter than encapsulationSize, or a flipped byte) is rejected with a clear exception.

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.

@ffang

ffang commented Aug 17, 2026

Copy link
Copy Markdown
Author

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:

* a tampered ML-DSA signature (a flipped byte of SignatureValue) is rejected by verification;

* an ML-DSA signature checked against the wrong public key fails;

* ML-KEM decryption with the wrong recipient private key fails cleanly rather than returning wrong plaintext;

* a truncated or corrupted KEM encapsulation (shorter than encapsulationSize, or a flipped byte) is rejected with a clear exception.

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
Freeman

"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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's been some talk related to w3c/strategy#484 about getting the namespace sorted so maybe coming soon.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads-up @bc-pi !

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.w3.org/2026/08/xmldsig-more# is looking to be the clubhouse leader

Arpan0995 added a commit to Arpan0995/santuario-xml-security-java that referenced this pull request Aug 21, 2026
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.
@Arpan0995

Copy link
Copy Markdown

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 mvn test -Dtest=XMLSignatureMLDSATest,XMLEncryptionMLKEMTest -P bouncycastle (9 + 9 executions, 0 failures), and skip via the existing assumeTrue guards when BouncyCastle is absent. If you would rather fold them into the split PRs instead, the ML-DSA cases go with the Signature one and the ML-KEM cases with the Encryption one; happy to rebase either way.

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.

5 participants