From 042d5f7a324c991059f42dfaaff9fe5e01298cbb Mon Sep 17 00:00:00 2001 From: Freeman Fang Date: Fri, 21 Aug 2026 09:49:35 -0400 Subject: [PATCH 01/10] Add ML-DSA (FIPS 204) post-quantum signature support Adds ML-DSA-44/65/87 XML digital signature support via the JSR-105 API (DOM) and the STAX signature path, wired through JCEMapper and the JSR-105 provider's algorithm URI registrations. Part of the post-quantum work tracked under SANTUARIO-634 (originally proposed in SANTUARIO-633 / apache/santuario-xml-security-java#645), split out here as the signature-only half per community request. - The ML-DSA test keystore is generated on the fly per test run instead of a committed PKCS12 binary, avoiding the maintenance burden of binary test fixtures. Uses SelfSignedCertGenerator, originally authored by Joze Rihtarsic (unmerged PR #617), copied in and extended here with ML-DSA-44/65/87 AlgorithmIdentifier support per his suggestion on #645. - Adds negative-test coverage on both the DOM/JSR-105 and STAX paths: a tampered SignatureValue is rejected, and verification against the wrong public key fails. Added per Arpan0995's review feedback on #645. --- .../dom/AbstractDOMSignatureMethod.java | 2 +- .../dsig/internal/dom/DOMSignatureMethod.java | 99 ++++ .../internal/dom/DOMXMLSignatureFactory.java | 8 +- .../xml/security/algorithms/JCEMapper.java | 12 + .../algorithms/SignatureAlgorithm.java | 10 + .../implementations/SignatureMLDSA.java | 208 ++++++++ .../keys/content/DEREncodedKeyValue.java | 1 + .../xml/security/signature/XMLSignature.java | 12 + .../algorithms/PKISignatureAlgorithm.java | 4 +- .../AbstractInboundSecurityToken.java | 10 +- src/main/resources/security-config.xml | 22 + .../xml/crypto/dsig/XMLSignatureAbstract.java | 15 + .../crypto/dsig/XMLSignatureMLDSATest.java | 205 ++++++++ .../signature/StaxMLDSASignatureTest.java | 190 +++++++ .../testutils/SelfSignedCertGenerator.java | 480 ++++++++++++++++++ 15 files changed, 1272 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java create mode 100644 src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java create mode 100644 src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java create mode 100644 src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java index 9728d20aa..9e2f7c0ac 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java @@ -47,7 +47,7 @@ abstract class AbstractDOMSignatureMethod extends DOMStructure implements SignatureMethod { // denotes the type of signature algorithm - enum Type { DSA, RSA, ECDSA, EDDSA, HMAC } + enum Type { DSA, RSA, ECDSA, EDDSA, MLDSA, HMAC } /** * Verifies the passed-in signature with the specified key, using the diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java index 688ab7668..83d8eb179 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java @@ -94,6 +94,16 @@ public abstract class DOMSignatureMethod extends AbstractDOMSignatureMethod { "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519"; static final String ED448 = "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 + // to be updated once final URIs are assigned (see SANTUARIO-634). + static final String ML_DSA_44 = + "http://www.w3.org/tbd#ml-dsa-44"; + static final String ML_DSA_65 = + "http://www.w3.org/tbd#ml-dsa-65"; + static final String ML_DSA_87 = + "http://www.w3.org/tbd#ml-dsa-87"; static final String ECDSA_SHA3_224 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-224"; static final String ECDSA_SHA3_256 = @@ -269,6 +279,12 @@ static SignatureMethod unmarshal(Element smElem) throws MarshalException { return new EDDSA_ED25519(smElem); } else if (alg.equals(ED448)) { return new EDDSA_ED448(smElem); + } else if (alg.equals(ML_DSA_44)) { + return new MLDSA_44(smElem); + } else if (alg.equals(ML_DSA_65)) { + return new MLDSA_65(smElem); + } else if (alg.equals(ML_DSA_87)) { + return new MLDSA_87(smElem); } else { throw new MarshalException ("unsupported SignatureMethod algorithm: " + alg); @@ -1291,4 +1307,87 @@ String getJCAAlgorithm() { return "Ed448"; } } + + abstract static class AbstractMLDSASignatureMethod extends DOMSignatureMethod { + + AbstractMLDSASignatureMethod(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + + AbstractMLDSASignatureMethod(Element dmElem) throws MarshalException { + super(dmElem); + } + + /** ML-DSA signatures are raw bytes; no reformatting needed. */ + @Override + byte[] postSignFormat(Key key, byte[] sig) { + return sig; + } + + /** ML-DSA signatures are raw bytes; no reformatting needed. */ + @Override + byte[] preVerifyFormat(Key key, byte[] sig) { + return sig; + } + + @Override + Type getAlgorithmType() { + return Type.MLDSA; + } + } + + static final class MLDSA_44 extends AbstractMLDSASignatureMethod { + MLDSA_44(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + MLDSA_44(Element dmElem) throws MarshalException { + super(dmElem); + } + @Override + public String getAlgorithm() { + return ML_DSA_44; + } + @Override + String getJCAAlgorithm() { + return "ML-DSA-44"; + } + } + + static final class MLDSA_65 extends AbstractMLDSASignatureMethod { + MLDSA_65(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + MLDSA_65(Element dmElem) throws MarshalException { + super(dmElem); + } + @Override + public String getAlgorithm() { + return ML_DSA_65; + } + @Override + String getJCAAlgorithm() { + return "ML-DSA-65"; + } + } + + static final class MLDSA_87 extends AbstractMLDSASignatureMethod { + MLDSA_87(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + MLDSA_87(Element dmElem) throws MarshalException { + super(dmElem); + } + @Override + public String getAlgorithm() { + return ML_DSA_87; + } + @Override + String getJCAAlgorithm() { + return "ML-DSA-87"; + } + } } diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java index 49f93514c..94074a211 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java @@ -355,7 +355,13 @@ public SignatureMethod newSignatureMethod(String algorithm, return new DOMSignatureMethod.EDDSA_ED25519(params); } else if (algorithm.equals(DOMSignatureMethod.ED448)) { return new DOMSignatureMethod.EDDSA_ED448(params); - }else { + } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_44)) { + return new DOMSignatureMethod.MLDSA_44(params); + } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_65)) { + return new DOMSignatureMethod.MLDSA_65(params); + } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_87)) { + return new DOMSignatureMethod.MLDSA_87(params); + } else { throw new NoSuchAlgorithmException("unsupported algorithm"); } } diff --git a/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java b/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java index 1dc09759d..2826f6597 100644 --- a/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java +++ b/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java @@ -233,6 +233,18 @@ public static void registerDefaultAlgorithms() { XMLSignature.ALGO_ID_SIGNATURE_EDDSA_ED448, new Algorithm("Ed448", "Ed448", "Signature") ); + algorithmsMap.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44, + new Algorithm("ML-DSA-44", "ML-DSA-44", "Signature") + ); + algorithmsMap.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, + new Algorithm("ML-DSA-65", "ML-DSA-65", "Signature") + ); + algorithmsMap.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87, + new Algorithm("ML-DSA-87", "ML-DSA-87", "Signature") + ); algorithmsMap.put( XMLSignature.ALGO_ID_MAC_HMAC_NOT_RECOMMENDED_MD5, new Algorithm("", "HmacMD5", "Mac", 0, 0) diff --git a/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java b/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java index 578e1eb1b..a0ae148a6 100644 --- a/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java +++ b/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java @@ -34,6 +34,7 @@ import org.apache.xml.security.algorithms.implementations.SignatureDSA; import org.apache.xml.security.algorithms.implementations.SignatureECDSA; import org.apache.xml.security.algorithms.implementations.SignatureEDDSA; +import org.apache.xml.security.algorithms.implementations.SignatureMLDSA; import org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.signature.XMLSignature; @@ -513,6 +514,15 @@ public static void registerDefaultAlgorithms() { algorithmHash.put( XMLSignature.ALGO_ID_SIGNATURE_EDDSA_ED448, SignatureEDDSA.SignatureEd448.class ); + algorithmHash.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44, SignatureMLDSA.SignatureMLDSA44.class + ); + algorithmHash.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, SignatureMLDSA.SignatureMLDSA65.class + ); + algorithmHash.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87, SignatureMLDSA.SignatureMLDSA87.class + ); algorithmHash.put( XMLSignature.ALGO_ID_MAC_HMAC_NOT_RECOMMENDED_MD5, IntegrityHmac.IntegrityHmacMD5.class ); diff --git a/src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java b/src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java new file mode 100644 index 000000000..daeef2db0 --- /dev/null +++ b/src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java @@ -0,0 +1,208 @@ +/** + * 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.algorithms.implementations; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.AlgorithmParameterSpec; + +import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.algorithms.SignatureAlgorithmSpi; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.signature.XMLSignatureException; +import org.apache.xml.security.utils.XMLUtils; + +/** + * ML-DSA (FIPS 204) signature algorithm implementation for XML-Dsig. + * Supports ML-DSA-44 (NIST security level 2), ML-DSA-65 (level 3), + * and ML-DSA-87 (level 5). Requires BouncyCastle 1.81+ as the JCA provider. + */ +public abstract class SignatureMLDSA extends SignatureAlgorithmSpi { + + private static final Logger LOG = System.getLogger(SignatureMLDSA.class.getName()); + + private final Signature signatureAlgorithm; + + public SignatureMLDSA() throws XMLSignatureException { + this(null); + } + + public SignatureMLDSA(Provider provider) throws XMLSignatureException { + String algorithmID = JCEMapper.translateURItoJCEID(this.engineGetURI()); + LOG.log(Level.DEBUG, "Created SignatureMLDSA using {0}", algorithmID); + + try { + if (provider == null) { + String providerId = JCEMapper.getProviderId(); + if (providerId == null) { + this.signatureAlgorithm = Signature.getInstance(algorithmID); + } else { + this.signatureAlgorithm = Signature.getInstance(algorithmID, providerId); + } + } else { + this.signatureAlgorithm = Signature.getInstance(algorithmID, provider); + } + } catch (NoSuchAlgorithmException | NoSuchProviderException ex) { + Object[] exArgs = { algorithmID, ex.getLocalizedMessage() }; + throw new XMLSignatureException("algorithms.NoSuchAlgorithm", exArgs); + } + } + + @Override + protected void engineSetParameter(AlgorithmParameterSpec params) throws XMLSignatureException { + try { + this.signatureAlgorithm.setParameter(params); + } catch (InvalidAlgorithmParameterException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected boolean engineVerify(byte[] signature) throws XMLSignatureException { + try { + LOG.log(Level.DEBUG, () -> "Called SignatureMLDSA.verify() on " + XMLUtils.encodeToString(signature)); + return this.signatureAlgorithm.verify(signature); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineInitVerify(Key publicKey) throws XMLSignatureException { + engineInitVerify(publicKey, signatureAlgorithm); + } + + @Override + protected byte[] engineSign() throws XMLSignatureException { + try { + return this.signatureAlgorithm.sign(); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineInitSign(Key privateKey, SecureRandom secureRandom) + throws XMLSignatureException { + engineInitSign(privateKey, secureRandom, this.signatureAlgorithm); + } + + @Override + protected void engineInitSign(Key privateKey) throws XMLSignatureException { + engineInitSign(privateKey, (SecureRandom) null); + } + + @Override + protected void engineUpdate(byte[] input) throws XMLSignatureException { + try { + this.signatureAlgorithm.update(input); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineUpdate(byte input) throws XMLSignatureException { + try { + this.signatureAlgorithm.update(input); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineUpdate(byte[] buf, int offset, int len) throws XMLSignatureException { + try { + this.signatureAlgorithm.update(buf, offset, len); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected String engineGetJCEAlgorithmString() { + return this.signatureAlgorithm.getAlgorithm(); + } + + @Override + protected String engineGetJCEProviderName() { + return this.signatureAlgorithm.getProvider().getName(); + } + + @Override + protected void engineSetHMACOutputLength(int HMACOutputLength) throws XMLSignatureException { + throw new XMLSignatureException("algorithms.HMACOutputLengthOnlyForHMAC"); + } + + @Override + protected void engineInitSign(Key signingKey, AlgorithmParameterSpec algorithmParameterSpec) + throws XMLSignatureException { + throw new XMLSignatureException("algorithms.CannotUseAlgorithmParameterSpecOnEdDSA"); + } + + /** ML-DSA-44 — NIST security level 2. */ + public static class SignatureMLDSA44 extends SignatureMLDSA { + public SignatureMLDSA44() throws XMLSignatureException { + super(); + } + public SignatureMLDSA44(Provider provider) throws XMLSignatureException { + super(provider); + } + @Override + public String engineGetURI() { + return XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44; + } + } + + /** ML-DSA-65 — NIST security level 3. */ + public static class SignatureMLDSA65 extends SignatureMLDSA { + public SignatureMLDSA65() throws XMLSignatureException { + super(); + } + public SignatureMLDSA65(Provider provider) throws XMLSignatureException { + super(provider); + } + @Override + public String engineGetURI() { + return XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65; + } + } + + /** ML-DSA-87 — NIST security level 5. */ + public static class SignatureMLDSA87 extends SignatureMLDSA { + public SignatureMLDSA87() throws XMLSignatureException { + super(); + } + public SignatureMLDSA87(Provider provider) throws XMLSignatureException { + super(provider); + } + @Override + public String engineGetURI() { + return XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87; + } + } +} diff --git a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java index a5c402578..85be2f938 100644 --- a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java +++ b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java @@ -40,6 +40,7 @@ public class DEREncodedKeyValue extends Signature11ElementProxy implements KeyIn private static final String[] supportedKeyTypes = { "RSA", "DSA", "EC", "DiffieHellman", "DH", "XDH", "X25519", "X448", "EdDSA", "Ed25519", "Ed448", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSASSA-PSS"}; /** diff --git a/src/main/java/org/apache/xml/security/signature/XMLSignature.java b/src/main/java/org/apache/xml/security/signature/XMLSignature.java index 8e1fa9e9d..9110db529 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -211,6 +211,18 @@ public final class XMLSignature extends SignatureElementProxy { public static final String ALGO_ID_SIGNATURE_EDDSA_ED448 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"; + /**Signature - ML-DSA-44 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + public static final String ALGO_ID_SIGNATURE_MLDSA_44 = + "http://www.w3.org/tbd#ml-dsa-44"; + + /**Signature - ML-DSA-65 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + public static final String ALGO_ID_SIGNATURE_MLDSA_65 = + "http://www.w3.org/tbd#ml-dsa-65"; + + /**Signature - ML-DSA-87 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + public static final String ALGO_ID_SIGNATURE_MLDSA_87 = + "http://www.w3.org/tbd#ml-dsa-87"; + /**Signature - SHA3-224withECDSA */ public static final String ALGO_ID_SIGNATURE_ECDSA_SHA3_224 = diff --git a/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java b/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java index 5cc5ddbd7..13a406c69 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java +++ b/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java @@ -126,7 +126,7 @@ public byte[] engineSign() throws XMLSecurityException { byte[] jcebytes = signature.sign(); if (this.jceName.contains("ECDSA")) { return ECDSAUtils.convertASN1toXMLDSIG(jcebytes, signIntLen); - } else if (this.jceName.contains("DSA")) { + } else if (this.jceName.contains("DSA") && !this.jceName.startsWith("ML-DSA")) { return JavaUtils.convertDsaASN1toXMLDSIG(jcebytes, 20); } return jcebytes; @@ -152,7 +152,7 @@ public boolean engineVerify(byte[] signature) throws XMLSecurityException { byte[] jcebytes = signature; if (this.jceName.contains("ECDSA")) { jcebytes = ECDSAUtils.convertXMLDSIGtoASN1(jcebytes); - } else if (this.jceName.contains("DSA")) { + } else if (this.jceName.contains("DSA") && !this.jceName.startsWith("ML-DSA")) { jcebytes = JavaUtils.convertDsaXMLDSIGtoASN1(jcebytes, 20); } return this.signature.verify(jcebytes); diff --git a/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java b/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java index 9aacd281d..66d737093 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java +++ b/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java @@ -19,6 +19,7 @@ package org.apache.xml.security.stax.impl.securityToken; import java.security.Key; +import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.DSAKey; import java.security.interfaces.ECKey; @@ -139,6 +140,10 @@ public final Key getSecretKey(String algorithmURI, XMLSecurityConstants.Algorith algorithmSuiteSecurityEvent.setKeyLength(((ECKey) key).getParams().getOrder().bitLength()); } else if (key instanceof SecretKey) { algorithmSuiteSecurityEvent.setKeyLength(key.getEncoded().length * 8); + } else if (key instanceof PrivateKey) { + // PQC or other asymmetric key types (e.g. ML-KEM): key length not classically defined + byte[] encoded = key.getEncoded(); + algorithmSuiteSecurityEvent.setKeyLength(encoded != null ? encoded.length * 8 : 0); } else { throw new XMLSecurityException("java.security.UnknownKeyType", new Object[] {key.getClass().getName()}); @@ -174,8 +179,9 @@ public final PublicKey getPublicKey(String algorithmURI, XMLSecurityConstants.Al } else if (publicKey instanceof ECKey) { algorithmSuiteSecurityEvent.setKeyLength(((ECKey) publicKey).getParams().getOrder().bitLength()); } else { - throw new XMLSecurityException("java.security.UnknownKeyType", - new Object[] {publicKey.getClass().getName()}); + // PQC or other asymmetric public key types (e.g. ML-DSA, ML-KEM): key length not classically defined + byte[] encoded = publicKey.getEncoded(); + algorithmSuiteSecurityEvent.setKeyLength(encoded != null ? encoded.length * 8 : 0); } inboundSecurityContext.registerSecurityEvent(algorithmSuiteSecurityEvent); } diff --git a/src/main/resources/security-config.xml b/src/main/resources/security-config.xml index f6c91db07..dc4a92879 100644 --- a/src/main/resources/security-config.xml +++ b/src/main/resources/security-config.xml @@ -321,6 +321,28 @@ RequiredKey="EC" JCEName="RIPEMD160withECDSA"/> + + + + + + + Key pairs and self-signed certificates are generated on the fly for each of + * ML-DSA-44/65/87 via {@link SelfSignedCertGenerator}, rather than loading a + * pre-generated keystore committed as a binary test resource (see SANTUARIO-634). + * 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 char[] KEY_PASSWORD = "security".toCharArray(); + + 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("PKCS12"); + keyStore.load(null, null); + for (String alias : new String[]{"ml-dsa-44", "ml-dsa-65", "ml-dsa-87"}) { + String jcaAlgorithm = alias.toUpperCase(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaAlgorithm, "BC"); + KeyPair keyPair = kpg.generateKeyPair(); + X509Certificate cert = SelfSignedCertGenerator.generate( + keyPair, jcaAlgorithm, "CN=Test " + jcaAlgorithm + ",O=Apache Santuario,C=US", 365); + keyStore.setKeyEntry(alias, keyPair.getPrivate(), KEY_PASSWORD, new Certificate[]{cert}); + } + 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); + } + + @Test + void testMLDSATamperedSignatureRejected() throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, "ml-dsa-65", false); + + byte[] tamperedXml = flipByteInSignatureValue(signedXml); + + boolean coreValidity = validateSignatureWithJcpApi(tamperedXml, new KeySelectors.RawX509KeySelector()); + Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); + } + + @Test + void testMLDSAWrongPublicKeyRejected() throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, "ml-dsa-65", false); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA-65", "BC"); + PublicKey wrongPublicKey = kpg.generateKeyPair().getPublic(); + + KeySelector wrongKeySelector = new KeySelector() { + @Override + public KeySelectorResult select(KeyInfo keyInfo, Purpose purpose, AlgorithmMethod method, + XMLCryptoContext context) throws KeySelectorException { + return () -> wrongPublicKey; + } + }; + + boolean coreValidity = validateSignatureWithJcpApi(signedXml, wrongKeySelector); + Assertions.assertFalse(coreValidity, "Verification against the wrong public key must not validate"); + } + + /** + * Decodes the <SignatureValue> text content, flips one byte, and re-serializes - + * simulates an attacker (or transport bug) corrupting the signature bytes while leaving + * the rest of the document, including the embedded certificate, intact. + */ + private byte[] flipByteInSignatureValue(byte[] signedXml) throws Exception { + Document doc; + try (ByteArrayInputStream is = new ByteArrayInputStream(signedXml)) { + doc = XMLUtils.read(is, false); + } + NodeList sigValues = doc.getElementsByTagNameNS(Constants.SignatureSpecNS, "SignatureValue"); + Assertions.assertEquals(1, sigValues.getLength(), "Expected exactly one SignatureValue element"); + Element sigValueElement = (Element) sigValues.item(0); + + byte[] sigBytes = Base64.getMimeDecoder().decode(sigValueElement.getTextContent()); + sigBytes[sigBytes.length / 2] ^= (byte) 0xFF; + String tamperedBase64 = Base64.getEncoder().encodeToString(sigBytes); + + // Replace the SignatureValue element's text content in place + NodeList children = sigValueElement.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + sigValueElement.removeChild(children.item(i)); + } + Text newText = doc.createTextNode(tamperedBase64); + sigValueElement.appendChild(newText); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLUtils.outputDOMc14nWithComments(doc, bos); + return bos.toByteArray(); + } + + @Override + KeyStore getKeyStore() { + return keyStore; + } + + @Override + char[] getKeyPassword() { + return KEY_PASSWORD; + } +} 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..3758a16a7 --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java @@ -0,0 +1,190 @@ +/** + * 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.security.PublicKey; +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.api.Test; +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 org.w3c.dom.Text; + +/** + * 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()); + } + + @Test + void testMLDSAStaxTamperedSignatureRejected() throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey("ML-DSA-65"), + "ML-DSA requires BouncyCastle 1.81+"); + + Document document = signWithMLDSA65(); + Element sigElement = tamperSignatureValue(document); + + XMLSignature signature = new XMLSignature(sigElement, ""); + boolean coreValidity = signature.checkSignatureValue(keyPairs.get("ML-DSA-65").getPublic()); + Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); + } + + @Test + void testMLDSAStaxWrongPublicKeyRejected() throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey("ML-DSA-65"), + "ML-DSA requires BouncyCastle 1.81+"); + + Document document = signWithMLDSA65(); + Element sigElement = (Element) document.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature").item(0); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA-65", "BC"); + PublicKey wrongPublicKey = kpg.generateKeyPair().getPublic(); + + XMLSignature signature = new XMLSignature(sigElement, ""); + boolean coreValidity = signature.checkSignatureValue(wrongPublicKey); + Assertions.assertFalse(coreValidity, "Verification against the wrong public key must not validate"); + } + + private Document signWithMLDSA65() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + List actions = new ArrayList<>(); + actions.add(XMLSecurityConstants.SIGNATURE); + properties.setActions(actions); + properties.setSignatureKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyValue); + properties.setSignatureAlgorithm("http://www.w3.org/tbd#ml-dsa-65"); + + KeyPair kp = keyPairs.get("ML-DSA-65"); + 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); + } + } + + /** + * Decodes the <SignatureValue> text content, flips one byte, and writes it back - + * simulates an attacker (or transport bug) corrupting the signature bytes while leaving + * the rest of the document intact. Returns the enclosing <Signature> element. + */ + private Element tamperSignatureValue(Document document) { + NodeList sigValues = document.getElementsByTagNameNS(Constants.SignatureSpecNS, "SignatureValue"); + Assertions.assertEquals(1, sigValues.getLength(), "Expected exactly one SignatureValue element"); + Element sigValueElement = (Element) sigValues.item(0); + + byte[] sigBytes = Base64.getMimeDecoder().decode(sigValueElement.getTextContent()); + sigBytes[sigBytes.length / 2] ^= (byte) 0xFF; + String tamperedBase64 = Base64.getEncoder().encodeToString(sigBytes); + + NodeList children = sigValueElement.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + sigValueElement.removeChild(children.item(i)); + } + Text newText = document.createTextNode(tamperedBase64); + sigValueElement.appendChild(newText); + + return (Element) sigValueElement.getParentNode(); + } +} diff --git a/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java b/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java new file mode 100644 index 000000000..74f08fa43 --- /dev/null +++ b/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java @@ -0,0 +1,480 @@ +/** + * 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.testutils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Set; + +/** + * Generates minimal self-signed X.509 v3 certificates using only public JDK APIs. + * + *

