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); + } + } +}