Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dsig:KeyValue/> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <dsig:KeyValue/>}. 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<String, KeyPair> 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<XMLSecurityConstants.Action> 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);
}
}
}