The certificate's DER structure is constructed directly from ASN.1/DER primitives + * and then parsed using CertificateFactory. No BouncyCastle, no sun.security.* internals, + * and no --add-opens flags are required. + * This class is designed to eliminate the need for storing test certificates in a keystore + * or truststore. Instead, the certificates are generated dynamically during test execution. + *

+ *

Supported signature algorithms

+ *
    + *
  • RSA — {@code SHA256withRSA}, {@code SHA384withRSA}, {@code SHA512withRSA}
  • + *
  • ECDSA — {@code SHA256withECDSA}, {@code SHA384withECDSA}, {@code SHA512withECDSA}
  • + *
  • EdDSA — {@code Ed25519}, {@code Ed448} (requires Java 15+)
  • + *
  • ML-DSA (FIPS 204, requires BouncyCastle) — {@code ML-DSA-44}, {@code ML-DSA-65}, {@code ML-DSA-87}
  • + *
+ * + *

Supported DN attributes

+ *
    + *
  • {@code CN} — commonName (UTF8String)
  • + *
  • {@code C} — countryName (PrintableString, two-letter ISO 3166 code)
  • + *
  • {@code O} — organizationName (UTF8String)
  • + *
  • {@code OU} — organizationalUnitName (UTF8String)
  • + *
+ * + *

Limitations

+ *
    + *
  • No X.509 extensions are added (basic-constraints, key-usage, etc.).
  • + *
  • Validity dates use UTCTime, which covers years 2000–2049.
  • + *
+ * + *

These are acceptable constraints for unit and integration tests. + * + *

Adapted from Joze Rihtarsic's {@code SelfSignedCertGenerator} utility contributed in + * https://github.com/apache/santuario-xml-security-java/pull/617, and extended here with + * ML-DSA (FIPS 204) support per his suggestion on SANTUARIO-634 to avoid committing binary + * keystores as test resources. + */ +public final class SelfSignedCertGenerator { + + private SelfSignedCertGenerator() { + } + + // ------------------------------------------------------------------------- + // ASN.1 universal tag constants (ITU-T X.690) + // ------------------------------------------------------------------------- + + private static final int TAG_INTEGER = 0x02; + private static final int TAG_BIT_STRING = 0x03; + private static final int TAG_OID = 0x06; + private static final int TAG_UTF8_STRING = 0x0C; + private static final int TAG_PRINTABLE_STRING = 0x13; + private static final int TAG_UTC_TIME = 0x17; + private static final int TAG_SEQUENCE = 0x30; + private static final int TAG_SET = 0x31; + /** Context-specific constructed [0] tag — used for the TBSCertificate version field. */ + private static final int TAG_CONTEXT_0 = 0xA0; + + // ------------------------------------------------------------------------- + // Pre-built DER encoding constants + // ------------------------------------------------------------------------- + + /** DER encoding of ASN.1 NULL (05 00). */ + private static final byte[] DER_NULL = {0x05, 0x00}; + + /** + * DER encoding of TBSCertificate {@code version} field set to v3 (INTEGER value 2) + * wrapped in an [0] EXPLICIT context tag. + */ + private static final byte[] TBS_VERSION_V3 = { + (byte) TAG_CONTEXT_0, 0x03, (byte) TAG_INTEGER, 0x01, 0x02 + }; + + // ------------------------------------------------------------------------- + // Signature algorithm OID strings + // ------------------------------------------------------------------------- + + /** SHA-256 with RSA Encryption — RFC 4055, OID 1.2.840.113549.1.1.11 */ + private static final String OID_SHA256_WITH_RSA = "1.2.840.113549.1.1.11"; + /** SHA-384 with RSA Encryption — RFC 4055, OID 1.2.840.113549.1.1.12 */ + private static final String OID_SHA384_WITH_RSA = "1.2.840.113549.1.1.12"; + /** SHA-512 with RSA Encryption — RFC 4055, OID 1.2.840.113549.1.1.13 */ + private static final String OID_SHA512_WITH_RSA = "1.2.840.113549.1.1.13"; + /** ECDSA with SHA-256 — RFC 5758, OID 1.2.840.10045.4.3.2 */ + private static final String OID_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2"; + /** ECDSA with SHA-384 — RFC 5758, OID 1.2.840.10045.4.3.3 */ + private static final String OID_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3"; + /** ECDSA with SHA-512 — RFC 5758, OID 1.2.840.10045.4.3.4 */ + private static final String OID_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4"; + /** Ed25519 — RFC 8410, OID 1.3.101.112 */ + private static final String OID_ED25519 = "1.3.101.112"; + /** Ed448 — RFC 8410, OID 1.3.101.113 */ + private static final String OID_ED448 = "1.3.101.113"; + /** ML-DSA-44 — NIST CSOR / draft-ietf-lamps-dilithium-certificates, OID 2.16.840.1.101.3.4.3.17 */ + private static final String OID_ML_DSA_44 = "2.16.840.1.101.3.4.3.17"; + /** ML-DSA-65 — NIST CSOR / draft-ietf-lamps-dilithium-certificates, OID 2.16.840.1.101.3.4.3.18 */ + private static final String OID_ML_DSA_65 = "2.16.840.1.101.3.4.3.18"; + /** ML-DSA-87 — NIST CSOR / draft-ietf-lamps-dilithium-certificates, OID 2.16.840.1.101.3.4.3.19 */ + private static final String OID_ML_DSA_87 = "2.16.840.1.101.3.4.3.19"; + + /** + * OIDs whose AlgorithmIdentifier MUST have absent (not NULL) parameters — the EdDSA arc + * (RFC 8410 §6) and the ML-DSA OIDs (draft-ietf-lamps-dilithium-certificates §5.1). + */ + private static final Set NO_PARAMS_ALGORITHMS = Set.of( + OID_ED25519, OID_ED448, OID_ML_DSA_44, OID_ML_DSA_65, OID_ML_DSA_87); + + // ------------------------------------------------------------------------- + // X.500 attribute type OID strings (RFC 4519) + // ------------------------------------------------------------------------- + + /** commonName — OID 2.5.4.3 */ + private static final String OID_COMMON_NAME = "2.5.4.3"; + /** countryName — OID 2.5.4.6 */ + private static final String OID_COUNTRY_NAME = "2.5.4.6"; + /** organizationName — OID 2.5.4.10 */ + private static final String OID_ORGANIZATION_NAME = "2.5.4.10"; + /** organizationalUnitName — OID 2.5.4.11 */ + private static final String OID_ORGANIZATIONAL_UNIT_NAME = "2.5.4.11"; + + // ------------------------------------------------------------------------- + // Pre-encoded DER OID bytes for RDN attribute types + // ------------------------------------------------------------------------- + + private static final byte[] OID_BYTES_CN = encodeOid(OID_COMMON_NAME); + private static final byte[] OID_BYTES_C = encodeOid(OID_COUNTRY_NAME); + private static final byte[] OID_BYTES_O = encodeOid(OID_ORGANIZATION_NAME); + private static final byte[] OID_BYTES_OU = encodeOid(OID_ORGANIZATIONAL_UNIT_NAME); + + /** + * Pre-encoded DER bytes for the {@code AlgorithmIdentifier} of each supported + * signature algorithm. Values are constant per the relevant RFCs; they do not + * depend on the key size or curve, only on the algorithm name. + * + *

