diff --git a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java
index 3368c97a3..10b96bfee 100644
--- a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java
+++ b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java
@@ -223,6 +223,7 @@ public enum DIRECTION {
public static final QName TAG_dsig11_ECParameters = new QName(NS_DSIG11, "ECParameters", PREFIX_DSIG11);
public static final QName TAG_dsig11_NamedCurve = new QName(NS_DSIG11, "NamedCurve", PREFIX_DSIG11);
public static final QName TAG_dsig11_PublicKey = new QName(NS_DSIG11, "PublicKey", PREFIX_DSIG11);
+ public static final QName TAG_dsig11_DEREncodedKeyValue = new QName(NS_DSIG11, "DEREncodedKeyValue", PREFIX_DSIG11);
public static final String NS_C14N_EXCL = "http://www.w3.org/2001/10/xml-exc-c14n#";
public static final String NS_XMLDSIG_FILTER2 = "http://www.w3.org/2002/06/xmldsig-filter2";
diff --git a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityUtils.java b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityUtils.java
index ec4a9f6ae..3c0aafd80 100644
--- a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityUtils.java
+++ b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityUtils.java
@@ -239,6 +239,18 @@ public static void createKeyValueTokenStructure(AbstractOutputProcessor abstract
abstractOutputProcessor.createCharactersAndOutputAsEvent(outputProcessorChain, XMLUtils.encodeToString(ECDSAUtils.encodePoint(ecPublicKey.getW(), ecPublicKey.getParams().getCurve())));
abstractOutputProcessor.createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig11_PublicKey);
abstractOutputProcessor.createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig11_ECKeyValue);
+ } else {
+ // Key types without a structured KeyValue form (e.g. ML-DSA, EdDSA) are carried as a
+ // dsig11:DEREncodedKeyValue holding the DER SubjectPublicKeyInfo, which is schema-valid
+ // inside dsig:KeyValue via its ##other wildcard. Without this, such a key produced an
+ // empty that the inbound processor rejects at schema validation.
+ byte[] encoded = publicKey.getEncoded();
+ if (encoded == null) {
+ throw new XMLSecurityException("stax.unsupportedKeyValue");
+ }
+ abstractOutputProcessor.createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig11_DEREncodedKeyValue, false, null);
+ abstractOutputProcessor.createCharactersAndOutputAsEvent(outputProcessorChain, XMLUtils.encodeToString(encoded));
+ abstractOutputProcessor.createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig11_DEREncodedKeyValue);
}
abstractOutputProcessor.createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig_KeyValue);
diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueTest.java
new file mode 100644
index 000000000..b1e1a9396
--- /dev/null
+++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueTest.java
@@ -0,0 +1,146 @@
+/**
+ * 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.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+
+import org.apache.xml.security.signature.XMLSignature;
+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.Constants;
+import org.apache.xml.security.utils.XMLUtils;
+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;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+/**
+ * Tests that ML-DSA StAX signing with the {@code KeyValue} key identifier emits a
+ * schema-valid {@code dsig11:DEREncodedKeyValue} carrying the DER SubjectPublicKeyInfo,
+ * rather than an empty {@code }. ML-DSA (and other key types without a
+ * structured KeyValue form) have no RSA/DSA/EC-style KeyValue child, so the DER encoding
+ * is the schema-valid way to carry the key inline.
+ */
+class StaxMLDSAKeyValueTest extends AbstractSignatureCreationTest {
+
+ private static final String NS_DSIG11 = "http://www.w3.org/2009/xmldsig11#";
+ 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 testKeyValueEmitsDerEncodedKeyValue(String sigAlgorithm, String jcaAlgorithm) throws Exception {
+ Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm),
+ "ML-DSA requires BouncyCastle 1.81+");
+
+ KeyPair kp = keyPairs.get(jcaAlgorithm);
+ Document document = signWithKeyValue(sigAlgorithm, kp);
+
+ // The KeyValue must not be empty (the pre-fix behavior) and must contain a DEREncodedKeyValue.
+ NodeList keyValues = document.getElementsByTagNameNS(Constants.SignatureSpecNS, "KeyValue");
+ Assertions.assertEquals(1, keyValues.getLength(), "Expected exactly one KeyValue");
+ Element keyValue = (Element) keyValues.item(0);
+
+ NodeList der = keyValue.getElementsByTagNameNS(NS_DSIG11, "DEREncodedKeyValue");
+ Assertions.assertEquals(1, der.getLength(),
+ "KeyValue must carry a dsig11:DEREncodedKeyValue for ML-DSA, not be empty");
+
+ // The DER content must decode back to the signer's public key.
+ byte[] encoded = Base64.getMimeDecoder().decode(der.item(0).getTextContent());
+ KeyFactory kf = KeyFactory.getInstance(jcaAlgorithm, "BC");
+ PublicKey recovered = kf.generatePublic(new X509EncodedKeySpec(encoded));
+ Assertions.assertArrayEquals(kp.getPublic().getEncoded(), recovered.getEncoded(),
+ "DEREncodedKeyValue must round-trip to the signer's public key");
+
+ // The recovered key must verify the signature, proving the embedded key is usable.
+ // Register the signed element's Id so the same-document Reference resolves on the re-parsed DOM.
+ NodeList signed = document.getElementsByTagNameNS("urn:example:po", "PaymentInfo");
+ for (int i = 0; i < signed.getLength(); i++) {
+ Element e = (Element) signed.item(i);
+ if (e.hasAttributeNS(null, "Id")) {
+ e.setIdAttributeNS(null, "Id", true);
+ }
+ }
+ Element sigElement = (Element) document.getElementsByTagNameNS(
+ Constants.SignatureSpecNS, "Signature").item(0);
+ XMLSignature signature = new XMLSignature(sigElement, "");
+ Assertions.assertTrue(signature.checkSignatureValue(recovered),
+ "Signature must verify under the key recovered from DEREncodedKeyValue");
+ }
+
+ private Document signWithKeyValue(String sigAlgorithm, KeyPair kp) throws Exception {
+ XMLSecurityProperties properties = new XMLSecurityProperties();
+ List actions = new ArrayList<>();
+ actions.add(XMLSecurityConstants.SIGNATURE);
+ properties.setActions(actions);
+ properties.setSignatureKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyValue);
+ properties.setSignatureAlgorithm(sigAlgorithm);
+ 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);
+ try (InputStream is = new ByteArrayInputStream(output)) {
+ return XMLUtils.read(is, false);
+ }
+ }
+}