Key pairs are generated on the fly in {@code @BeforeAll}; no pre-generated
+ * key material is committed to the repository.
+ *
+ * Run with the Maven {@code bouncycastle} profile on Java 21+ (the {@code javax.crypto.KEM}
+ * API, JEP 452, is used internally via reflection - see {@link KeyUtils#kemEncapsulate}):
+ *
mvn test -Dtest=XMLEncryptionMLKEMTest -P bouncycastle
+ */
+class XMLEncryptionMLKEMTest {
+
+ private static boolean mlKemAvailable;
+ private static boolean bcAddedForTheTest;
+
+ private static java.util.Map keyPairs = new java.util.HashMap<>();
+
+ @BeforeAll
+ static void setUp() {
+ org.apache.xml.security.Init.init();
+
+ if (Security.getProvider("BC") == null) {
+ try {
+ Class> cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
+ Provider bc = (Provider) cls.getConstructor().newInstance();
+ Security.addProvider(bc);
+ bcAddedForTheTest = true;
+ } catch (ReflectiveOperationException e) {
+ mlKemAvailable = false;
+ return;
+ }
+ }
+
+ try {
+ for (String alg : new String[]{"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg, "BC");
+ keyPairs.put(alg, kpg.generateKeyPair());
+ }
+ // javax.crypto.KEM (JEP 452) is only available since Java 21; the KemEncapsulation
+ // helper is used via reflection, so probe it here rather than failing deep inside
+ // encryptKey.
+ Class.forName("javax.crypto.KEM");
+ mlKemAvailable = true;
+ } catch (Exception | LinkageError e) {
+ mlKemAvailable = false;
+ }
+ }
+
+ @AfterAll
+ static void tearDown() {
+ if (bcAddedForTheTest) {
+ Security.removeProvider("BC");
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_512 + ",ML-KEM-512",
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_768 + ",ML-KEM-768",
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_1024 + ",ML-KEM-1024",
+ })
+ void testMLKEMEncryptDecrypt(String keyEncapsulationUri, String jcaAlgorithm) throws Exception {
+ Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)");
+
+ PublicKey pubKey = keyPairs.get(jcaAlgorithm).getPublic();
+ PrivateKey privKey = keyPairs.get(jcaAlgorithm).getPrivate();
+
+ // Build a minimal XML document to encrypt
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ Document doc = dbf.newDocumentBuilder().newDocument();
+ Element root = doc.createElement("PaymentInfo");
+ root.setTextContent("CardNumber:4019111111111111");
+ doc.appendChild(root);
+
+ // Generate a random AES-256 content-encryption key (CEK)
+ KeyGenerator kg = KeyGenerator.getInstance("AES");
+ kg.init(256);
+ SecretKey cek = kg.generateKey();
+
+ // --- ENCRYPT ---
+ // Encapsulate a shared secret to the recipient's ML-KEM public key, derive an
+ // AES-256 key-wrap key from it via HKDF-SHA256, and wrap the CEK with that key.
+ String kwAlgorithm = EncryptionConstants.ALGO_ID_KEYWRAP_AES256;
+ int wrapKeyBitLength = KeyUtils.getAESKeyBitSizeForWrapAlgorithm(kwAlgorithm);
+ HKDFParams kdfParams = HKDFParams.createBuilder(wrapKeyBitLength, XMLSignature.ALGO_ID_MAC_HMAC_SHA256).build();
+ AlgorithmParameterSpec keyEncapsulationParameters =
+ new KeyEncapsulationParameters(keyEncapsulationUri, kdfParams);
+
+ XMLCipher keyCipher = XMLCipher.getInstance(kwAlgorithm);
+ keyCipher.init(XMLCipher.WRAP_MODE, pubKey);
+ EncryptedKey encryptedKey = keyCipher.encryptKey(doc, cek, keyEncapsulationParameters, null);
+
+ // Verify the produced EncryptedKey uses the Generic Hybrid Cipher structure, not an
+ // opaque flat key-transport blob
+ assertEquals(EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID,
+ encryptedKey.getEncryptionMethod().getAlgorithm());
+ assertEquals(keyEncapsulationUri, encryptedKey.getEncryptionMethod().getKeyEncapsulationAlgorithm());
+ assertEquals(kwAlgorithm, encryptedKey.getEncryptionMethod().getDataEncapsulationAlgorithm());
+ assertTrue(encryptedKey.getEncryptionMethod().getKeyEncapsulationKeyLength() > 0);
+
+ // Encrypt the document content with AES-256-GCM
+ XMLCipher dataCipher = XMLCipher.getInstance(XMLCipher.AES_256_GCM);
+ dataCipher.init(XMLCipher.ENCRYPT_MODE, cek);
+ EncryptedData encryptedData = dataCipher.getEncryptedData();
+
+ KeyInfo keyInfo = new KeyInfo(doc);
+ keyInfo.add(encryptedKey);
+ encryptedData.setKeyInfo(keyInfo);
+
+ doc = dataCipher.doFinal(doc, root, false);
+
+ // Serialise to bytes to simulate wire transfer
+ java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
+ javax.xml.transform.Transformer t =
+ javax.xml.transform.TransformerFactory.newInstance().newTransformer();
+ t.transform(new javax.xml.transform.dom.DOMSource(doc),
+ new javax.xml.transform.stream.StreamResult(bos));
+
+ // Verify the serialised XML carries the spec's element names, per
+ // https://www.w3.org/TR/xmlsec-generic-hybrid/ section 6.1 "Key Transport Example"
+ String serialized = bos.toString(java.nio.charset.StandardCharsets.UTF_8);
+ assertTrue(serialized.contains("GenericHybridCipherMethod"), "Missing GenericHybridCipherMethod element");
+ assertTrue(serialized.contains("KeyEncapsulationMethod"), "Missing KeyEncapsulationMethod element");
+ assertTrue(serialized.contains("DataEncapsulationMethod"), "Missing DataEncapsulationMethod element");
+ assertTrue(serialized.contains("http://www.w3.org/2010/xmlsec-ghc#generic-hybrid"),
+ "Missing Generic Hybrid Cipher EncryptionMethod algorithm");
+
+ // --- DECRYPT ---
+ Document encDoc = dbf.newDocumentBuilder()
+ .parse(new java.io.ByteArrayInputStream(bos.toByteArray()));
+
+ Element encDataElem = (Element) encDoc.getElementsByTagNameNS(
+ EncryptionConstants.EncryptionSpecNS, "EncryptedData").item(0);
+
+ XMLCipher decryptCipher = XMLCipher.getInstance();
+ decryptCipher.init(XMLCipher.DECRYPT_MODE, null);
+ EncryptedData encData = decryptCipher.loadEncryptedData(encDoc, encDataElem);
+
+ // Unwrap the CEK using the recipient's ML-KEM private key
+ EncryptedKey ek = encData.getKeyInfo().itemEncryptedKey(0);
+ XMLCipher unwrapCipher = XMLCipher.getInstance();
+ unwrapCipher.init(XMLCipher.UNWRAP_MODE, privKey);
+ Key recoveredCek = unwrapCipher.decryptKey(
+ ek, encData.getEncryptionMethod().getAlgorithm());
+
+ // Decrypt document content
+ decryptCipher.init(XMLCipher.DECRYPT_MODE, recoveredCek);
+ Document decryptedDoc = decryptCipher.doFinal(encDoc, encDataElem);
+
+ Element decryptedRoot = decryptedDoc.getDocumentElement();
+ assertEquals("PaymentInfo", decryptedRoot.getLocalName());
+ assertEquals("CardNumber:4019111111111111", decryptedRoot.getTextContent());
+ }
+}
diff --git a/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java
new file mode 100644
index 000000000..5db2e4a1d
--- /dev/null
+++ b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java
@@ -0,0 +1,117 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.xml.security.test.javax.xml.crypto.dsig;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.Security;
+
+import org.apache.xml.security.signature.XMLSignature;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * Tests for ML-DSA (FIPS 204) XML digital signatures via the
+ * {@code javax.xml.crypto.dsig.XMLSignatureFactory} DOM API.
+ *
+ * The key material is stored in {@code mldsa.p12} (PKCS12, password "security"),
+ * pre-generated with BouncyCastle 1.81+ and committed as a test resource.
+ * The test requires BouncyCastle on the runtime classpath to supply the ML-DSA
+ * JCA provider; compile-time BC classes are deliberately avoided so the default
+ * build (without {@code -P bouncycastle}) still compiles cleanly.
+ *
+ *
Run with the Maven {@code bouncycastle} profile:
+ *
mvn test -Dtest=XMLSignatureMLDSATest -P bouncycastle
+ */
+class XMLSignatureMLDSATest extends XMLSignatureAbstract {
+
+ static final String MLDSA_KS =
+ "src/test/resources/org/apache/xml/security/samples/input/mldsa.p12";
+ static final String MLDSA_KS_PASSWORD = "security";
+ static final String MLDSA_KS_TYPE = "PKCS12";
+
+ private static boolean mlDsaAvailable;
+ private static boolean bcAddedForTheTest;
+ private static KeyStore keyStore;
+
+ @BeforeAll
+ static void setUp() {
+ Security.insertProviderAt(
+ new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI(), 1);
+
+ if (Security.getProvider("BC") == null) {
+ try {
+ Class> cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
+ Provider bc = (Provider) cls.getConstructor().newInstance();
+ Security.addProvider(bc);
+ bcAddedForTheTest = true;
+ } catch (ReflectiveOperationException e) {
+ mlDsaAvailable = false;
+ return;
+ }
+ }
+
+ try {
+ keyStore = KeyStore.getInstance(MLDSA_KS_TYPE);
+ keyStore.load(Files.newInputStream(Path.of(MLDSA_KS)),
+ MLDSA_KS_PASSWORD.toCharArray());
+ // probe that ML-DSA is actually supported by the loaded provider
+ keyStore.getKey("ml-dsa-65", MLDSA_KS_PASSWORD.toCharArray());
+ mlDsaAvailable = true;
+ } catch (Exception e) {
+ mlDsaAvailable = false;
+ }
+ }
+
+ @AfterAll
+ static void tearDown() {
+ if (bcAddedForTheTest) {
+ Security.removeProvider("BC");
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44 + ",ml-dsa-44",
+ XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65 + ",ml-dsa-65",
+ XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87",
+ })
+ void testMLDSASignAndVerify(String signatureAlgorithmURI, String alias) throws Exception {
+ Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+");
+ byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false);
+ Assertions.assertNotNull(signedXml);
+ assertValidSignatureWithJcpApi(signedXml, false);
+ }
+
+ @Override
+ KeyStore getKeyStore() {
+ return keyStore;
+ }
+
+ @Override
+ char[] getKeyPassword() {
+ return MLDSA_KS_PASSWORD.toCharArray();
+ }
+}
diff --git a/src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java b/src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java
new file mode 100644
index 000000000..783943f9c
--- /dev/null
+++ b/src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java
@@ -0,0 +1,273 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.xml.security.test.stax.encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Provider;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import org.apache.xml.security.encryption.EncryptedData;
+import org.apache.xml.security.encryption.EncryptedKey;
+import org.apache.xml.security.encryption.XMLCipher;
+import org.apache.xml.security.keys.KeyInfo;
+import org.apache.xml.security.stax.ext.InboundXMLSec;
+import org.apache.xml.security.stax.ext.OutboundXMLSec;
+import org.apache.xml.security.stax.ext.SecurePart;
+import org.apache.xml.security.stax.ext.XMLSec;
+import org.apache.xml.security.stax.ext.XMLSecurityConstants;
+import org.apache.xml.security.stax.ext.XMLSecurityProperties;
+import org.apache.xml.security.test.stax.utils.StAX2DOM;
+import org.apache.xml.security.test.stax.utils.XMLSecEventAllocator;
+import org.apache.xml.security.test.stax.utils.XmlReaderToWriter;
+import org.apache.xml.security.utils.EncryptionConstants;
+import org.apache.xml.security.utils.XMLUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * StAX-path tests for ML-KEM key transport with AES-256-GCM content encryption, using the W3C
+ * "XML Security: Generic Hybrid Cipher" key transport structure
+ * (https://www.w3.org/TR/xmlsec-generic-hybrid/, see SANTUARIO-633) - the same structure exercised
+ * by the DOM {@code XMLCipher} API in {@code XMLEncryptionMLKEMTest}.
+ */
+class StaxMLKEMEncryptionTest {
+
+ private static boolean mlKemAvailable;
+ private static boolean bcAddedForTheTest;
+ private static final Map keyPairs = new HashMap<>();
+ private final XMLInputFactory xmlInputFactory;
+
+ @BeforeAll
+ static void setUp() {
+ org.apache.xml.security.Init.init();
+ if (Security.getProvider("BC") == null) {
+ try {
+ Class> cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
+ Provider bc = (Provider) cls.getConstructor().newInstance();
+ Security.insertProviderAt(bc, 2);
+ bcAddedForTheTest = true;
+ } catch (ReflectiveOperationException e) {
+ mlKemAvailable = false;
+ return;
+ }
+ }
+ try {
+ for (String alg : new String[]{"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg, "BC");
+ keyPairs.put(alg, kpg.generateKeyPair());
+ }
+ // javax.crypto.KEM (JEP 452) is only available since Java 21
+ Class.forName("javax.crypto.KEM");
+ mlKemAvailable = true;
+ } catch (Exception | LinkageError e) {
+ mlKemAvailable = false;
+ }
+ }
+
+ @AfterAll
+ static void cleanup() {
+ if (bcAddedForTheTest) {
+ Security.removeProvider("BC");
+ }
+ }
+
+ public StaxMLKEMEncryptionTest() throws Exception {
+ org.apache.xml.security.Init.init();
+ xmlInputFactory = XMLInputFactory.newInstance();
+ xmlInputFactory.setEventAllocator(new XMLSecEventAllocator());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_512 + ",ML-KEM-512",
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_768 + ",ML-KEM-768",
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_1024 + ",ML-KEM-1024"
+ })
+ void testMLKEMEncryptDecrypt(String keyEncapsulationUri, String jcaAlgorithm) throws Exception {
+ Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)");
+
+ XMLSecurityProperties properties = new XMLSecurityProperties();
+ List actions = new ArrayList<>();
+ actions.add(XMLSecurityConstants.ENCRYPTION);
+ properties.setActions(actions);
+
+ KeyGenerator keygen = KeyGenerator.getInstance("AES");
+ keygen.init(256);
+ SecretKey cek = keygen.generateKey();
+ properties.setEncryptionKey(cek);
+ properties.setEncryptionSymAlgorithm("http://www.w3.org/2009/xmlenc11#aes256-gcm");
+
+ KeyPair kp = keyPairs.get(jcaAlgorithm);
+ properties.setEncryptionKeyTransportAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID);
+ properties.setEncryptionKeyEncapsulationAlgorithm(keyEncapsulationUri);
+ properties.setEncryptionDataEncapsulationAlgorithm(EncryptionConstants.ALGO_ID_KEYWRAP_AES256);
+ properties.setEncryptionTransportKey(kp.getPublic());
+
+ SecurePart securePart = new SecurePart(
+ new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Element);
+ properties.addEncryptionPart(securePart);
+
+ byte[] output = process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", properties);
+
+ // Verify the produced XML carries the spec's element names, per
+ // https://www.w3.org/TR/xmlsec-generic-hybrid/ section 6.1 "Key Transport Example"
+ String serialized = new String(output, StandardCharsets.UTF_8);
+ assertTrue(serialized.contains("GenericHybridCipherMethod"), "Missing GenericHybridCipherMethod element");
+ assertTrue(serialized.contains("KeyEncapsulationMethod"), "Missing KeyEncapsulationMethod element");
+ assertTrue(serialized.contains("DataEncapsulationMethod"), "Missing DataEncapsulationMethod element");
+ assertTrue(serialized.contains("http://www.w3.org/2010/xmlsec-ghc#generic-hybrid"),
+ "Missing Generic Hybrid Cipher EncryptionMethod algorithm");
+
+ Document document;
+ try (InputStream is = new ByteArrayInputStream(output)) {
+ document = XMLUtils.read(is, false);
+ }
+
+ NodeList nodeList = document.getElementsByTagNameNS("urn:example:po", "PaymentInfo");
+ assertEquals(0, nodeList.getLength());
+
+ nodeList = document.getElementsByTagNameNS("urn:example:po", "CreditCard");
+ assertEquals(0, nodeList.getLength());
+
+ nodeList = document.getElementsByTagNameNS(
+ XMLSecurityConstants.TAG_xenc_EncryptedData.getNamespaceURI(),
+ XMLSecurityConstants.TAG_xenc_EncryptedData.getLocalPart()
+ );
+ assertEquals(1, nodeList.getLength());
+
+ Document decrypted = decryptUsingDOM(document, kp.getPrivate());
+
+ nodeList = decrypted.getElementsByTagNameNS("urn:example:po", "CreditCard");
+ assertEquals(1, nodeList.getLength());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_512 + ",ML-KEM-512",
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_768 + ",ML-KEM-768",
+ EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_1024 + ",ML-KEM-1024"
+ })
+ void testMLKEMStaxEncryptStaxDecrypt(String keyEncapsulationUri, String jcaAlgorithm) throws Exception {
+ Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)");
+
+ XMLSecurityProperties encryptProperties = new XMLSecurityProperties();
+ List actions = new ArrayList<>();
+ actions.add(XMLSecurityConstants.ENCRYPTION);
+ encryptProperties.setActions(actions);
+
+ KeyGenerator keygen = KeyGenerator.getInstance("AES");
+ keygen.init(256);
+ SecretKey cek = keygen.generateKey();
+ encryptProperties.setEncryptionKey(cek);
+ encryptProperties.setEncryptionSymAlgorithm("http://www.w3.org/2009/xmlenc11#aes256-gcm");
+
+ KeyPair kp = keyPairs.get(jcaAlgorithm);
+ encryptProperties.setEncryptionKeyTransportAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID);
+ encryptProperties.setEncryptionKeyEncapsulationAlgorithm(keyEncapsulationUri);
+ encryptProperties.setEncryptionDataEncapsulationAlgorithm(EncryptionConstants.ALGO_ID_KEYWRAP_AES256);
+ encryptProperties.setEncryptionTransportKey(kp.getPublic());
+
+ SecurePart securePart = new SecurePart(
+ new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Element);
+ encryptProperties.addEncryptionPart(securePart);
+
+ byte[] encrypted = process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", encryptProperties);
+
+ XMLSecurityProperties decryptProperties = new XMLSecurityProperties();
+ decryptProperties.setDecryptionKey(kp.getPrivate());
+ InboundXMLSec inboundXMLSec = XMLSec.getInboundWSSec(decryptProperties);
+ XMLStreamReader xmlStreamReader =
+ xmlInputFactory.createXMLStreamReader(new ByteArrayInputStream(encrypted));
+ XMLStreamReader securityStreamReader = inboundXMLSec.processInMessage(xmlStreamReader, null, null);
+
+ Document decrypted = StAX2DOM.readDoc(securityStreamReader);
+
+ NodeList nodeList = decrypted.getElementsByTagNameNS("urn:example:po", "CreditCard");
+ assertEquals(1, nodeList.getLength());
+ }
+
+ private byte[] process(String inputXmlFile, XMLSecurityProperties properties) throws Exception {
+ OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XMLStreamWriter xmlStreamWriter = outboundXMLSec.processOutMessage(baos, StandardCharsets.UTF_8.name());
+ try (InputStream sourceDocument = this.getClass().getClassLoader().getResourceAsStream(inputXmlFile)) {
+ XMLStreamReader xmlStreamReader = null;
+ try {
+ xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument);
+ XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter);
+ return baos.toByteArray();
+ } finally {
+ if (xmlStreamReader != null) {
+ xmlStreamReader.close();
+ }
+ }
+ } finally {
+ xmlStreamWriter.close();
+ }
+ }
+
+ private Document decryptUsingDOM(Document document, Key privateKey) throws Exception {
+ NodeList nodeList = document.getElementsByTagNameNS(
+ XMLSecurityConstants.TAG_xenc_EncryptedData.getNamespaceURI(),
+ XMLSecurityConstants.TAG_xenc_EncryptedData.getLocalPart()
+ );
+ Element ee = (Element) nodeList.item(0);
+
+ XMLCipher cipher = XMLCipher.getInstance();
+ cipher.init(XMLCipher.DECRYPT_MODE, null);
+ EncryptedData encryptedData = cipher.loadEncryptedData(document, ee);
+
+ XMLCipher kwCipher = XMLCipher.getInstance();
+ kwCipher.init(XMLCipher.UNWRAP_MODE, privateKey);
+ KeyInfo ki = encryptedData.getKeyInfo();
+ EncryptedKey encryptedKey = ki.itemEncryptedKey(0);
+ Key symmetricKey = kwCipher.decryptKey(
+ encryptedKey, encryptedData.getEncryptionMethod().getAlgorithm()
+ );
+
+ cipher.init(XMLCipher.DECRYPT_MODE, symmetricKey);
+ return cipher.doFinal(document, ee);
+ }
+}
diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java
new file mode 100644
index 000000000..4798f7126
--- /dev/null
+++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java
@@ -0,0 +1,102 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.xml.security.test.stax.signature;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+
+import org.apache.xml.security.stax.ext.SecurePart;
+import org.apache.xml.security.stax.ext.XMLSecurityConstants;
+import org.apache.xml.security.stax.ext.XMLSecurityProperties;
+import org.apache.xml.security.stax.securityToken.SecurityTokenConstants;
+import org.apache.xml.security.utils.XMLUtils;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.w3c.dom.Document;
+
+/**
+ * StAX-path tests for ML-DSA (FIPS 204) XML digital signatures.
+ */
+class StaxMLDSASignatureTest extends AbstractSignatureCreationTest {
+
+ private static final Map keyPairs = new HashMap<>();
+
+ @BeforeAll
+ static void generateKeys() throws Exception {
+ if (!isBcInstalled()) {
+ return;
+ }
+ try {
+ for (String alg : new String[]{"ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg, "BC");
+ keyPairs.put(alg, kpg.generateKeyPair());
+ }
+ } catch (Exception e) {
+ // ML-DSA not available with this BC version
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "http://www.w3.org/tbd#ml-dsa-44,ML-DSA-44",
+ "http://www.w3.org/tbd#ml-dsa-65,ML-DSA-65",
+ "http://www.w3.org/tbd#ml-dsa-87,ML-DSA-87"
+ })
+ void testMLDSASign(String sigAlgorithm, String jcaAlgorithm) throws Exception {
+ Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm),
+ "ML-DSA requires BouncyCastle 1.81+");
+
+ XMLSecurityProperties properties = new XMLSecurityProperties();
+ List actions = new ArrayList<>();
+ actions.add(XMLSecurityConstants.SIGNATURE);
+ properties.setActions(actions);
+ properties.setSignatureKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyValue);
+ properties.setSignatureAlgorithm(sigAlgorithm);
+
+ KeyPair kp = keyPairs.get(jcaAlgorithm);
+ properties.setSignatureKey(kp.getPrivate());
+ properties.setSignatureVerificationKey(kp.getPublic());
+
+ SecurePart securePart = new SecurePart(
+ new QName("urn:example:po", "PaymentInfo"),
+ SecurePart.Modifier.Content,
+ new String[]{"http://www.w3.org/2001/10/xml-exc-c14n#"},
+ "http://www.w3.org/2001/04/xmlenc#sha256");
+ properties.addSignaturePart(securePart);
+
+ byte[] output = process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", properties, null);
+
+ Document document;
+ try (InputStream is = new ByteArrayInputStream(output)) {
+ document = XMLUtils.read(is, false);
+ }
+
+ verifyUsingDOM(document, kp.getPublic(), properties.getSignatureSecureParts());
+ }
+}
diff --git a/src/test/resources/org/apache/xml/security/samples/input/mldsa.p12 b/src/test/resources/org/apache/xml/security/samples/input/mldsa.p12
new file mode 100644
index 000000000..3c13c9b21
Binary files /dev/null and b/src/test/resources/org/apache/xml/security/samples/input/mldsa.p12 differ