RSA and ECDSA algorithms include a trailing {@code NULL} parameters element + * (RFC 4055 §3.2, conventionally also used for ECDSA). EdDSA and ML-DSA algorithms + * omit parameters entirely (RFC 8410; draft-ietf-lamps-dilithium-certificates §5.1). + */ + private static final Map ALG_IDS = Map.ofEntries( + Map.entry("SHA256withRSA", encodeAlgorithmIdentifier(OID_SHA256_WITH_RSA)), + Map.entry("SHA384withRSA", encodeAlgorithmIdentifier(OID_SHA384_WITH_RSA)), + Map.entry("SHA512withRSA", encodeAlgorithmIdentifier(OID_SHA512_WITH_RSA)), + Map.entry("SHA256withECDSA", encodeAlgorithmIdentifier(OID_SHA256_WITH_ECDSA)), + Map.entry("SHA384withECDSA", encodeAlgorithmIdentifier(OID_SHA384_WITH_ECDSA)), + Map.entry("SHA512withECDSA", encodeAlgorithmIdentifier(OID_SHA512_WITH_ECDSA)), + Map.entry("Ed25519", encodeAlgorithmIdentifier(OID_ED25519)), + Map.entry("Ed448", encodeAlgorithmIdentifier(OID_ED448)), + Map.entry("ML-DSA-44", encodeAlgorithmIdentifier(OID_ML_DSA_44)), + Map.entry("ML-DSA-65", encodeAlgorithmIdentifier(OID_ML_DSA_65)), + Map.entry("ML-DSA-87", encodeAlgorithmIdentifier(OID_ML_DSA_87))); + + /** + * Generates a self-signed X.509 v3 certificate. + * + * @param keyPair the key pair to certify; the private key signs the TBS structure + * and the public key is embedded in SubjectPublicKeyInfo + * @param signatureAlgorithm JCA algorithm name, e.g. {@code "SHA256withRSA"} or {@code "Ed25519"} + * @param subjectDN distinguished name with supported attributes: CN, C, O, OU, + * e.g. {@code "CN=Test Certificate,O=Acme,C=US"} + * @param validityDays number of days the certificate is valid, starting from now + * @return the signed X.509 certificate + * @throws IllegalArgumentException if {@code signatureAlgorithm} is not in the supported set + */ + public static X509Certificate generate(KeyPair keyPair, + String signatureAlgorithm, + String subjectDN, + int validityDays) throws Exception { + byte[] algId = ALG_IDS.get(signatureAlgorithm); + if (algId == null) { + throw new IllegalArgumentException( + "Unsupported signature algorithm: " + signatureAlgorithm + + ". Supported: " + ALG_IDS.keySet()); + } + + // publicKey.getEncoded() returns the SubjectPublicKeyInfo in X.509/DER format. + byte[] spki = keyPair.getPublic().getEncoded(); + byte[] name = encodeName(subjectDN); + byte[] tbs = buildTbs(algId, name, spki, validityDays); + + Signature signer = Signature.getInstance(signatureAlgorithm); + signer.initSign(keyPair.getPrivate()); + signer.update(tbs); + byte[] sigBytes = signer.sign(); + + // Certificate ::= SEQUENCE { TBSCertificate, AlgorithmIdentifier, BIT STRING } + byte[] certDer = sequence(cat(tbs, algId, bitString(sigBytes))); + + return (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(certDer)); + } + + // ------------------------------------------------------------------------- + // TBSCertificate builder + // ------------------------------------------------------------------------- + + /** + * Builds the DER-encoded TBSCertificate. + * + *

+     * TBSCertificate ::= SEQUENCE {
+     *   version         [0] EXPLICIT INTEGER DEFAULT v1,
+     *   serialNumber    INTEGER,
+     *   signature       AlgorithmIdentifier,
+     *   issuer          Name,
+     *   validity        Validity,
+     *   subject         Name,
+     *   subjectPublicKeyInfo SubjectPublicKeyInfo
+     * }
+     * 
+ */ + private static byte[] buildTbs(byte[] algId, byte[] name, + byte[] spki, int validityDays) { + // Serial: milliseconds since epoch — unique enough for test certs + byte[] serial = integer(BigInteger.valueOf(System.currentTimeMillis())); + byte[] validity = buildValidity(validityDays); + // issuer == subject for self-signed + return sequence(cat(TBS_VERSION_V3, serial, algId, name, validity, name, spki)); + } + + private static byte[] buildValidity(int validityDays) { + Instant notBefore = Instant.now(); + Instant notAfter = notBefore.plusSeconds(validityDays * 86_400L); + return sequence(cat(utcTime(notBefore), utcTime(notAfter))); + } + + // ------------------------------------------------------------------------- + // DN encoding — CN, C, O, OU attributes (RFC 4519) + // ------------------------------------------------------------------------- + + /** + * Encodes a Name containing a single CN attribute. + * + *
+     * Name ::= SEQUENCE OF SET OF SEQUENCE { OID, value }
+     * 
+ */ + private static byte[] encodeName(String dn) { + ByteArrayOutputStream rdns = new ByteArrayOutputStream(); + for (String part : dn.split(",")) { + String trimmed = part.strip(); + int eq = trimmed.indexOf('='); + if (eq < 0) continue; + String key = trimmed.substring(0, eq).strip().toUpperCase(); + String val = trimmed.substring(eq + 1).strip(); + byte[] oidBytes; + byte[] valueBytes; + switch (key) { + case "CN": + oidBytes = OID_BYTES_CN; + valueBytes = tlv(TAG_UTF8_STRING, val.getBytes(StandardCharsets.UTF_8)); + break; + case "C": + oidBytes = OID_BYTES_C; + // countryName uses PrintableString; ISO 3166-1 alpha-2 codes are ASCII + valueBytes = tlv(TAG_PRINTABLE_STRING, val.getBytes(StandardCharsets.US_ASCII)); + break; + case "O": + oidBytes = OID_BYTES_O; + valueBytes = tlv(TAG_UTF8_STRING, val.getBytes(StandardCharsets.UTF_8)); + break; + case "OU": + oidBytes = OID_BYTES_OU; + valueBytes = tlv(TAG_UTF8_STRING, val.getBytes(StandardCharsets.UTF_8)); + break; + default: + continue; // unsupported attribute — skip + } + byte[] rdn = set(sequence(cat(oidBytes, valueBytes))); + rdns.write(rdn, 0, rdn.length); + } + if (rdns.size() == 0) { + // fallback: treat the whole string as a CN value + byte[] cnValue = tlv(TAG_UTF8_STRING, dn.getBytes(StandardCharsets.UTF_8)); + byte[] rdn = set(sequence(cat(OID_BYTES_CN, cnValue))); + rdns.write(rdn, 0, rdn.length); + } + return sequence(rdns.toByteArray()); + } + + // ------------------------------------------------------------------------- + // DER / ASN.1 primitives + // ------------------------------------------------------------------------- + + /** + * Encodes the provided content as an ASN.1 DER SEQUENCE. + * + *

A SEQUENCE in ASN.1 represents an ordered collection of elements + *

The DER tag for a SEQUENCE is 0x30.

+ *
+     * AttributeTypeAndValue ::= SEQUENCE {
+     *   type   OBJECT IDENTIFIER,
+     *   value  DirectoryString
+     * }
+     * 
+ * + *

Thus, for the DN CN=Test, the inner attribute pair is encoded as:

+ * + *
+     * 30 ...                SEQUENCE (AttributeTypeAndValue)
+     *   06 03 55 04 03      OID 2.5.4.3 (commonName)
+     *   0C 04 54 65 73 74   UTF8String "Test"
+     * 
+ * + * @param content the already‑encoded DER content to wrap in a SEQUENCE + * @return the DER‑encoded SEQUENCE (tag 0x30 + length + content) + */ + private static byte[] sequence(byte[] content) { + return tlv(TAG_SEQUENCE, content); + } + + /** + * Encodes the provided content as an ASN.1 DER SET value. + * + *

In ASN.1, a SET represents an unordered collection of elements. Although the + * abstract syntax does not impose ordering, DER requires all elements inside a SET + * to be sorted by their encoded byte values to ensure canonical form.

+ * + *

The DER tag for a SET is 0x31.

+ * + *

Use in X.509:
+ * Within an X.509 Distinguished Name (DN), each RelativeDistinguishedName (RDN) + * is encoded as a SET containing one or more AttributeTypeAndValue structures. + * A DN therefore follows the structure:

+ * + *
+     * Name ::= SEQUENCE OF
+     *            SET OF
+     *              SEQUENCE {
+     *                type   OBJECT IDENTIFIER,   -- e.g., 2.5.4.3 (commonName)
+     *                value  DirectoryString      -- e.g., UTF8String "Test"
+     *              }
+     * 
+ * @param content the already‑encoded DER content to wrap in a SET + * @return the DER-encoded SET (tag 0x31 + length + content) + */ + private static byte[] set(byte[] content) { + return tlv(TAG_SET, content); + } + + private static byte[] integer(BigInteger value) { + // toByteArray() produces two's-complement big-endian; positive integers may + // have a leading 0x00 byte if the MSB would otherwise be set — that is correct + // DER INTEGER encoding for a non-negative number. + return tlv(TAG_INTEGER, value.toByteArray()); + } + + private static byte[] bitString(byte[] value) { + return tlv(TAG_BIT_STRING, cat(new byte[]{0x00}, value)); // 0x00 = zero unused bits + } + + // UTCTime covers 2000–2049 (yy < 50 → 20yy). Sufficient for short-lived test certs. + private static final DateTimeFormatter UTC_TIME_FMT = + DateTimeFormatter.ofPattern("yyMMddHHmmss'Z'").withZone(ZoneOffset.UTC); + + private static byte[] utcTime(Instant instant) { + return tlv(TAG_UTC_TIME, UTC_TIME_FMT.format(instant).getBytes(StandardCharsets.US_ASCII)); + } + + /** + * Encodes a DER TLV (Tag–Length–Value) triplet. + * Lengths up to 65535 bytes are supported; that is sufficient for all key types + * used in practice. + */ + private static byte[] tlv(int tag, byte[] value) { + int len = value.length; + byte[] lenBytes; + if (len < 128) { + lenBytes = new byte[]{(byte) len}; + } else if (len < 256) { + lenBytes = new byte[]{(byte) 0x81, (byte) len}; + } else { + lenBytes = new byte[]{(byte) 0x82, (byte) (len >> 8), (byte) (len & 0xFF)}; + } + byte[] out = new byte[1 + lenBytes.length + len]; + out[0] = (byte) tag; + System.arraycopy(lenBytes, 0, out, 1, lenBytes.length); + System.arraycopy(value, 0, out, 1 + lenBytes.length, len); + return out; + } + + /** + * Concatenates byte arrays. + */ + private static byte[] cat(byte[]... parts) { + int total = 0; + for (byte[] p : parts) { + total += p.length; + } + byte[] buf = new byte[total]; + int pos = 0; + for (byte[] p : parts) { + System.arraycopy(p, 0, buf, pos, p.length); + pos += p.length; + } + return buf; + } + + /** + * Encode oid as certificate algorithm identifier. + * @param oid + * @return + */ + public static byte[] encodeAlgorithmIdentifier(String oid) { + // RFC 8410 §6: all OIDs under arc 1.3.101 (X25519, X448, Ed25519, Ed448) MUST omit parameters. + // draft-ietf-lamps-dilithium-certificates §5.1: ML-DSA OIDs MUST omit parameters. + // RFC 4055 §3.2: RSA signature algorithms MUST include a NULL parameters element. + byte[] params = NO_PARAMS_ALGORITHMS.contains(oid) ? new byte[0] : DER_NULL; + return sequence(cat(encodeOid(oid), params)); + } + + /** + * Endodes all number values to ASN.1/DER encoded bytearray + * @param oid - the value + * @return encoded byte array + */ + public static byte[] encodeOid(String oid) { + String[] parts = oid.split("\\."); + ByteArrayOutputStream body = new ByteArrayOutputStream(); + body.write(40 * Integer.parseInt(parts[0]) + Integer.parseInt(parts[1])); + for (int i = 2; i < parts.length; i++) { + byte[] arc = encodeBase128(Long.parseLong(parts[i])); + body.write(arc, 0, arc.length); + } + return tlv(TAG_OID, body.toByteArray()); + } + + /** + * It encodes a non-negative integer using base-128 (variable-length) encoding, which is the standard + * way ASN.1/DER encodes OID arc values + * @param value the long value + * @return ASN.1/DER encoded value + */ + private static byte[] encodeBase128(long value) { + byte[] stack = new byte[10]; + int count = 0; + do { + stack[count++] = (byte) (value & 0x7F); + value >>= 7; + } while (value > 0); + byte[] result = new byte[count]; + for (int i = 0; i < count; i++) { + result[i] = (byte) (stack[count - 1 - i] | (i < count - 1 ? 0x80 : 0x00)); + } + return result; + } +} From 43b53177aa7e9d4de6b8066399585cfe01b376b5 Mon Sep 17 00:00:00 2001 From: arpansharma Date: Fri, 21 Aug 2026 12:55:12 -0500 Subject: [PATCH 02/10] Parameterize the negative signature tests across all ML-DSA parameter sets Converts the tampered-SignatureValue and wrong-public-key rejection tests on both the DOM/JSR-105 and StAX paths from single hardcoded ML-DSA-65 cases to @ParameterizedTest/@CsvSource across ML-DSA-44/65/87, matching the style of the existing sign-and-verify tests. The StAX helper takes the signature and key algorithms as parameters instead of hardcoding ML-DSA-65. --- .../crypto/dsig/XMLSignatureMLDSATest.java | 25 +++++++++---- .../signature/StaxMLDSASignatureTest.java | 37 ++++++++++++------- 2 files changed, 40 insertions(+), 22 deletions(-) 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 index 46fed4bbb..29b3f0bf6 100644 --- 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 @@ -47,7 +47,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.w3c.dom.Document; @@ -131,10 +130,15 @@ void testMLDSASignAndVerify(String signatureAlgorithmURI, String alias) throws E assertValidSignatureWithJcpApi(signedXml, false); } - @Test - void testMLDSATamperedSignatureRejected() throws Exception { + @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 testMLDSATamperedSignatureRejected(String signatureAlgorithmURI, String alias) throws Exception { Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); - byte[] signedXml = doSignWithJcpApi(XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, "ml-dsa-65", false); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); byte[] tamperedXml = flipByteInSignatureValue(signedXml); @@ -142,12 +146,17 @@ void testMLDSATamperedSignatureRejected() throws Exception { Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); } - @Test - void testMLDSAWrongPublicKeyRejected() throws Exception { + @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 testMLDSAWrongPublicKeyRejected(String signatureAlgorithmURI, String alias) throws Exception { Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); - byte[] signedXml = doSignWithJcpApi(XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, "ml-dsa-65", false); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA-65", "BC"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alias.toUpperCase(), "BC"); PublicKey wrongPublicKey = kpg.generateKeyPair().getPublic(); KeySelector wrongKeySelector = new KeySelector() { 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 index 3758a16a7..0fd45b669 100644 --- 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 @@ -41,7 +41,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.w3c.dom.Document; @@ -109,28 +108,38 @@ void testMLDSASign(String sigAlgorithm, String jcaAlgorithm) throws Exception { verifyUsingDOM(document, kp.getPublic(), properties.getSignatureSecureParts()); } - @Test - void testMLDSAStaxTamperedSignatureRejected() throws Exception { - Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey("ML-DSA-65"), + @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 testMLDSAStaxTamperedSignatureRejected(String sigAlgorithm, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), "ML-DSA requires BouncyCastle 1.81+"); - Document document = signWithMLDSA65(); + Document document = signWith(sigAlgorithm, jcaAlgorithm); Element sigElement = tamperSignatureValue(document); XMLSignature signature = new XMLSignature(sigElement, ""); - boolean coreValidity = signature.checkSignatureValue(keyPairs.get("ML-DSA-65").getPublic()); + boolean coreValidity = signature.checkSignatureValue(keyPairs.get(jcaAlgorithm).getPublic()); Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); } - @Test - void testMLDSAStaxWrongPublicKeyRejected() throws Exception { - Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey("ML-DSA-65"), + @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 testMLDSAStaxWrongPublicKeyRejected(String sigAlgorithm, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), "ML-DSA requires BouncyCastle 1.81+"); - Document document = signWithMLDSA65(); + Document document = signWith(sigAlgorithm, jcaAlgorithm); Element sigElement = (Element) document.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature").item(0); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA-65", "BC"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaAlgorithm, "BC"); PublicKey wrongPublicKey = kpg.generateKeyPair().getPublic(); XMLSignature signature = new XMLSignature(sigElement, ""); @@ -138,15 +147,15 @@ void testMLDSAStaxWrongPublicKeyRejected() throws Exception { Assertions.assertFalse(coreValidity, "Verification against the wrong public key must not validate"); } - private Document signWithMLDSA65() throws Exception { + private Document signWith(String sigAlgorithm, String jcaAlgorithm) throws Exception { XMLSecurityProperties properties = new XMLSecurityProperties(); List actions = new ArrayList<>(); actions.add(XMLSecurityConstants.SIGNATURE); properties.setActions(actions); properties.setSignatureKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyValue); - properties.setSignatureAlgorithm("http://www.w3.org/tbd#ml-dsa-65"); + properties.setSignatureAlgorithm(sigAlgorithm); - KeyPair kp = keyPairs.get("ML-DSA-65"); + KeyPair kp = keyPairs.get(jcaAlgorithm); properties.setSignatureKey(kp.getPrivate()); properties.setSignatureVerificationKey(kp.getPublic()); From 58ef7d8782a9c4d2d629475fae18840f92cc4996 Mon Sep 17 00:00:00 2001 From: arpansharma Date: Fri, 21 Aug 2026 16:40:45 -0500 Subject: [PATCH 03/10] Emit dsig11:DEREncodedKeyValue for keys without a structured KeyValue form StAX signing with the KeyValue key identifier dispatched on the public key algorithm with branches for RSA, DSA and EC only. For any other key type (ML-DSA, EdDSA) it fell through and emitted an empty , which the inbound processor then rejects at schema validation before reaching signature verification. Emit a dsig11:DEREncodedKeyValue holding the DER SubjectPublicKeyInfo for those key types, which is schema-valid inside dsig:KeyValue via its ##other wildcard and mirrors the DOM DEREncodedKeyValue support. Adds a parameterized test across ML-DSA-44/65/87 asserting the KeyValue carries a DEREncodedKeyValue that round-trips to the signer's public key and verifies the signature. Inbound extraction of a public key from DEREncodedKeyValue is not added here; the StAX inbound path has no DEREncodedKeyValue support for any key type yet. --- .../stax/ext/XMLSecurityConstants.java | 1 + .../security/stax/ext/XMLSecurityUtils.java | 12 ++ .../stax/signature/StaxMLDSAKeyValueTest.java | 146 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueTest.java 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); + } + } +} From 5898249237b100c6ba9629ced36b9dd1cb3e373d Mon Sep 17 00:00:00 2001 From: arpansharma Date: Sat, 22 Aug 2026 13:33:12 -0500 Subject: [PATCH 04/10] Resolve dsig11:DEREncodedKeyValue on the StAX inbound path The StAX inbound processor resolved a KeyValue only through its RSA, DSA and EC forms, so a dsig11:DEREncodedKeyValue (the KeyValue form for key types without a structured element, such as ML-DSA) failed at key resolution with "No or unsupported key in KeyValue" even though the outbound side now emits it. Add DEREncodedKeyValueSecurityToken, which rebuilds the public key from the DER SubjectPublicKeyInfo by trying the same key types as the DOM DEREncodedKeyValue, and resolve it from both placements: nested inside ds:KeyValue (as emitted) and as a direct ds:KeyInfo child (the XML Signature 1.1 placement). With this a StAX-signed ML-DSA document verifies through the StAX inbound path with no out-of-band key. Adds StaxMLDSAKeyValueInboundTest covering both placements (asserting the resolved key is the signer's) and tampered signature rejection, parameterized across ML-DSA-44/65/87; all nine cases fail without the factory changes. --- .../DEREncodedKeyValueSecurityToken.java | 95 +++++++ .../SecurityTokenFactoryImpl.java | 20 ++ .../StaxMLDSAKeyValueInboundTest.java | 264 ++++++++++++++++++ 3 files changed, 379 insertions(+) create mode 100644 src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java create mode 100644 src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java diff --git a/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java b/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java new file mode 100644 index 000000000..221b75cc2 --- /dev/null +++ b/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java @@ -0,0 +1,95 @@ +/** + * 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.stax.impl.securityToken; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import org.apache.xml.security.binding.xmldsig11.DEREncodedKeyValueType; +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.stax.ext.InboundSecurityContext; +import org.apache.xml.security.stax.impl.util.IDGenerator; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; + +/** + * Inbound security token for a {@code dsig11:DEREncodedKeyValue}: the DER-encoded + * SubjectPublicKeyInfo of a public key, which is the KeyValue form for key types that have no + * structured KeyValue element (ML-DSA, EdDSA, ...). The StAX counterpart of the DOM + * {@code DEREncodedKeyValue} support; the public key is rebuilt lazily from the encoding by + * trying each supported key type's {@link KeyFactory}. + */ +public class DEREncodedKeyValueSecurityToken extends AbstractInboundSecurityToken { + + // Same key types as the DOM DEREncodedKeyValue.supportedKeyTypes + private static final String[] SUPPORTED_KEY_TYPES = { "RSA", "DSA", "EC", + "DiffieHellman", "DH", "XDH", "X25519", "X448", + "EdDSA", "Ed25519", "Ed448", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", + "RSASSA-PSS"}; + + private final byte[] encodedKey; + + public DEREncodedKeyValueSecurityToken(DEREncodedKeyValueType derEncodedKeyValueType, + InboundSecurityContext inboundSecurityContext) + throws XMLSecurityException { + super(inboundSecurityContext, IDGenerator.generateID(null), SecurityTokenConstants.KeyIdentifier_KeyValue, true); + + byte[] value = derEncodedKeyValueType.getValue(); + if (value == null || value.length == 0) { + throw new XMLSecurityException("stax.unsupportedKeyValue"); + } + this.encodedKey = value.clone(); + } + + private PublicKey buildPublicKey() throws XMLSecurityException { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedKey); + for (String keyType : SUPPORTED_KEY_TYPES) { + try { + PublicKey publicKey = KeyFactory.getInstance(keyType).generatePublic(keySpec); + if (publicKey != null) { + return publicKey; + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { //NOPMD + // Not this key type; try the next one + } + } + throw new XMLSecurityException("stax.unsupportedKeyValue"); + } + + @Override + public PublicKey getPublicKey() throws XMLSecurityException { + if (super.getPublicKey() == null) { + setPublicKey(buildPublicKey()); + } + return super.getPublicKey(); + } + + @Override + public boolean isAsymmetric() { + return true; + } + + @Override + public SecurityTokenConstants.TokenType getTokenType() { + return SecurityTokenConstants.KeyValueToken; + } +} diff --git a/src/main/java/org/apache/xml/security/stax/impl/securityToken/SecurityTokenFactoryImpl.java b/src/main/java/org/apache/xml/security/stax/impl/securityToken/SecurityTokenFactoryImpl.java index 5378e1975..d5d76b299 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/securityToken/SecurityTokenFactoryImpl.java +++ b/src/main/java/org/apache/xml/security/stax/impl/securityToken/SecurityTokenFactoryImpl.java @@ -33,6 +33,7 @@ import org.apache.xml.security.binding.xmldsig.RSAKeyValueType; import org.apache.xml.security.binding.xmldsig.X509DataType; import org.apache.xml.security.binding.xmldsig.X509IssuerSerialType; +import org.apache.xml.security.binding.xmldsig11.DEREncodedKeyValueType; import org.apache.xml.security.binding.xmldsig11.ECKeyValueType; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.stax.ext.InboundSecurityContext; @@ -76,6 +77,17 @@ public InboundSecurityToken getSecurityToken(KeyInfoType keyInfoType, return getSecurityToken(keyValueType, securityProperties, inboundSecurityContext, keyUsage); } + // DEREncodedKeyValue as a direct KeyInfo child, the XML Signature 1.1 placement + // (the nested-in-KeyValue placement is handled in the KeyValue branch above) + final DEREncodedKeyValueType derEncodedKeyValueType = XMLSecurityUtils.getQNameType( + keyInfoType.getContent(), XMLSecurityConstants.TAG_dsig11_DEREncodedKeyValue); + if (derEncodedKeyValueType != null) { + DEREncodedKeyValueSecurityToken token = + new DEREncodedKeyValueSecurityToken(derEncodedKeyValueType, inboundSecurityContext); + setTokenKey(securityProperties, keyUsage, token); + return token; + } + // KeyName final String keyName = XMLSecurityUtils.getQNameType(keyInfoType.getContent(), XMLSecurityConstants.TAG_dsig_KeyName); @@ -165,6 +177,14 @@ private static InboundSecurityToken getSecurityToken(KeyValueType keyValueType, setTokenKey(securityProperties, keyUsage, token); return token; } + final DEREncodedKeyValueType derEncodedKeyValueType = + XMLSecurityUtils.getQNameType(keyValueType.getContent(), XMLSecurityConstants.TAG_dsig11_DEREncodedKeyValue); + if (derEncodedKeyValueType != null) { + DEREncodedKeyValueSecurityToken token = + new DEREncodedKeyValueSecurityToken(derEncodedKeyValueType, inboundSecurityContext); + setTokenKey(securityProperties, keyUsage, token); + return token; + } throw new XMLSecurityException("stax.unsupportedKeyValue"); } diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java new file mode 100644 index 000000000..9abe7116b --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java @@ -0,0 +1,264 @@ +/** + * 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.ByteArrayOutputStream; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +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 javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.xml.security.stax.ext.InboundXMLSec; +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.stax.securityEvent.KeyValueTokenSecurityEvent; +import org.apache.xml.security.stax.securityEvent.SecurityEvent; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.apache.xml.security.test.stax.utils.StAX2DOM; +import org.apache.xml.security.test.stax.utils.XMLSecEventAllocator; +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; +import org.w3c.dom.Text; + +/** + * StAX inbound verification of ML-DSA signatures whose KeyInfo carries the public key as a + * {@code dsig11:DEREncodedKeyValue} (the KeyValue form emitted for key types without a + * structured KeyValue element). No verification key is supplied out of band: the inbound + * processor must resolve the key from the document itself, then verify with it. + */ +class StaxMLDSAKeyValueInboundTest 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 testInboundVerifiesWithKeyFromDerEncodedKeyValue(String sigAlgorithm, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), + "ML-DSA requires BouncyCastle 1.81+"); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + byte[] signed = signWithKeyValue(sigAlgorithm, kp); + + List events = new ArrayList<>(); + Document verified = verifyInbound(signed, events); + Assertions.assertNotNull(verified.getDocumentElement(), "Inbound processing must yield a document"); + + // The key the inbound processor verified with must be the signer's, recovered from the + // document's DEREncodedKeyValue (no key was supplied out of band). + PublicKey resolved = null; + for (SecurityEvent event : events) { + if (event instanceof KeyValueTokenSecurityEvent) { + resolved = ((KeyValueTokenSecurityEvent) event).getSecurityToken().getPublicKey(); + } + } + Assertions.assertNotNull(resolved, "Expected a KeyValueTokenSecurityEvent carrying the resolved key"); + Assertions.assertArrayEquals(kp.getPublic().getEncoded(), resolved.getEncoded(), + "Key resolved from DEREncodedKeyValue must be the signer's public key"); + } + + @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 testInboundTamperedSignatureRejected(String sigAlgorithm, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), + "ML-DSA requires BouncyCastle 1.81+"); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + byte[] tampered = tamperSignatureValue(signWithKeyValue(sigAlgorithm, kp)); + + // The key still resolves from the document; the rejection must come from signature + // validation itself, not from a failure to resolve the key. + XMLStreamException ex = Assertions.assertThrows(XMLStreamException.class, + () -> verifyInbound(tampered, new ArrayList<>())); + String chain = messageChain(ex); + Assertions.assertTrue(chain.contains("INVALID signature"), + "Expected a core-validation failure, got: " + chain); + } + + private static String messageChain(Throwable t) { + StringBuilder sb = new StringBuilder(); + for (Throwable c = t; c != null; c = c.getCause()) { + sb.append(c.getClass().getSimpleName()).append(": ").append(c.getMessage()).append(" | "); + } + return sb.toString(); + } + + /** + * XML Signature 1.1 defines {@code dsig11:DEREncodedKeyValue} as a direct child of + * {@code ds:KeyInfo}; the nested-in-KeyValue placement is what this library emits. A document + * from another implementation may use the canonical placement, so the inbound side must + * resolve the key from there as well. + */ + @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 testInboundVerifiesWithDerEncodedKeyValueAsKeyInfoChild(String sigAlgorithm, String jcaAlgorithm) + throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), + "ML-DSA requires BouncyCastle 1.81+"); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + byte[] signed = moveDerEncodedKeyValueToKeyInfo(signWithKeyValue(sigAlgorithm, kp)); + + List events = new ArrayList<>(); + Document verified = verifyInbound(signed, events); + Assertions.assertNotNull(verified.getDocumentElement(), "Inbound processing must yield a document"); + + PublicKey resolved = null; + for (SecurityEvent event : events) { + if (event instanceof KeyValueTokenSecurityEvent) { + resolved = ((KeyValueTokenSecurityEvent) event).getSecurityToken().getPublicKey(); + } + } + Assertions.assertNotNull(resolved, "Expected a KeyValueTokenSecurityEvent carrying the resolved key"); + Assertions.assertArrayEquals(kp.getPublic().getEncoded(), resolved.getEncoded(), + "Key resolved from a KeyInfo-level DEREncodedKeyValue must be the signer's public key"); + } + + /** + * Re-parents the emitted {@code dsig11:DEREncodedKeyValue} from inside {@code ds:KeyValue} to + * be a direct child of {@code ds:KeyInfo} (removing the now-empty KeyValue), giving the + * canonical XML Signature 1.1 layout. KeyInfo is outside the signed content, so the + * signature stays valid. + */ + private byte[] moveDerEncodedKeyValueToKeyInfo(byte[] signed) throws Exception { + Document document; + try (InputStream is = new ByteArrayInputStream(signed)) { + document = XMLUtils.read(is, false); + } + Element keyInfo = (Element) document.getElementsByTagNameNS(Constants.SignatureSpecNS, "KeyInfo").item(0); + Element keyValue = (Element) keyInfo.getElementsByTagNameNS(Constants.SignatureSpecNS, "KeyValue").item(0); + Element der = (Element) keyValue.getElementsByTagNameNS( + "http://www.w3.org/2009/xmldsig11#", "DEREncodedKeyValue").item(0); + Assertions.assertNotNull(der, "Expected a DEREncodedKeyValue inside KeyValue to re-parent"); + keyValue.removeChild(der); + keyInfo.replaceChild(der, keyValue); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + javax.xml.transform.TransformerFactory.newInstance().newTransformer().transform( + new javax.xml.transform.dom.DOMSource(document), + new javax.xml.transform.stream.StreamResult(bos)); + return bos.toByteArray(); + } + + private Document verifyInbound(byte[] signed, List events) throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + // deliberately no setSignatureVerificationKey(...) + InboundXMLSec inboundXMLSec = XMLSec.getInboundWSSec(properties); + XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); + xmlInputFactory.setEventAllocator(new XMLSecEventAllocator()); + XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new ByteArrayInputStream(signed)); + XMLStreamReader securityStreamReader = + inboundXMLSec.processInMessage(xmlStreamReader, null, events::add); + return StAX2DOM.readDoc(securityStreamReader); + } + + private byte[] 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); + + return process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", properties, null); + } + + /** Flips one byte of the SignatureValue and re-serializes. */ + private byte[] tamperSignatureValue(byte[] signed) throws Exception { + Document document; + try (InputStream is = new ByteArrayInputStream(signed)) { + document = XMLUtils.read(is, false); + } + NodeList sigValues = document.getElementsByTagNameNS(Constants.SignatureSpecNS, "SignatureValue"); + Assertions.assertEquals(1, sigValues.getLength(), "Expected exactly one SignatureValue element"); + Element sigValueElement = (Element) sigValues.item(0); + + byte[] sigBytes = Base64.getMimeDecoder().decode(sigValueElement.getTextContent()); + sigBytes[sigBytes.length / 2] ^= (byte) 0xFF; + + NodeList children = sigValueElement.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + sigValueElement.removeChild(children.item(i)); + } + Text newText = document.createTextNode(Base64.getEncoder().encodeToString(sigBytes)); + sigValueElement.appendChild(newText); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + javax.xml.transform.TransformerFactory.newInstance().newTransformer().transform( + new javax.xml.transform.dom.DOMSource(document), + new javax.xml.transform.stream.StreamResult(bos)); + return bos.toByteArray(); + } +} From dcd65cdb49d2e139bae23578aa6542dadf61b655 Mon Sep 17 00:00:00 2001 From: arpansharma Date: Tue, 25 Aug 2026 14:26:55 -0500 Subject: [PATCH 05/10] Reject malformed DEREncodedKeyValue content cleanly buildPublicKey() caught only NoSuchAlgorithmException and InvalidKeySpecException, but some providers (BouncyCastle's XDH/EdDSA KeyFactorySpi) throw an unchecked ArrayIndexOutOfBoundsException for malformed or short input rather than InvalidKeySpecException. Because the DEREncodedKeyValue content is untrusted, attacker-controlled inbound data, that exception propagated out of processInMessage instead of being rejected cleanly. Also catch RuntimeException in the key-type loop so a malformed encoding falls through to a clean stax.unsupportedKeyValue rejection, and add a garbage-content case to StaxMLDSAKeyValueInboundTest (parameterized across ML-DSA-44/65/87) that fails without this change. --- .../DEREncodedKeyValueSecurityToken.java | 8 ++- .../StaxMLDSAKeyValueInboundTest.java | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java b/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java index 221b75cc2..4e8042c05 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java +++ b/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java @@ -68,8 +68,12 @@ private PublicKey buildPublicKey() throws XMLSecurityException { if (publicKey != null) { return publicKey; } - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { //NOPMD - // Not this key type; try the next one + } catch (NoSuchAlgorithmException | InvalidKeySpecException | RuntimeException e) { //NOPMD + // Not this key type; try the next one. Some providers (e.g. BouncyCastle's + // XDH/EdDSA KeyFactorySpi) throw an unchecked exception such as + // ArrayIndexOutOfBoundsException instead of InvalidKeySpecException for + // malformed or short input, which must not propagate since encodedKey here is + // untrusted, attacker-controlled inbound content. } } throw new XMLSecurityException("stax.unsupportedKeyValue"); diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java index 9abe7116b..5b93e604f 100644 --- a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java @@ -203,6 +203,55 @@ private byte[] moveDerEncodedKeyValueToKeyInfo(byte[] signed) throws Exception { return bos.toByteArray(); } + /** + * A DEREncodedKeyValue whose content is garbage (decodes to no valid SubjectPublicKeyInfo) + * must be rejected cleanly at key resolution, not crash the pipeline with an uncaught + * RuntimeException. Some providers throw an unchecked exception (e.g. BouncyCastle's + * XDH/EdDSA KeyFactorySpi throws ArrayIndexOutOfBoundsException) for malformed input, and + * inbound KeyInfo content is attacker-controlled. + */ + @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 testInboundGarbageDerContentRejectedCleanly(String sigAlgorithm, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), + "ML-DSA requires BouncyCastle 1.81+"); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + byte[] corrupted = corruptDerEncodedKeyValue(signWithKeyValue(sigAlgorithm, kp)); + + XMLStreamException ex = Assertions.assertThrows(XMLStreamException.class, + () -> verifyInbound(corrupted, new ArrayList<>())); + String chain = messageChain(ex); + Assertions.assertFalse(chain.contains("ArrayIndexOutOfBoundsException"), + "Malformed DEREncodedKeyValue content must not surface as an uncaught RuntimeException: " + chain); + } + + /** Replaces the DEREncodedKeyValue's base64 content with bytes that decode to no known SubjectPublicKeyInfo. */ + private byte[] corruptDerEncodedKeyValue(byte[] signed) throws Exception { + Document document; + try (InputStream is = new ByteArrayInputStream(signed)) { + document = XMLUtils.read(is, false); + } + Element der = (Element) document.getElementsByTagNameNS( + "http://www.w3.org/2009/xmldsig11#", "DEREncodedKeyValue").item(0); + byte[] garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + NodeList children = der.getChildNodes(); + for (int i = children.getLength() - 1; i >= 0; i--) { + der.removeChild(children.item(i)); + } + der.appendChild(document.createTextNode(Base64.getEncoder().encodeToString(garbage))); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + javax.xml.transform.TransformerFactory.newInstance().newTransformer().transform( + new javax.xml.transform.dom.DOMSource(document), + new javax.xml.transform.stream.StreamResult(bos)); + return bos.toByteArray(); + } + private Document verifyInbound(byte[] signed, List events) throws Exception { XMLSecurityProperties properties = new XMLSecurityProperties(); // deliberately no setSignatureVerificationKey(...) From 2859acc5aadfffc58c69d5dd5ab4c1a4632c0824 Mon Sep 17 00:00:00 2001 From: arpansharma Date: Tue, 25 Aug 2026 14:56:00 -0500 Subject: [PATCH 06/10] Reject malformed DEREncodedKeyValue content cleanly on the DOM path The DOM DEREncodedKeyValue#getPublicKey() has the same narrow exception handling as the StAX token fixed in the previous commit: it caught only NoSuchAlgorithmException and InvalidKeySpecException while iterating the supported key types, so an unchecked exception from a KeyFactorySpi (BouncyCastle 1.85's XDH/EdDSA throw ArrayIndexOutOfBoundsException for malformed or short input) propagated out instead of a clean rejection. Because DEREncodedKeyValueResolver is a default KeyResolver and a DEREncodedKeyValue in an inbound document is untrusted, attacker-controlled content, this could crash key resolution reached via KeyInfo#getPublicKey() with an uncontrolled runtime exception. Catch RuntimeException in the loop so a malformed encoding falls through to the declared XMLSecurityException. Adds a test that fails without the fix (BouncyCastle at first provider position, skipped otherwise). --- .../keys/content/DEREncodedKeyValue.java | 7 +- ...EREncodedKeyValueMalformedContentTest.java | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/apache/xml/security/test/dom/keys/DEREncodedKeyValueMalformedContentTest.java diff --git a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java index 85be2f938..6cbd66b5a 100644 --- a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java +++ b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java @@ -121,8 +121,11 @@ public PublicKey getPublicKey() throws XMLSecurityException { if (publicKey != null) { return publicKey; } - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { //NOPMD - // Do nothing, try the next type + } catch (NoSuchAlgorithmException | InvalidKeySpecException | RuntimeException e) { //NOPMD + // Do nothing, try the next type. Some providers (e.g. BouncyCastle's XDH/EdDSA + // KeyFactorySpi) throw an unchecked exception such as ArrayIndexOutOfBoundsException + // instead of InvalidKeySpecException for malformed or short input, which must not + // propagate since the encoded key here is untrusted, attacker-controlled content. } } throw new XMLSecurityException("DEREncodedKeyValue.UnsupportedEncodedKey"); diff --git a/src/test/java/org/apache/xml/security/test/dom/keys/DEREncodedKeyValueMalformedContentTest.java b/src/test/java/org/apache/xml/security/test/dom/keys/DEREncodedKeyValueMalformedContentTest.java new file mode 100644 index 000000000..65eb7ece1 --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/dom/keys/DEREncodedKeyValueMalformedContentTest.java @@ -0,0 +1,80 @@ +/** + * 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.dom.keys; + +import java.security.Provider; +import java.security.Security; + +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.keys.content.DEREncodedKeyValue; +import org.apache.xml.security.test.dom.TestUtils; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * {@link DEREncodedKeyValue#getPublicKey()} resolves the key by trying each supported key type's + * {@code KeyFactory}. Some providers throw an unchecked exception for malformed or short input + * instead of {@code InvalidKeySpecException} - notably BouncyCastle 1.85's XDH/EdDSA + * KeyFactorySpi throws {@code ArrayIndexOutOfBoundsException}. Since a DEREncodedKeyValue read + * from an inbound document (via {@code DEREncodedKeyValueResolver}, a default KeyResolver) is + * untrusted, attacker-controlled content, such an exception must not propagate out of key + * resolution. + * + *

The unchecked exception is only observed when BouncyCastle is the provider selected for + * XDH/EdDSA, i.e. registered ahead of the JDK's own providers (a common BouncyCastle-primary + * deployment). With the JDK providers taking precedence they reject the same input cleanly with + * {@code InvalidKeySpecException}, so this test inserts BouncyCastle at the first position (as + * {@code XMLCipherTest} does for its BouncyCastle-specific case) and is skipped when BouncyCastle + * is unavailable. + */ +class DEREncodedKeyValueMalformedContentTest { + + @Test + void testMalformedDerContentRejectedCleanly() throws Exception { + boolean bcAtFirstPosition = false; + if (Security.getProvider("BC") == null) { + try { + Class bcClass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider bc = (Provider) bcClass.getConstructor().newInstance(); + Security.insertProviderAt(bc, 1); + bcAtFirstPosition = true; + } catch (ReflectiveOperationException e) { + // BouncyCastle not installed, ignore + } + } + assumeTrue(bcAtFirstPosition, "requires BouncyCastle at first provider position"); + + try { + Document doc = TestUtils.newDocument(); + // Bytes that decode to no valid SubjectPublicKeyInfo; short enough that BouncyCastle + // 1.85's XDH/EdDSA KeyFactory reads past the end (ArrayIndexOutOfBoundsException). + DEREncodedKeyValue derEncodedKeyValue = + new DEREncodedKeyValue(doc, new byte[]{0, 1, 2, 3, 4, 5, 6, 7}); + + // Must fail cleanly with the declared XMLSecurityException, not an uncaught + // RuntimeException such as ArrayIndexOutOfBoundsException. + assertThrows(XMLSecurityException.class, derEncodedKeyValue::getPublicKey); + } finally { + Security.removeProvider("BC"); + } + } +} From 0189ba1ffcda26dff6a7223b07c80aaea55b27c7 Mon Sep 17 00:00:00 2001 From: Freeman Fang Date: Fri, 11 Sep 2026 13:16:53 -0400 Subject: [PATCH 07/10] Use finalized ML-DSA URIs and reject unsupported SignatureContext - ML-DSA algorithm URIs move from the "tbd" placeholder to the assigned http://www.w3.org/2026/08/xmldsig-more# namespace (draft-eastlake-rfc9231bis-xmlsec-uris-09 section 3.3.15), in DOMSignatureMethod, XMLSignature, security-config.xml and the StAX ML-DSA tests. - draft-09 section 3.3.15 defines an optional dsig-more:SignatureContext element. java.security.Signature cannot pass a signature context to ML-DSA (a Non-Goal of JEP 497), so a signature carrying one can be neither produced nor verified correctly. XMLSignature (sign and checkSignatureValue) and DOMXMLSignature (sign and validate) now detect the element and fail with a clear exception instead of silently ignoring the context. New Constants.XML_DSIG_NS_MORE_26_08 / _TAG_SIGNATURECONTEXT and a signature.SignatureContextUnsupported message back the check. - XMLSignatureMLDSATest now exercises the JDK's own ML-DSA implementation where available (JEP 497, JDK 24+), only registering BouncyCastle when running on a pre-24 JDK without it, and no longer hard-codes the "BC" provider for key generation. Adds sign- and verify-side SignatureContext rejection tests on both the native and JSR-105 paths, parameterized across ML-DSA-44/65/87. --- .../dsig/internal/dom/DOMSignatureMethod.java | 11 +- .../dsig/internal/dom/DOMXMLSignature.java | 30 +++ .../resource/xmlsecurity_en.properties | 1 + .../xml/security/signature/XMLSignature.java | 39 ++- .../apache/xml/security/utils/Constants.java | 6 + src/main/resources/security-config.xml | 8 +- .../crypto/dsig/XMLSignatureMLDSATest.java | 227 ++++++++++++++++-- .../StaxMLDSAKeyValueInboundTest.java | 24 +- .../stax/signature/StaxMLDSAKeyValueTest.java | 6 +- .../signature/StaxMLDSASignatureTest.java | 18 +- 10 files changed, 310 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java index 83d8eb179..cf2696841 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java @@ -95,15 +95,14 @@ public abstract class DOMSignatureMethod extends AbstractDOMSignatureMethod { static final String ED448 = "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 - // to be updated once final URIs are assigned (see SANTUARIO-634). + // URIs for ML-DSA (FIPS 204) per draft-eastlake-rfc9231bis-xmlsec-uris-09 + // section 3.3.15 (see SANTUARIO-634). static final String ML_DSA_44 = - "http://www.w3.org/tbd#ml-dsa-44"; + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44"; static final String ML_DSA_65 = - "http://www.w3.org/tbd#ml-dsa-65"; + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65"; static final String ML_DSA_87 = - "http://www.w3.org/tbd#ml-dsa-87"; + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87"; static final String ECDSA_SHA3_224 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-224"; static final String ECDSA_SHA3_256 = diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java index e9aae0097..081991fbe 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java @@ -60,11 +60,13 @@ import javax.xml.crypto.dsig.dom.DOMValidateContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import org.apache.xml.security.utils.Constants; import org.apache.xml.security.utils.XMLUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; /** * DOM-based implementation of XMLSignature. @@ -278,6 +280,8 @@ public boolean validate(XMLValidateContext vc) return validationStatus; } + checkForUnsupportedSignatureContext(localSigElem); + // validate the signature boolean sigValidity = sv.validate(vc); if (!sigValidity) { @@ -339,6 +343,30 @@ public boolean validate(XMLValidateContext vc) return validationStatus; } + /** + * Rejects a signature that carries an ML-DSA {@code SignatureContext} element + * (draft-eastlake-rfc9231bis-xmlsec-uris-09, section 3.3.15). The + * {@code java.security.Signature} API offers no way to pass a signature context + * to ML-DSA (see the Non-Goals of JEP 497), so such a signature can be neither + * created nor verified correctly here; bail out rather than silently ignoring + * the context. + */ + private static void checkForUnsupportedSignatureContext(Element sigElem) + throws XMLSignatureException + { + if (sigElem == null) { + return; + } + NodeList contexts = sigElem.getElementsByTagNameNS( + Constants.XML_DSIG_NS_MORE_26_08, Constants._TAG_SIGNATURECONTEXT); + if (contexts.getLength() > 0) { + throw new XMLSignatureException("The ML-DSA SignatureContext element (" + + Constants.XML_DSIG_NS_MORE_26_08 + Constants._TAG_SIGNATURECONTEXT + + ") is not supported: the java.security.Signature API cannot pass a " + + "signature context to ML-DSA (JEP 497)"); + } + } + @Override public void sign(XMLSignContext signContext) throws MarshalException, XMLSignatureException @@ -350,6 +378,8 @@ public void sign(XMLSignContext signContext) marshal(context.getParent(), context.getNextSibling(), DOMUtils.getSignaturePrefix(context), context); + checkForUnsupportedSignatureContext(sigElem); + // generate references and signature value List allReferences = new ArrayList<>(); diff --git a/src/main/java/org/apache/xml/security/resource/xmlsecurity_en.properties b/src/main/java/org/apache/xml/security/resource/xmlsecurity_en.properties index 74e8c150c..af52ad71e 100644 --- a/src/main/java/org/apache/xml/security/resource/xmlsecurity_en.properties +++ b/src/main/java/org/apache/xml/security/resource/xmlsecurity_en.properties @@ -151,6 +151,7 @@ signature.Verification.MultipleIDs = Multiple Elements with the same ID {0} were signature.Verification.NoSignatureElement = Input document contains no {0} Element in namespace {1} signature.Verification.Reference.NoInput = The Reference for URI {0} has no XMLSignatureInput signature.Verification.SignatureError = Signature error +signature.SignatureContextUnsupported = The ML-DSA SignatureContext element ({0}) is not supported: the java.security.Signature API cannot pass a signature context to ML-DSA (JEP 497), so the signature can neither be created nor verified signature.XMLSignatureInput.MissingConstuctor = Cannot construct a XMLSignatureInput from class {0} signature.XMLSignatureInput.SerializeDOM = Input initialized with DOM Element. Use Canonicalization to serialize it signature.XMLSignatureInput.nodesetReference = Unable to convert to nodeset the reference diff --git a/src/main/java/org/apache/xml/security/signature/XMLSignature.java b/src/main/java/org/apache/xml/security/signature/XMLSignature.java index 9110db529..dae7a4999 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -51,6 +51,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.w3c.dom.Text; /** @@ -211,17 +212,17 @@ public final class XMLSignature extends SignatureElementProxy { public static final String ALGO_ID_SIGNATURE_EDDSA_ED448 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"; - /**Signature - ML-DSA-44 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + /**Signature - ML-DSA-44 (FIPS 204, URI per draft-eastlake-rfc9231bis-xmlsec-uris-09 section 3.3.15; see SANTUARIO-634) */ public static final String ALGO_ID_SIGNATURE_MLDSA_44 = - "http://www.w3.org/tbd#ml-dsa-44"; + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44"; - /**Signature - ML-DSA-65 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + /**Signature - ML-DSA-65 (FIPS 204, URI per draft-eastlake-rfc9231bis-xmlsec-uris-09 section 3.3.15; see SANTUARIO-634) */ public static final String ALGO_ID_SIGNATURE_MLDSA_65 = - "http://www.w3.org/tbd#ml-dsa-65"; + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65"; - /**Signature - ML-DSA-87 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + /**Signature - ML-DSA-87 (FIPS 204, URI per draft-eastlake-rfc9231bis-xmlsec-uris-09 section 3.3.15; see SANTUARIO-634) */ public static final String ALGO_ID_SIGNATURE_MLDSA_87 = - "http://www.w3.org/tbd#ml-dsa-87"; + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87"; /**Signature - SHA3-224withECDSA */ @@ -863,6 +864,7 @@ public void sign(Key signingKey) throws XMLSignatureException { ); } + checkForUnsupportedSignatureContext(); // snapshot the lists so that concurrent registration during sign() cannot // cause ConcurrentModificationException or skip newly added processors @@ -903,6 +905,29 @@ public void sign(Key signingKey) throws XMLSignatureException { } } + /** + * Rejects a signature that carries an ML-DSA {@code SignatureContext} element + * (draft-eastlake-rfc9231bis-xmlsec-uris-09, section 3.3.15). The + * {@code java.security.Signature} API offers no way to pass a signature context + * to ML-DSA (see the Non-Goals of JEP 497), so such a signature can be neither + * created nor verified correctly here; bail out rather than silently ignoring + * the context and producing/accepting a signature that does not match it. + * + * @throws XMLSignatureException if a {@code SignatureContext} element is present + */ + private void checkForUnsupportedSignatureContext() throws XMLSignatureException { + Element signatureElement = getElement(); + if (signatureElement == null) { + return; + } + NodeList contexts = signatureElement.getElementsByTagNameNS( + Constants.XML_DSIG_NS_MORE_26_08, Constants._TAG_SIGNATURECONTEXT); + if (contexts.getLength() > 0) { + throw new XMLSignatureException("signature.SignatureContextUnsupported", + new Object[] {Constants.XML_DSIG_NS_MORE_26_08 + Constants._TAG_SIGNATURECONTEXT}); + } + } + /** * Adds a {@link ResourceResolverSpi} to enable the retrieval of resources. * @@ -957,6 +982,8 @@ public boolean checkSignatureValue(Key pk) throws XMLSignatureException { // SignedInfo. // If followManifestsDuringValidation is true it will do the same for // References inside a Manifest. + checkForUnsupportedSignatureContext(); + try { SignedInfo si = this.getSignedInfo(); //create a SignatureAlgorithms from the SignatureMethod inside diff --git a/src/main/java/org/apache/xml/security/utils/Constants.java b/src/main/java/org/apache/xml/security/utils/Constants.java index c894bf501..dd5a018f5 100644 --- a/src/main/java/org/apache/xml/security/utils/Constants.java +++ b/src/main/java/org/apache/xml/security/utils/Constants.java @@ -70,6 +70,12 @@ public final class Constants { /** The 2021 xmldsig-more URL for Internet Engineering Task Force (IETF) algorithms **/ public static final String XML_DSIG_NS_MORE_21_04 = "http://www.w3.org/2021/04/xmldsig-more#"; + /** The 2026 xmldsig-more URL for IETF algorithms (draft-eastlake-rfc9231bis-xmlsec-uris-09), e.g. ML-DSA **/ + public static final String XML_DSIG_NS_MORE_26_08 = "http://www.w3.org/2026/08/xmldsig-more#"; + + /** Tag of the ML-DSA {@code SignatureContext} element (draft-eastlake-rfc9231bis-xmlsec-uris-09, section 3.3.15) **/ + public static final String _TAG_SIGNATURECONTEXT = "SignatureContext"; + /** The URI for XML spec*/ public static final String XML_LANG_SPACE_SpecNS = "http://www.w3.org/XML/1998/namespace"; diff --git a/src/main/resources/security-config.xml b/src/main/resources/security-config.xml index dc4a92879..f5e11a848 100644 --- a/src/main/resources/security-config.xml +++ b/src/main/resources/security-config.xml @@ -321,22 +321,22 @@ RequiredKey="EC" JCEName="RIPEMD160withECDSA"/> - - + - - Key pairs and self-signed certificates are generated on the fly for each of * ML-DSA-44/65/87 via {@link SelfSignedCertGenerator}, rather than loading a * pre-generated keystore committed as a binary test resource (see SANTUARIO-634). - * 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. + * The ML-DSA JCA provider is the JDK's own from Java 24 on (JEP 497); on older + * JDKs the tests fall back to BouncyCastle via the shared test auxiliary provider, + * and are skipped if neither is available. 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: + *

On a pre-24 JDK, run with the Maven {@code bouncycastle} profile: *

mvn test -Dtest=XMLSignatureMLDSATest -P bouncycastle
*/ class XMLSignatureMLDSATest extends XMLSignatureAbstract { @@ -73,7 +95,7 @@ class XMLSignatureMLDSATest extends XMLSignatureAbstract { static final char[] KEY_PASSWORD = "security".toCharArray(); private static boolean mlDsaAvailable; - private static boolean bcAddedForTheTest; + private static boolean auxProviderRegistered; private static KeyStore keyStore; @BeforeAll @@ -81,16 +103,17 @@ 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) { + // The JDK ships ML-DSA (JEP 497) from Java 24 on; before that it needs BouncyCastle. + // Prefer whatever provider the platform already offers, and only pull in the shared + // test auxiliary provider (BouncyCastle) when running on a pre-24 JDK without it, + // so the tests exercise the JDK implementation where one is available. + if (JDKTestUtils.getJDKVersion() < 24 && Security.getProvider("BC") == null) { + if (JDKTestUtils.getAuxiliaryProvider() == null) { mlDsaAvailable = false; return; } + JDKTestUtils.registerAuxiliaryProvider(); + auxProviderRegistered = true; } try { @@ -98,7 +121,7 @@ static void setUp() { keyStore.load(null, null); for (String alias : new String[]{"ml-dsa-44", "ml-dsa-65", "ml-dsa-87"}) { String jcaAlgorithm = alias.toUpperCase(); - KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaAlgorithm, "BC"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaAlgorithm); KeyPair keyPair = kpg.generateKeyPair(); X509Certificate cert = SelfSignedCertGenerator.generate( keyPair, jcaAlgorithm, "CN=Test " + jcaAlgorithm + ",O=Apache Santuario,C=US", 365); @@ -112,8 +135,8 @@ static void setUp() { @AfterAll static void tearDown() { - if (bcAddedForTheTest) { - Security.removeProvider("BC"); + if (auxProviderRegistered) { + JDKTestUtils.unregisterAuxiliaryProvider(); } } @@ -124,7 +147,7 @@ static void tearDown() { 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+"); + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); Assertions.assertNotNull(signedXml); assertValidSignatureWithJcpApi(signedXml, false); @@ -137,7 +160,7 @@ void testMLDSASignAndVerify(String signatureAlgorithmURI, String alias) throws E XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87", }) void testMLDSATamperedSignatureRejected(String signatureAlgorithmURI, String alias) throws Exception { - Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); byte[] tamperedXml = flipByteInSignatureValue(signedXml); @@ -153,10 +176,10 @@ void testMLDSATamperedSignatureRejected(String signatureAlgorithmURI, String ali XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87", }) void testMLDSAWrongPublicKeyRejected(String signatureAlgorithmURI, String alias) throws Exception { - Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); - KeyPairGenerator kpg = KeyPairGenerator.getInstance(alias.toUpperCase(), "BC"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alias.toUpperCase()); PublicKey wrongPublicKey = kpg.generateKeyPair().getPublic(); KeySelector wrongKeySelector = new KeySelector() { @@ -202,6 +225,170 @@ private byte[] flipByteInSignatureValue(byte[] signedXml) throws Exception { return bos.toByteArray(); } + // ===== ML-DSA SignatureContext rejection ===== + // draft-eastlake-rfc9231bis-xmlsec-uris-09 section 3.3.15 defines an optional + // dsig-more:SignatureContext element (carried in a ds:Object). java.security.Signature + // cannot pass a context to ML-DSA (JEP 497 non-goal), so the library must refuse to + // create or verify such a signature rather than silently ignoring the context. + + private static final String SIGNATURE_CONTEXT_NS = "http://www.w3.org/2026/08/xmldsig-more#"; + + @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 testMLDSAJcpSignRejectsSignatureContext(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); + + Document doc = TestUtils.newDocument(); + Element root = doc.createElement("RootElement"); + doc.appendChild(root); + Element signed = doc.createElement("SignedElement"); + signed.setAttribute("id", "e1"); + signed.appendChild(doc.createTextNode("Some data to sign")); + root.appendChild(signed); + + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, KEY_PASSWORD); + X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); + + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + Reference ref = fac.newReference("#e1", fac.newDigestMethod(DigestMethod.SHA256, null), + Collections.singletonList(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)), + null, null); + SignedInfo si = fac.newSignedInfo( + fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), + fac.newSignatureMethod(signatureAlgorithmURI, null), + Collections.singletonList(ref)); + + Element ctx = doc.createElementNS(SIGNATURE_CONTEXT_NS, "dsig-more:SignatureContext"); + ctx.setTextContent(Base64.getEncoder().encodeToString("email-signature".getBytes(StandardCharsets.UTF_8))); + XMLObject obj = fac.newXMLObject( + Collections.singletonList(new DOMStructure(ctx)), null, null, null); + + KeyInfoFactory kif = fac.getKeyInfoFactory(); + KeyInfo ki = kif.newKeyInfo(Collections.singletonList( + kif.newX509Data(Collections.singletonList(cert)))); + + javax.xml.crypto.dsig.XMLSignature sig = + fac.newXMLSignature(si, ki, Collections.singletonList(obj), null, null); + DOMSignContext sc = new DOMSignContext(privateKey, doc.getDocumentElement()); + sc.setIdAttributeNS(signed, null, "id"); + + javax.xml.crypto.dsig.XMLSignatureException ex = Assertions.assertThrows( + javax.xml.crypto.dsig.XMLSignatureException.class, () -> sig.sign(sc)); + Assertions.assertTrue(ex.getMessage().contains("SignatureContext"), ex.getMessage()); + } + + @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 testMLDSAJcpVerifyRejectsSignatureContext(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + byte[] withContext = injectSignatureContext(signedXml); + + Assertions.assertThrows(javax.xml.crypto.dsig.XMLSignatureException.class, + () -> validateSignatureWithJcpApi(withContext, new KeySelectors.RawX509KeySelector())); + } + + @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 testMLDSANativeSignRejectsSignatureContext(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); + + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, KEY_PASSWORD); + X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); + + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("", "RootElement"); + doc.appendChild(root); + root.appendChild(doc.createTextNode("Some simple text")); + + Element canon = XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_CANONICALIZATIONMETHOD); + canon.setAttributeNS(null, Constants._ATT_ALGORITHM, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + SignatureAlgorithm sigAlg = new SignatureAlgorithm(doc, signatureAlgorithmURI); + XMLSignature sig = new XMLSignature(doc, null, sigAlg.getElement(), canon); + root.appendChild(sig.getElement()); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + sig.addKeyInfo(cert); + + ObjectContainer obj = new ObjectContainer(doc); + Element ctx = doc.createElementNS(SIGNATURE_CONTEXT_NS, "dsig-more:SignatureContext"); + ctx.setTextContent(Base64.getEncoder().encodeToString("email-signature".getBytes(StandardCharsets.UTF_8))); + obj.appendChild(ctx); + sig.appendObject(obj); + + org.apache.xml.security.signature.XMLSignatureException ex = Assertions.assertThrows( + org.apache.xml.security.signature.XMLSignatureException.class, () -> sig.sign(privateKey)); + Assertions.assertTrue(ex.getMessage().contains("SignatureContext"), ex.getMessage()); + } + + @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 testMLDSANativeVerifyRejectsSignatureContext(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires JDK 24+ or BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + byte[] withContext = injectSignatureContext(signedXml); + + Document doc; + try (ByteArrayInputStream is = new ByteArrayInputStream(withContext)) { + doc = XMLUtils.read(is, false); + } + Element sigElement = (Element) doc.getElementsByTagNameNS( + Constants.SignatureSpecNS, "Signature").item(0); + X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); + XMLSignature signature = new XMLSignature(sigElement, ""); + + org.apache.xml.security.signature.XMLSignatureException ex = Assertions.assertThrows( + org.apache.xml.security.signature.XMLSignatureException.class, + () -> signature.checkSignatureValue(cert)); + Assertions.assertTrue(ex.getMessage().contains("SignatureContext"), ex.getMessage()); + } + + /** + * Inserts a {@code ...} + * as the last child of the {@code ds:Signature} element, simulating a signature that carries an + * ML-DSA signature context. + */ + private byte[] injectSignatureContext(byte[] signedXml) throws Exception { + Document doc; + try (ByteArrayInputStream is = new ByteArrayInputStream(signedXml)) { + doc = XMLUtils.read(is, false); + } + Element sig = (Element) doc.getElementsByTagNameNS( + Constants.SignatureSpecNS, "Signature").item(0); + Assertions.assertNotNull(sig, "Expected a ds:Signature element"); + + String sigPrefix = sig.getPrefix(); + String objectQName = sigPrefix == null ? "Object" : sigPrefix + ":Object"; + Element object = doc.createElementNS(Constants.SignatureSpecNS, objectQName); + Element ctx = doc.createElementNS(SIGNATURE_CONTEXT_NS, "dsig-more:SignatureContext"); + ctx.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:dsig-more", SIGNATURE_CONTEXT_NS); + ctx.setTextContent(Base64.getEncoder().encodeToString("email-signature".getBytes(StandardCharsets.UTF_8))); + object.appendChild(ctx); + sig.appendChild(object); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLUtils.outputDOMc14nWithComments(doc, bos); + return bos.toByteArray(); + } + @Override KeyStore getKeyStore() { return keyStore; diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java index 5b93e604f..424a810ee 100644 --- a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java @@ -84,9 +84,9 @@ static void generateKeys() throws Exception { @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testInboundVerifiesWithKeyFromDerEncodedKeyValue(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), @@ -114,9 +114,9 @@ void testInboundVerifiesWithKeyFromDerEncodedKeyValue(String sigAlgorithm, Strin @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testInboundTamperedSignatureRejected(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), @@ -150,9 +150,9 @@ private static String messageChain(Throwable t) { */ @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testInboundVerifiesWithDerEncodedKeyValueAsKeyInfoChild(String sigAlgorithm, String jcaAlgorithm) throws Exception { @@ -212,9 +212,9 @@ private byte[] moveDerEncodedKeyValueToKeyInfo(byte[] signed) throws Exception { */ @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testInboundGarbageDerContentRejectedCleanly(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), 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 index b1e1a9396..1aa5620a5 100644 --- 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 @@ -78,9 +78,9 @@ static void generateKeys() throws Exception { @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testKeyValueEmitsDerEncodedKeyValue(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), 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 index 0fd45b669..dc7354caf 100644 --- 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 @@ -72,9 +72,9 @@ static void generateKeys() throws Exception { @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testMLDSASign(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), @@ -110,9 +110,9 @@ void testMLDSASign(String sigAlgorithm, String jcaAlgorithm) throws Exception { @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testMLDSAStaxTamperedSignatureRejected(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), @@ -128,9 +128,9 @@ void testMLDSAStaxTamperedSignatureRejected(String sigAlgorithm, String jcaAlgor @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" + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-44,ML-DSA-44", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65,ML-DSA-65", + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-87,ML-DSA-87" }) void testMLDSAStaxWrongPublicKeyRejected(String sigAlgorithm, String jcaAlgorithm) throws Exception { Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), From a25d9282fc87697a5d54238dbaada726183a3f1c Mon Sep 17 00:00:00 2001 From: Freeman Fang Date: Fri, 18 Sep 2026 12:22:52 -0400 Subject: [PATCH 08/10] Collapse per-algorithm SignatureMethod subclasses into a URI-keyed registry DOMSignatureMethod had ~30 near-identical leaf subclasses (SHA1withRSA, SHA224withECDSA, MLDSA_44, etc.), each differing only in algorithm URI and underlying JCA algorithm name(s), with the dispatch between them duplicated in both DOMSignatureMethod.unmarshal() and DOMXMLSignatureFactory.newSignatureMethod(). Replace the leaf classes with six generic "shape" classes (RSASignatureMethod, RSAPSSSignatureMethod, DSASignatureMethod, ECDSASignatureMethod, EDDSASignatureMethod, MLDSASignatureMethod) parameterized by URI/algorithm data and looked up via a single Map populated at class-init time. The RSA-PSS-generic and HMAC-output-length special cases, which take caller-supplied AlgorithmParameterSpec/params rather than fixed per-algorithm data, are left as direct construction in their existing if/else branches --- .../dsig/internal/dom/DOMSignatureMethod.java | 1027 ++++++----------- .../internal/dom/DOMXMLSignatureFactory.java | 76 +- 2 files changed, 330 insertions(+), 773 deletions(-) diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java index cf2696841..714453d2b 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java @@ -38,6 +38,8 @@ import java.security.spec.AlgorithmParameterSpec; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; +import java.util.HashMap; +import java.util.Map; import javax.xml.crypto.MarshalException; import javax.xml.crypto.dsig.SignatureMethod; @@ -136,6 +138,171 @@ public abstract class DOMSignatureMethod extends AbstractDOMSignatureMethod { static final String RSA_SHA3_512_MGF1 = "http://www.w3.org/2007/05/xmldsig-more#sha3-512-rsa-MGF1"; + // ================================================================== + // Algorithm registry. + // + // Most SignatureMethod algorithms below differ only by algorithm URI + // and underlying JCA algorithm name(s); rather than a dedicated + // subclass per algorithm, a single class per algorithm "shape" (see + // RSASignatureMethod, RSAPSSSignatureMethod, DSASignatureMethod, + // ECDSASignatureMethod, EDDSASignatureMethod, MLDSASignatureMethod + // below) is parameterized with that data and looked up here by URI. + // A couple of special cases that take caller-supplied parameters + // (generic RSA-PSS, HMAC output length) don't fit this shape - they + // stay directly constructed by their callers instead of going + // through this map; see unmarshal() below and + // DOMXMLSignatureFactory#newSignatureMethod. + // ================================================================== + + @FunctionalInterface + interface ParamsConstructor { + DOMSignatureMethod newInstance(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException; + } + + @FunctionalInterface + interface ElementConstructor { + DOMSignatureMethod newInstance(Element dmElem) throws MarshalException; + } + + static final class AlgEntry { + final ParamsConstructor paramsConstructor; + final ElementConstructor elementConstructor; + + AlgEntry(ParamsConstructor paramsConstructor, ElementConstructor elementConstructor) { + this.paramsConstructor = paramsConstructor; + this.elementConstructor = elementConstructor; + } + } + + private static final Map ALGORITHMS = new HashMap<>(); + + private static void register(String algorithmURI, ParamsConstructor paramsConstructor, + ElementConstructor elementConstructor) { + ALGORITHMS.put(algorithmURI, new AlgEntry(paramsConstructor, elementConstructor)); + } + + private static void registerRSAPSS(String algorithmURI, String jcaFallbackAlgorithm, + PSSParameterSpec pssParameterSpec) { + register(algorithmURI, + p -> new RSAPSSSignatureMethod(algorithmURI, jcaFallbackAlgorithm, pssParameterSpec, p), + e -> new RSAPSSSignatureMethod(algorithmURI, jcaFallbackAlgorithm, pssParameterSpec, e)); + } + + private static void registerECDSA(String algorithmURI, String jcaDigestName) { + register(algorithmURI, + p -> new ECDSASignatureMethod(algorithmURI, + jcaDigestName + "withECDSAinP1363Format", jcaDigestName + "withECDSA", p), + e -> new ECDSASignatureMethod(algorithmURI, + jcaDigestName + "withECDSAinP1363Format", jcaDigestName + "withECDSA", e)); + } + + static { + register(SignatureMethod.RSA_SHA1, + p -> new RSASignatureMethod(SignatureMethod.RSA_SHA1, "SHA1withRSA", p), + e -> new RSASignatureMethod(SignatureMethod.RSA_SHA1, "SHA1withRSA", e)); + register(RSA_SHA224, + p -> new RSASignatureMethod(RSA_SHA224, "SHA224withRSA", p), + e -> new RSASignatureMethod(RSA_SHA224, "SHA224withRSA", e)); + register(RSA_SHA256, + p -> new RSASignatureMethod(RSA_SHA256, "SHA256withRSA", p), + e -> new RSASignatureMethod(RSA_SHA256, "SHA256withRSA", e)); + register(RSA_SHA384, + p -> new RSASignatureMethod(RSA_SHA384, "SHA384withRSA", p), + e -> new RSASignatureMethod(RSA_SHA384, "SHA384withRSA", e)); + register(RSA_SHA512, + p -> new RSASignatureMethod(RSA_SHA512, "SHA512withRSA", p), + e -> new RSASignatureMethod(RSA_SHA512, "SHA512withRSA", e)); + register(RSA_RIPEMD160, + p -> new RSASignatureMethod(RSA_RIPEMD160, "RIPEMD160withRSA", p), + e -> new RSASignatureMethod(RSA_RIPEMD160, "RIPEMD160withRSA", e)); + // Unlike the other *_MGF1 algorithms below, RSA_RIPEMD160_MGF1 has always gone + // through the plain RSA path rather than RSASSA-PSS parameterization. + register(RSA_RIPEMD160_MGF1, + p -> new RSASignatureMethod(RSA_RIPEMD160_MGF1, "RIPEMD160withRSAandMGF1", p), + e -> new RSASignatureMethod(RSA_RIPEMD160_MGF1, "RIPEMD160withRSAandMGF1", e)); + + registerRSAPSS(RSA_SHA1_MGF1, "SHA1withRSAandMGF1", + new PSSParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, + 20, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA224_MGF1, "SHA224withRSAandMGF1", + new PSSParameterSpec("SHA-224", "MGF1", MGF1ParameterSpec.SHA224, + 28, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA256_MGF1, "SHA256withRSAandMGF1", + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, + 32, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA384_MGF1, "SHA384withRSAandMGF1", + new PSSParameterSpec("SHA-384", "MGF1", MGF1ParameterSpec.SHA384, + 48, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA512_MGF1, "SHA512withRSAandMGF1", + new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, + 64, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA3_224_MGF1, "SHA3-224withRSAandMGF1", + new PSSParameterSpec("SHA3-224", "MGF1", + new MGF1ParameterSpec("SHA3-224"), 28, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA3_256_MGF1, "SHA3-256withRSAandMGF1", + new PSSParameterSpec("SHA3-256", "MGF1", + new MGF1ParameterSpec("SHA3-256"), 32, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA3_384_MGF1, "SHA3-384withRSAandMGF1", + new PSSParameterSpec("SHA3-384", "MGF1", + new MGF1ParameterSpec("SHA3-384"), 48, PSSParameterSpec.TRAILER_FIELD_BC)); + registerRSAPSS(RSA_SHA3_512_MGF1, "SHA3-512withRSAandMGF1", + new PSSParameterSpec("SHA3-512", "MGF1", + new MGF1ParameterSpec("SHA3-512"), 64, PSSParameterSpec.TRAILER_FIELD_BC)); + + register(SignatureMethod.DSA_SHA1, + p -> new DSASignatureMethod(SignatureMethod.DSA_SHA1, + "SHA1withDSAinP1363Format", "SHA1withDSA", p), + e -> new DSASignatureMethod(SignatureMethod.DSA_SHA1, + "SHA1withDSAinP1363Format", "SHA1withDSA", e)); + register(DSA_SHA256, + p -> new DSASignatureMethod(DSA_SHA256, + "SHA256withDSAinP1363Format", "SHA256withDSA", p), + e -> new DSASignatureMethod(DSA_SHA256, + "SHA256withDSAinP1363Format", "SHA256withDSA", e)); + + registerECDSA(ECDSA_SHA1, "SHA1"); + registerECDSA(ECDSA_SHA224, "SHA224"); + registerECDSA(ECDSA_SHA256, "SHA256"); + registerECDSA(ECDSA_SHA384, "SHA384"); + registerECDSA(ECDSA_SHA512, "SHA512"); + registerECDSA(ECDSA_SHA3_224, "SHA3-224"); + registerECDSA(ECDSA_SHA3_256, "SHA3-256"); + registerECDSA(ECDSA_SHA3_384, "SHA3-384"); + registerECDSA(ECDSA_SHA3_512, "SHA3-512"); + // "RIPEMD160withECDSAinP1363Format" - is this real? kept as-is from the + // pre-existing per-algorithm implementation. + registerECDSA(ECDSA_RIPEMD160, "RIPEMD160"); + + register(ED25519, + p -> new EDDSASignatureMethod(ED25519, "Ed25519", p), + e -> new EDDSASignatureMethod(ED25519, "Ed25519", e)); + register(ED448, + p -> new EDDSASignatureMethod(ED448, "Ed448", p), + e -> new EDDSASignatureMethod(ED448, "Ed448", e)); + + register(ML_DSA_44, + p -> new MLDSASignatureMethod(ML_DSA_44, "ML-DSA-44", p), + e -> new MLDSASignatureMethod(ML_DSA_44, "ML-DSA-44", e)); + register(ML_DSA_65, + p -> new MLDSASignatureMethod(ML_DSA_65, "ML-DSA-65", p), + e -> new MLDSASignatureMethod(ML_DSA_65, "ML-DSA-65", e)); + register(ML_DSA_87, + p -> new MLDSASignatureMethod(ML_DSA_87, "ML-DSA-87", p), + e -> new MLDSASignatureMethod(ML_DSA_87, "ML-DSA-87", e)); + } + + /** + * Looks up the algorithm registered for {@code algorithmURI}, if any. Used by + * both {@link #unmarshal unmarshal} (inbound, from an Element) and + * {@code DOMXMLSignatureFactory#newSignatureMethod} (outbound, from caller + * params) so the two entry points share one algorithm table instead of two + * separately maintained dispatch chains. + */ + static AlgEntry lookup(String algorithmURI) { + return ALGORITHMS.get(algorithmURI); + } + /** * Creates a DOMSignatureMethod. * @@ -204,64 +371,8 @@ public abstract class DOMSignatureMethod extends AbstractDOMSignatureMethod { static SignatureMethod unmarshal(Element smElem) throws MarshalException { String alg = DOMUtils.getAttributeValue(smElem, "Algorithm"); - if (alg.equals(SignatureMethod.RSA_SHA1)) { - return new SHA1withRSA(smElem); - } else if (alg.equals(RSA_SHA224)) { - return new SHA224withRSA(smElem); - } else if (alg.equals(RSA_SHA256)) { - return new SHA256withRSA(smElem); - } else if (alg.equals(RSA_SHA384)) { - return new SHA384withRSA(smElem); - } else if (alg.equals(RSA_SHA512)) { - return new SHA512withRSA(smElem); - } else if (alg.equals(RSA_RIPEMD160)) { - return new RIPEMD160withRSA(smElem); - } else if (alg.equals(RSA_SHA1_MGF1)) { - return new SHA1withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA224_MGF1)) { - return new SHA224withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA256_MGF1)) { - return new SHA256withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA384_MGF1)) { - return new SHA384withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA512_MGF1)) { - return new SHA512withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA3_224_MGF1)) { - return new SHA3_224withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA3_256_MGF1)) { - return new SHA3_256withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA3_384_MGF1)) { - return new SHA3_384withRSAandMGF1(smElem); - } else if (alg.equals(RSA_SHA3_512_MGF1)) { - return new SHA3_512withRSAandMGF1(smElem); - } else if (alg.equals(DOMRSAPSSSignatureMethod.RSA_PSS)) { + if (alg.equals(DOMRSAPSSSignatureMethod.RSA_PSS)) { return new DOMRSAPSSSignatureMethod.RSAPSS(smElem); - } else if (alg.equals(RSA_RIPEMD160_MGF1)) { - return new RIPEMD160withRSAandMGF1(smElem); - } else if (alg.equals(SignatureMethod.DSA_SHA1)) { - return new SHA1withDSA(smElem); - } else if (alg.equals(DSA_SHA256)) { - return new SHA256withDSA(smElem); - } else if (alg.equals(ECDSA_SHA1)) { - return new SHA1withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA224)) { - return new SHA224withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA256)) { - return new SHA256withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA384)) { - return new SHA384withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA512)) { - return new SHA512withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA3_224)) { - return new SHA3_224withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA3_256)) { - return new SHA3_256withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA3_384)) { - return new SHA3_384withECDSA(smElem); - } else if (alg.equals(ECDSA_SHA3_512)) { - return new SHA3_512withECDSA(smElem); - } else if (alg.equals(ECDSA_RIPEMD160)) { - return new RIPEMD160withECDSA(smElem); } else if (alg.equals(SignatureMethod.HMAC_SHA1)) { return new DOMHMACSignatureMethod.SHA1(smElem); } else if (alg.equals(DOMHMACSignatureMethod.HMAC_SHA224)) { @@ -274,20 +385,13 @@ static SignatureMethod unmarshal(Element smElem) throws MarshalException { return new DOMHMACSignatureMethod.SHA512(smElem); } else if (alg.equals(DOMHMACSignatureMethod.HMAC_RIPEMD160)) { return new DOMHMACSignatureMethod.RIPEMD160(smElem); - } else if (alg.equals(ED25519)) { - return new EDDSA_ED25519(smElem); - } else if (alg.equals(ED448)) { - return new EDDSA_ED448(smElem); - } else if (alg.equals(ML_DSA_44)) { - return new MLDSA_44(smElem); - } else if (alg.equals(ML_DSA_65)) { - return new MLDSA_65(smElem); - } else if (alg.equals(ML_DSA_87)) { - return new MLDSA_87(smElem); - } else { + } + AlgEntry entry = ALGORITHMS.get(alg); + if (entry == null) { throw new MarshalException ("unsupported SignatureMethod algorithm: " + alg); } + return entry.elementConstructor.newInstance(smElem); } @Override @@ -417,7 +521,7 @@ Type getAlgorithmType() { abstract static class AbstractRSAPSSSignatureMethod extends AbstractRSASignatureMethod { - + AbstractRSAPSSSignatureMethod(AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); @@ -627,766 +731,279 @@ Type getAlgorithmType() { } } - static final class SHA1withRSA extends AbstractRSASignatureMethod { - SHA1withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA1withRSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return SignatureMethod.RSA_SHA1; - } - @Override - String getJCAAlgorithm() { - return "SHA1withRSA"; - } - } - - static final class SHA224withRSA extends AbstractRSASignatureMethod { - SHA224withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA224withRSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA224; - } - @Override - String getJCAAlgorithm() { - return "SHA224withRSA"; - } - } + abstract static class AbstractMLDSASignatureMethod extends DOMSignatureMethod { - static final class SHA256withRSA extends AbstractRSASignatureMethod { - SHA256withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { + AbstractMLDSASignatureMethod(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { super(params); } - SHA256withRSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA256; - } - @Override - String getJCAAlgorithm() { - return "SHA256withRSA"; - } - } - static final class SHA384withRSA extends AbstractRSASignatureMethod { - SHA384withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA384withRSA(Element dmElem) throws MarshalException { + AbstractMLDSASignatureMethod(Element dmElem) throws MarshalException { super(dmElem); } - @Override - public String getAlgorithm() { - return RSA_SHA384; - } - @Override - String getJCAAlgorithm() { - return "SHA384withRSA"; - } - } - static final class SHA512withRSA extends AbstractRSASignatureMethod { - SHA512withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA512withRSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA512; - } + /** ML-DSA signatures are raw bytes; no reformatting needed. */ @Override - String getJCAAlgorithm() { - return "SHA512withRSA"; + byte[] postSignFormat(Key key, byte[] sig) { + return sig; } - } - static final class RIPEMD160withRSA extends AbstractRSASignatureMethod { - RIPEMD160withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - RIPEMD160withRSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_RIPEMD160; - } + /** ML-DSA signatures are raw bytes; no reformatting needed. */ @Override - String getJCAAlgorithm() { - return "RIPEMD160withRSA"; + byte[] preVerifyFormat(Key key, byte[] sig) { + return sig; } - } - - static final class SHA1withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - private static final PSSParameterSpec SHA1_MGF1_PARAMS - = new PSSParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, - 20, PSSParameterSpec.TRAILER_FIELD_BC); - - SHA1withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA1withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } @Override - public String getAlgorithm() { - return RSA_SHA1_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA1_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA1withRSAandMGF1"; + Type getAlgorithmType() { + return Type.MLDSA; } } - static final class SHA224withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - - private static final PSSParameterSpec SHA224_MGF1_PARAMS - = new PSSParameterSpec("SHA-224", "MGF1", MGF1ParameterSpec.SHA224, - 28, PSSParameterSpec.TRAILER_FIELD_BC); + /** + * A plain RSA signature algorithm (no P1363 conversion, no PSS parameters), + * e.g. SHA256withRSA. Replaces what used to be one dedicated subclass per + * algorithm URI; see the {@code register(...)} calls above for the concrete + * (URI, JCA algorithm name) pairs. + */ + static final class RSASignatureMethod extends AbstractRSASignatureMethod { + private final String algorithmURI; + private final String jcaAlgorithm; - SHA224withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { + RSASignatureMethod(String algorithmURI, String jcaAlgorithm, + AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - SHA224withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA224_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA224_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA224withRSAandMGF1"; - } - } - - static final class SHA256withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - - private static final PSSParameterSpec SHA256_MGF1_PARAMS - = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, - 32, PSSParameterSpec.TRAILER_FIELD_BC); - SHA256withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA256withRSAandMGF1(Element dmElem) throws MarshalException { + RSASignatureMethod(String algorithmURI, String jcaAlgorithm, Element dmElem) + throws MarshalException { super(dmElem); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - @Override - public String getAlgorithm() { - return RSA_SHA256_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA256_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA256withRSAandMGF1"; - } - } - - static final class SHA384withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - private static final PSSParameterSpec SHA384_MGF1_PARAMS - = new PSSParameterSpec("SHA-384", "MGF1", MGF1ParameterSpec.SHA384, - 48, PSSParameterSpec.TRAILER_FIELD_BC); - - SHA384withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA384withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } @Override public String getAlgorithm() { - return RSA_SHA384_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA384_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA384withRSAandMGF1"; + return algorithmURI; } - } - - static final class SHA512withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - - private static final PSSParameterSpec SHA512_MGF1_PARAMS - = new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, - 64, PSSParameterSpec.TRAILER_FIELD_BC); - SHA512withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA512withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA512_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA512_MGF1_PARAMS; - } @Override String getJCAAlgorithm() { - return "SHA512withRSAandMGF1"; + return jcaAlgorithm; } } - static final class SHA3_224withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - - private static final PSSParameterSpec SHA3_224_MGF1_PARAMS - = new PSSParameterSpec("SHA3-224", "MGF1", - new MGF1ParameterSpec("SHA3-224"), 28, - PSSParameterSpec.TRAILER_FIELD_BC); - - SHA3_224withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { + /** + * An RSASSA-PSS signature algorithm with a fixed (algorithm-specific) + * {@link PSSParameterSpec}, e.g. SHA256withRSAandMGF1. Replaces what used + * to be one dedicated subclass per digest; see the {@code registerRSAPSS(...)} + * calls above. Distinct from the generic {@code RSA-PSS} algorithm + * (see {@link DOMRSAPSSSignatureMethod}), whose PSS parameters are supplied + * by the caller rather than fixed per URI - that one is still constructed + * directly, not through this registry. + */ + static final class RSAPSSSignatureMethod extends AbstractRSAPSSSignatureMethod { + private final String algorithmURI; + private final String jcaFallbackAlgorithm; + private final PSSParameterSpec pssParameterSpec; + + RSAPSSSignatureMethod(String algorithmURI, String jcaFallbackAlgorithm, + PSSParameterSpec pssParameterSpec, + AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaFallbackAlgorithm = jcaFallbackAlgorithm; + this.pssParameterSpec = pssParameterSpec; } - SHA3_224withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA3_224_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA3_224_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA3-224withRSAandMGF1"; - } - } - static final class SHA3_256withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - - private static final PSSParameterSpec SHA3_256_MGF1_PARAMS - = new PSSParameterSpec("SHA3-256", "MGF1", - new MGF1ParameterSpec("SHA3-256"), 32, - PSSParameterSpec.TRAILER_FIELD_BC); - - SHA3_256withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA3_256withRSAandMGF1(Element dmElem) throws MarshalException { + RSAPSSSignatureMethod(String algorithmURI, String jcaFallbackAlgorithm, + PSSParameterSpec pssParameterSpec, Element dmElem) + throws MarshalException { super(dmElem); + this.algorithmURI = algorithmURI; + this.jcaFallbackAlgorithm = jcaFallbackAlgorithm; + this.pssParameterSpec = pssParameterSpec; } - @Override - public String getAlgorithm() { - return RSA_SHA3_256_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA3_256_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA3-256withRSAandMGF1"; - } - } - - static final class SHA3_384withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - private static final PSSParameterSpec SHA3_384_MGF1_PARAMS - = new PSSParameterSpec("SHA3-384", "MGF1", - new MGF1ParameterSpec("SHA3-384"), 48, - PSSParameterSpec.TRAILER_FIELD_BC); - - SHA3_384withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA3_384withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } @Override public String getAlgorithm() { - return RSA_SHA3_384_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA3_384_MGF1_PARAMS; - } - @Override - String getJCAAlgorithm() { - return "SHA3-384withRSAandMGF1"; + return algorithmURI; } - } - - static final class SHA3_512withRSAandMGF1 extends AbstractRSAPSSSignatureMethod { - private static final PSSParameterSpec SHA3_512_MGF1_PARAMS - = new PSSParameterSpec("SHA3-512", "MGF1", - new MGF1ParameterSpec("SHA3-512"), 64, - PSSParameterSpec.TRAILER_FIELD_BC); - - SHA3_512withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA3_512withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA3_512_MGF1; - } @Override public PSSParameterSpec getPSSParameterSpec() { - return SHA3_512_MGF1_PARAMS; + return pssParameterSpec; } - @Override - String getJCAAlgorithm() { - return "SHA3-512withRSAandMGF1"; - } - } - static final class RIPEMD160withRSAandMGF1 extends AbstractRSASignatureMethod { - RIPEMD160withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - RIPEMD160withRSAandMGF1(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_RIPEMD160_MGF1; - } + /** + * The provider-specific fallback name used when the generic + * {@code RSASSA-PSS} algorithm isn't available (see + * {@link AbstractRSAPSSSignatureMethod#getSignature}). + */ @Override String getJCAAlgorithm() { - return "RIPEMD160withRSAandMGF1"; + return jcaFallbackAlgorithm; } } - static final class SHA1withDSA extends AbstractDSASignatureMethod { - SHA1withDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA1withDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return SignatureMethod.DSA_SHA1; - } - @Override - String getJCAAlgorithm() { - return "SHA1withDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA1withDSA"; - } - } + /** + * A DSA signature algorithm using the IEEE P1363 format, with an ASN.1 + * fallback, e.g. SHA256withDSA. Replaces what used to be one dedicated + * subclass per digest; see the {@code register(...)} calls above. + */ + static final class DSASignatureMethod extends AbstractDSASignatureMethod { + private final String algorithmURI; + private final String jcaAlgorithm; + private final String jcaFallbackAlgorithm; - static final class SHA256withDSA extends AbstractDSASignatureMethod { - SHA256withDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { + DSASignatureMethod(String algorithmURI, String jcaAlgorithm, + String jcaFallbackAlgorithm, AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; + this.jcaFallbackAlgorithm = jcaFallbackAlgorithm; } - SHA256withDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return DSA_SHA256; - } - @Override - String getJCAAlgorithm() { - return "SHA256withDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA256withDSA"; - } - } - static final class SHA1withECDSA extends AbstractECDSASignatureMethod { - SHA1withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA1withECDSA(Element dmElem) throws MarshalException { + DSASignatureMethod(String algorithmURI, String jcaAlgorithm, + String jcaFallbackAlgorithm, Element dmElem) + throws MarshalException { super(dmElem); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; + this.jcaFallbackAlgorithm = jcaFallbackAlgorithm; } - @Override - public String getAlgorithm() { - return ECDSA_SHA1; - } - @Override - String getJCAAlgorithm() { - return "SHA1withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA1withECDSA"; - } - } - static final class SHA224withECDSA extends AbstractECDSASignatureMethod { - SHA224withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA224withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } @Override public String getAlgorithm() { - return ECDSA_SHA224; + return algorithmURI; } - @Override - String getJCAAlgorithm() { - return "SHA224withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA224withECDSA"; - } - } - static final class SHA256withECDSA extends AbstractECDSASignatureMethod { - SHA256withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA256withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ECDSA_SHA256; - } @Override String getJCAAlgorithm() { - return "SHA256withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA256withECDSA"; + return jcaAlgorithm; } - } - static final class SHA384withECDSA extends AbstractECDSASignatureMethod { - SHA384withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA384withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ECDSA_SHA384; - } - @Override - String getJCAAlgorithm() { - return "SHA384withECDSAinP1363Format"; - } @Override String getJCAFallbackAlgorithm() { - return "SHA384withECDSA"; + return jcaFallbackAlgorithm; } } - static final class SHA512withECDSA extends AbstractECDSASignatureMethod { - SHA512withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA512withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ECDSA_SHA512; - } - @Override - String getJCAAlgorithm() { - return "SHA512withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA512withECDSA"; - } - } + /** + * An ECDSA signature algorithm using the IEEE P1363 format, with an ASN.1 + * fallback, e.g. SHA256withECDSA. Replaces what used to be one dedicated + * subclass per digest; see the {@code registerECDSA(...)} calls above. + */ + static final class ECDSASignatureMethod extends AbstractECDSASignatureMethod { + private final String algorithmURI; + private final String jcaAlgorithm; + private final String jcaFallbackAlgorithm; - static final class SHA3_224withECDSA extends AbstractECDSASignatureMethod { - SHA3_224withECDSA(AlgorithmParameterSpec params) + ECDSASignatureMethod(String algorithmURI, String jcaAlgorithm, + String jcaFallbackAlgorithm, AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; + this.jcaFallbackAlgorithm = jcaFallbackAlgorithm; } - SHA3_224withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ECDSA_SHA3_224; - } - @Override - String getJCAAlgorithm() { - return "SHA3-224withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA3-224withECDSA"; - } - } - static final class SHA3_256withECDSA extends AbstractECDSASignatureMethod { - SHA3_256withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA3_256withECDSA(Element dmElem) throws MarshalException { + ECDSASignatureMethod(String algorithmURI, String jcaAlgorithm, + String jcaFallbackAlgorithm, Element dmElem) + throws MarshalException { super(dmElem); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; + this.jcaFallbackAlgorithm = jcaFallbackAlgorithm; } - @Override - public String getAlgorithm() { - return ECDSA_SHA3_256; - } - @Override - String getJCAAlgorithm() { - return "SHA3-256withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA3-256withECDSA"; - } - } - static final class SHA3_384withECDSA extends AbstractECDSASignatureMethod { - SHA3_384withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA3_384withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } @Override public String getAlgorithm() { - return ECDSA_SHA3_384; + return algorithmURI; } - @Override - String getJCAAlgorithm() { - return "SHA3-384withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA3-384withECDSA"; - } - } - static final class SHA3_512withECDSA extends AbstractECDSASignatureMethod { - SHA3_512withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA3_512withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ECDSA_SHA3_512; - } @Override String getJCAAlgorithm() { - return "SHA3-512withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA3-512withECDSA"; + return jcaAlgorithm; } - } - static final class RIPEMD160withECDSA extends AbstractECDSASignatureMethod { - RIPEMD160withECDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - RIPEMD160withECDSA(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ECDSA_RIPEMD160; - } - @Override - String getJCAAlgorithm() { - return "RIPEMD160withECDSAinP1363Format"; // Is this real? - } @Override String getJCAFallbackAlgorithm() { - return "RIPEMD160withECDSA"; + return jcaFallbackAlgorithm; } } - static final class EDDSA_ED25519 extends AbstractEDDSASignatureMethod { - - EDDSA_ED25519(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - - EDDSA_ED25519(Element dmElem) throws MarshalException { - super(dmElem); - } - - @Override - public String getAlgorithm() { - return ED25519; - } - - @Override - String getJCAAlgorithm() { - return "Ed25519"; - } - } + /** + * An EdDSA signature algorithm, e.g. Ed25519. Replaces what used to be one + * dedicated subclass per curve; see the {@code register(...)} calls above. + */ + static final class EDDSASignatureMethod extends AbstractEDDSASignatureMethod { + private final String algorithmURI; + private final String jcaAlgorithm; - static final class EDDSA_ED448 extends AbstractEDDSASignatureMethod { - EDDSA_ED448(AlgorithmParameterSpec params) + EDDSASignatureMethod(String algorithmURI, String jcaAlgorithm, + AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - EDDSA_ED448(Element dmElem) throws MarshalException { + EDDSASignatureMethod(String algorithmURI, String jcaAlgorithm, Element dmElem) + throws MarshalException { super(dmElem); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } @Override public String getAlgorithm() { - return ED448; + return algorithmURI; } @Override String getJCAAlgorithm() { - return "Ed448"; + return jcaAlgorithm; } } - abstract static class AbstractMLDSASignatureMethod extends DOMSignatureMethod { + /** + * An ML-DSA (FIPS 204) signature algorithm, e.g. ML-DSA-65. Replaces what + * used to be one dedicated subclass per parameter set; see the + * {@code register(...)} calls above. + */ + static final class MLDSASignatureMethod extends AbstractMLDSASignatureMethod { + private final String algorithmURI; + private final String jcaAlgorithm; - AbstractMLDSASignatureMethod(AlgorithmParameterSpec params) + MLDSASignatureMethod(String algorithmURI, String jcaAlgorithm, + AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - AbstractMLDSASignatureMethod(Element dmElem) throws MarshalException { - super(dmElem); - } - - /** ML-DSA signatures are raw bytes; no reformatting needed. */ - @Override - byte[] postSignFormat(Key key, byte[] sig) { - return sig; - } - - /** ML-DSA signatures are raw bytes; no reformatting needed. */ - @Override - byte[] preVerifyFormat(Key key, byte[] sig) { - return sig; - } - - @Override - Type getAlgorithmType() { - return Type.MLDSA; - } - } - - static final class MLDSA_44 extends AbstractMLDSASignatureMethod { - MLDSA_44(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - MLDSA_44(Element dmElem) throws MarshalException { + MLDSASignatureMethod(String algorithmURI, String jcaAlgorithm, Element dmElem) + throws MarshalException { super(dmElem); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - @Override - public String getAlgorithm() { - return ML_DSA_44; - } - @Override - String getJCAAlgorithm() { - return "ML-DSA-44"; - } - } - static final class MLDSA_65 extends AbstractMLDSASignatureMethod { - MLDSA_65(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - MLDSA_65(Element dmElem) throws MarshalException { - super(dmElem); - } @Override public String getAlgorithm() { - return ML_DSA_65; - } - @Override - String getJCAAlgorithm() { - return "ML-DSA-65"; + return algorithmURI; } - } - static final class MLDSA_87 extends AbstractMLDSASignatureMethod { - MLDSA_87(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - MLDSA_87(Element dmElem) throws MarshalException { - super(dmElem); - } - @Override - public String getAlgorithm() { - return ML_DSA_87; - } @Override String getJCAAlgorithm() { - return "ML-DSA-87"; + return jcaAlgorithm; } } } diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java index 94074a211..fe180a7cf 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java @@ -281,44 +281,11 @@ public SignatureMethod newSignatureMethod(String algorithm, if (algorithm == null) { throw new NullPointerException(); } - if (algorithm.equals(SignatureMethod.RSA_SHA1)) { - return new DOMSignatureMethod.SHA1withRSA(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA224)) { - return new DOMSignatureMethod.SHA224withRSA(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA256)) { - return new DOMSignatureMethod.SHA256withRSA(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA384)) { - return new DOMSignatureMethod.SHA384withRSA(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA512)) { - return new DOMSignatureMethod.SHA512withRSA(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_RIPEMD160)) { - return new DOMSignatureMethod.RIPEMD160withRSA(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA1_MGF1)) { - return new DOMSignatureMethod.SHA1withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA224_MGF1)) { - return new DOMSignatureMethod.SHA224withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA256_MGF1)) { - return new DOMSignatureMethod.SHA256withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA384_MGF1)) { - return new DOMSignatureMethod.SHA384withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA512_MGF1)) { - return new DOMSignatureMethod.SHA512withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA3_224_MGF1)) { - return new DOMSignatureMethod.SHA3_224withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA3_256_MGF1)) { - return new DOMSignatureMethod.SHA3_256withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA3_384_MGF1)) { - return new DOMSignatureMethod.SHA3_384withRSAandMGF1(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_SHA3_512_MGF1)) { - return new DOMSignatureMethod.SHA3_512withRSAandMGF1(params); - } else if (algorithm.equals(DOMRSAPSSSignatureMethod.RSA_PSS)) { + // A few algorithms take caller-supplied parameters that don't fit the + // shared (URI -> JCA algorithm name) registry below, so they are + // constructed directly here rather than looked up. + if (algorithm.equals(DOMRSAPSSSignatureMethod.RSA_PSS)) { return new DOMRSAPSSSignatureMethod.RSAPSS(params); - } else if (algorithm.equals(DOMSignatureMethod.RSA_RIPEMD160_MGF1)) { - return new DOMSignatureMethod.RIPEMD160withRSAandMGF1(params); - } else if (algorithm.equals(SignatureMethod.DSA_SHA1)) { - return new DOMSignatureMethod.SHA1withDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.DSA_SHA256)) { - return new DOMSignatureMethod.SHA256withDSA(params); } else if (algorithm.equals(SignatureMethod.HMAC_SHA1)) { return new DOMHMACSignatureMethod.SHA1(params); } else if (algorithm.equals(DOMHMACSignatureMethod.HMAC_SHA224)) { @@ -331,39 +298,12 @@ public SignatureMethod newSignatureMethod(String algorithm, return new DOMHMACSignatureMethod.SHA512(params); } else if (algorithm.equals(DOMHMACSignatureMethod.HMAC_RIPEMD160)) { return new DOMHMACSignatureMethod.RIPEMD160(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA1)) { - return new DOMSignatureMethod.SHA1withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA224)) { - return new DOMSignatureMethod.SHA224withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA256)) { - return new DOMSignatureMethod.SHA256withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA384)) { - return new DOMSignatureMethod.SHA384withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA512)) { - return new DOMSignatureMethod.SHA512withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA3_224)) { - return new DOMSignatureMethod.SHA3_224withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA3_256)) { - return new DOMSignatureMethod.SHA3_256withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA3_384)) { - return new DOMSignatureMethod.SHA3_384withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_SHA3_512)) { - return new DOMSignatureMethod.SHA3_512withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ECDSA_RIPEMD160)) { - return new DOMSignatureMethod.RIPEMD160withECDSA(params); - } else if (algorithm.equals(DOMSignatureMethod.ED25519)) { - return new DOMSignatureMethod.EDDSA_ED25519(params); - } else if (algorithm.equals(DOMSignatureMethod.ED448)) { - return new DOMSignatureMethod.EDDSA_ED448(params); - } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_44)) { - return new DOMSignatureMethod.MLDSA_44(params); - } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_65)) { - return new DOMSignatureMethod.MLDSA_65(params); - } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_87)) { - return new DOMSignatureMethod.MLDSA_87(params); - } else { + } + DOMSignatureMethod.AlgEntry entry = DOMSignatureMethod.lookup(algorithm); + if (entry == null) { throw new NoSuchAlgorithmException("unsupported algorithm"); } + return entry.paramsConstructor.newInstance(params); } @Override From e6960fb3c5a6f82669b8b96157864bf79eff256f Mon Sep 17 00:00:00 2001 From: Freeman Fang Date: Wed, 23 Sep 2026 11:03:40 -0400 Subject: [PATCH 09/10] Log swallowed exception when decoding DEREncodedKeyValue DEREncodedKeyValue#getPublicKey() tries each supported key type's KeyFactory in turn and quietly discards any failure before trying the next one. Because this catch block also handles RuntimeException thrown by some providers, a real decoding problem could go completely unnoticed. Log each failure at DEBUG level, including the key type that was tried and the exception, so problems can be diagnosed without flooding the logs: failing on the non-matching key types is normal. --- .../xml/security/keys/content/DEREncodedKeyValue.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java index 6cbd66b5a..8c7ad3b43 100644 --- a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java +++ b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java @@ -18,6 +18,8 @@ */ package org.apache.xml.security.keys.content; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -36,6 +38,8 @@ */ public class DEREncodedKeyValue extends Signature11ElementProxy implements KeyInfoContent { + private static final Logger LOG = System.getLogger(DEREncodedKeyValue.class.getName()); + /** JCA algorithm key types supported by this implementation. */ private static final String[] supportedKeyTypes = { "RSA", "DSA", "EC", "DiffieHellman", "DH", "XDH", "X25519", "X448", @@ -121,8 +125,9 @@ public PublicKey getPublicKey() throws XMLSecurityException { if (publicKey != null) { return publicKey; } - } catch (NoSuchAlgorithmException | InvalidKeySpecException | RuntimeException e) { //NOPMD - // Do nothing, try the next type. Some providers (e.g. BouncyCastle's XDH/EdDSA + } catch (NoSuchAlgorithmException | InvalidKeySpecException | RuntimeException e) { + LOG.log(Level.DEBUG, () -> "Unable to decode DEREncodedKeyValue as key type " + keyType, e); + // Try the next type. Some providers (e.g. BouncyCastle's XDH/EdDSA // KeyFactorySpi) throw an unchecked exception such as ArrayIndexOutOfBoundsException // instead of InvalidKeySpecException for malformed or short input, which must not // propagate since the encoded key here is untrusted, attacker-controlled content. From 4bab98079413f92d100da874a6bc0763957899aa Mon Sep 17 00:00:00 2001 From: Freeman Fang Date: Fri, 25 Sep 2026 11:45:16 -0400 Subject: [PATCH 10/10] Only check for SignatureContext on ML-DSA signatures The SignatureContext element is only defined for ML-DSA, but the current implementation scanned the whole ds:Signature subtree whatever the signature algorithm was. An outer RSA or ECDSA signature over a document that already carries an ML-DSA signature with a SignatureContext (for example, an enveloping signature used for notarization, or a classical signature added during PQC migration) was wrongly rejected, because the inner element lies within the outer signature's subtree. Both DOMXMLSignature (JSR-105) and the native XMLSignature now return early unless the signature method is ML-DSA-44/65/87. Add testOuterNonMLDSASignatureOverMLDSASignedDocument, which covers JSR-105 sign/verify, native verify, and native sign/verify of an RSA enveloping signature over an ML-DSA signed document. It needs no ML-DSA provider and fails if either fix is reverted. --- .../dsig/internal/dom/DOMXMLSignature.java | 16 ++- .../xml/security/signature/XMLSignature.java | 11 +- .../crypto/dsig/XMLSignatureMLDSATest.java | 100 ++++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java index 081991fbe..3ad0123d8 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignature.java @@ -38,6 +38,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.xml.crypto.KeySelector; import javax.xml.crypto.KeySelectorException; @@ -77,6 +78,12 @@ public final class DOMXMLSignature extends DOMStructure private static final Logger LOG = System.getLogger(DOMXMLSignature.class.getName()); + // ML-DSA URIs, the only algorithms for which a SignatureContext element is defined + private static final Set ML_DSA_ALGORITHMS = Set.of( + DOMSignatureMethod.ML_DSA_44, + DOMSignatureMethod.ML_DSA_65, + DOMSignatureMethod.ML_DSA_87); + private final String id; private final SignatureValue sv; private KeyInfo ki; @@ -349,12 +356,15 @@ public boolean validate(XMLValidateContext vc) * {@code java.security.Signature} API offers no way to pass a signature context * to ML-DSA (see the Non-Goals of JEP 497), so such a signature can be neither * created nor verified correctly here; bail out rather than silently ignoring - * the context. + * the context. The element is only defined for ML-DSA, so signatures using any + * other algorithm are not inspected. */ - private static void checkForUnsupportedSignatureContext(Element sigElem) + private void checkForUnsupportedSignatureContext(Element sigElem) throws XMLSignatureException { - if (sigElem == null) { + String signatureMethodURI = si.getSignatureMethod().getAlgorithm(); + if (sigElem == null || signatureMethodURI == null + || !ML_DSA_ALGORITHMS.contains(signatureMethodURI)) { return; } NodeList contexts = sigElem.getElementsByTagNameNS( diff --git a/src/main/java/org/apache/xml/security/signature/XMLSignature.java b/src/main/java/org/apache/xml/security/signature/XMLSignature.java index dae7a4999..90e88635d 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import javax.crypto.SecretKey; @@ -247,6 +248,10 @@ public final class XMLSignature extends SignatureElementProxy { private static final Logger LOG = System.getLogger(XMLSignature.class.getName()); + // ML-DSA URIs, the only algorithms for which a SignatureContext element is defined + private static final Set ML_DSA_ALGORITHMS = Set.of( + ALGO_ID_SIGNATURE_MLDSA_44, ALGO_ID_SIGNATURE_MLDSA_65, ALGO_ID_SIGNATURE_MLDSA_87); + /** ds:Signature.ds:SignedInfo element */ private final SignedInfo signedInfo; @@ -912,12 +917,16 @@ public void sign(Key signingKey) throws XMLSignatureException { * to ML-DSA (see the Non-Goals of JEP 497), so such a signature can be neither * created nor verified correctly here; bail out rather than silently ignoring * the context and producing/accepting a signature that does not match it. + * The element is only defined for ML-DSA, so signatures using any other + * algorithm are not inspected. * * @throws XMLSignatureException if a {@code SignatureContext} element is present */ private void checkForUnsupportedSignatureContext() throws XMLSignatureException { Element signatureElement = getElement(); - if (signatureElement == null) { + String signatureMethodURI = signedInfo.getSignatureMethodURI(); + if (signatureElement == null || signatureMethodURI == null + || !ML_DSA_ALGORITHMS.contains(signatureMethodURI)) { return; } NodeList contexts = signatureElement.getElementsByTagNameNS( 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 index 84ac9783e..3e924b42f 100644 --- 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 @@ -42,11 +42,13 @@ import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.XMLObject; import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.dom.DOMValidateContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; @@ -68,6 +70,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.w3c.dom.Document; @@ -361,6 +364,103 @@ void testMLDSANativeVerifyRejectsSignatureContext(String signatureAlgorithmURI, Assertions.assertTrue(ex.getMessage().contains("SignatureContext"), ex.getMessage()); } + /** + * SignatureContext is only defined for ML-DSA, so only an ML-DSA signature is checked for it. + * The realistic case is nesting: an outer RSA enveloping signature (e.g. a notarization or a + * classical signature added during PQC migration) whose ds:Object holds a document that already + * carries an ML-DSA signature with a SignatureContext. The element then sits in the outer + * signature's subtree, but belongs to the inner one and must not break the outer signature, + * on either the JSR-105 or the native API. The inner signature is only a structural stand-in + * and is never verified, so this runs without an ML-DSA provider. + */ + @Test + void testOuterNonMLDSASignatureOverMLDSASignedDocument() throws Exception { + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + + // JSR-105 enveloping RSA signature over the ML-DSA signed document + Document doc = TestUtils.newDocument(); + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + Reference ref = fac.newReference("#notarized", fac.newDigestMethod(DigestMethod.SHA256, null)); + SignedInfo si = fac.newSignedInfo( + fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null), + fac.newSignatureMethod(SignatureMethod.RSA_SHA256, null), + Collections.singletonList(ref)); + XMLObject obj = fac.newXMLObject( + Collections.singletonList(new DOMStructure(createMLDSASignedDocument(doc))), "notarized", null, null); + javax.xml.crypto.dsig.XMLSignature sig = + fac.newXMLSignature(si, null, Collections.singletonList(obj), null, null); + sig.sign(new DOMSignContext(keyPair.getPrivate(), doc)); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLUtils.outputDOMc14nWithComments(doc, bos); + byte[] signedXml = bos.toByteArray(); + + Document parsed; + try (ByteArrayInputStream is = new ByteArrayInputStream(signedXml)) { + parsed = XMLUtils.read(is, false); + } + Element outerSig = parsed.getDocumentElement(); + Assertions.assertEquals(1, + outerSig.getElementsByTagNameNS(SIGNATURE_CONTEXT_NS, "SignatureContext").getLength(), + "The inner SignatureContext must lie within the outer signature"); + DOMValidateContext vc = new DOMValidateContext(keyPair.getPublic(), outerSig); + Assertions.assertTrue(fac.unmarshalXMLSignature(vc).validate(vc), + "An outer RSA signature over an ML-DSA signed document must validate"); + + // native verify of the same document + Element object = (Element) outerSig.getElementsByTagNameNS(Constants.SignatureSpecNS, "Object").item(0); + object.setIdAttributeNS(null, Constants._ATT_ID, true); + Assertions.assertTrue(new XMLSignature(outerSig, "").checkSignatureValue(keyPair.getPublic())); + + // native enveloping RSA sign and verify over the ML-DSA signed document + Document nativeDoc = TestUtils.newDocument(); + Element canon = XMLUtils.createElementInSignatureSpace(nativeDoc, Constants._TAG_CANONICALIZATIONMETHOD); + canon.setAttributeNS(null, Constants._ATT_ALGORITHM, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + SignatureAlgorithm sigAlg = new SignatureAlgorithm(nativeDoc, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + XMLSignature signature = new XMLSignature(nativeDoc, null, sigAlg.getElement(), canon); + nativeDoc.appendChild(signature.getElement()); + + ObjectContainer container = new ObjectContainer(nativeDoc); + container.setId("notarized"); + container.appendChild(createMLDSASignedDocument(nativeDoc)); + signature.appendObject(container); + signature.addDocument("#notarized", null, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + + signature.sign(keyPair.getPrivate()); + Assertions.assertTrue(signature.checkSignatureValue(keyPair.getPublic())); + } + + /** + * Builds a document carrying an ML-DSA-65 {@code ds:Signature} with a {@code SignatureContext} + * in its {@code ds:Object}. Only the structure matters here: the signature value is a placeholder. + */ + private static Element createMLDSASignedDocument(Document doc) { + Element signedDoc = doc.createElementNS(null, "SignedDocument"); + Element content = doc.createElementNS(null, "Content"); + content.setTextContent("Some data signed with ML-DSA"); + signedDoc.appendChild(content); + + Element innerSig = XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_SIGNATURE); + innerSig.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:" + innerSig.getPrefix(), Constants.SignatureSpecNS); + Element signedInfo = XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_SIGNEDINFO); + Element sigMethod = XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_SIGNATUREMETHOD); + sigMethod.setAttributeNS(null, Constants._ATT_ALGORITHM, XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65); + signedInfo.appendChild(sigMethod); + innerSig.appendChild(signedInfo); + Element sigValue = XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_SIGNATUREVALUE); + sigValue.setTextContent(Base64.getEncoder().encodeToString(new byte[32])); + innerSig.appendChild(sigValue); + + Element innerObject = XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_OBJECT); + Element ctx = doc.createElementNS(SIGNATURE_CONTEXT_NS, "dsig-more:SignatureContext"); + ctx.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:dsig-more", SIGNATURE_CONTEXT_NS); + ctx.setTextContent(Base64.getEncoder().encodeToString("email-signature".getBytes(StandardCharsets.UTF_8))); + innerObject.appendChild(ctx); + innerSig.appendChild(innerObject); + signedDoc.appendChild(innerSig); + return signedDoc; + } + /** * Inserts a {@code ...} * as the last child of the {@code ds:Signature} element, simulating a signature that carries an