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..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; @@ -94,6 +96,15 @@ 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"; + + // 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/2026/08/xmldsig-more#ml-dsa-44"; + static final String ML_DSA_65 = + "http://www.w3.org/2026/08/xmldsig-more#ml-dsa-65"; + static final String 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 = @@ -127,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. * @@ -195,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)) { @@ -265,14 +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 { + } + AlgEntry entry = ALGORITHMS.get(alg); + if (entry == null) { throw new MarshalException ("unsupported SignatureMethod algorithm: " + alg); } + return entry.elementConstructor.newInstance(smElem); } @Override @@ -402,7 +521,7 @@ Type getAlgorithmType() { abstract static class AbstractRSAPSSSignatureMethod extends AbstractRSASignatureMethod { - + AbstractRSAPSSSignatureMethod(AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); @@ -612,683 +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"; - } - } + abstract static class AbstractMLDSASignatureMethod extends DOMSignatureMethod { - static final class SHA224withRSA extends AbstractRSASignatureMethod { - SHA224withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { + AbstractMLDSASignatureMethod(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"; - } - } - static final class SHA256withRSA extends AbstractRSASignatureMethod { - SHA256withRSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA256withRSA(Element dmElem) throws MarshalException { + AbstractMLDSASignatureMethod(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 { - super(dmElem); - } - @Override - public String getAlgorithm() { - return RSA_SHA384; - } + /** ML-DSA signatures are raw bytes; no reformatting needed. */ @Override - String getJCAAlgorithm() { - return "SHA384withRSA"; + byte[] postSignFormat(Key key, byte[] sig) { + return sig; } - } - 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[] preVerifyFormat(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; - } - @Override - String getJCAAlgorithm() { - return "RIPEMD160withRSA"; + Type getAlgorithmType() { + return Type.MLDSA; } } - 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); + /** + * 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; - SHA1withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { + RSASignatureMethod(String algorithmURI, String jcaAlgorithm, + AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - 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"; - } - } - - 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); - - SHA224withRSAandMGF1(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA224withRSAandMGF1(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_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 { - super(dmElem); - } @Override public String getAlgorithm() { - return RSA_SHA256_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA256_MGF1_PARAMS; + return algorithmURI; } - @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 jcaAlgorithm; } } - 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 { + /** + * 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; } - 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"; - } - } - - 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 { - super(params); - } - SHA3_224withRSAandMGF1(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_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 { - super(dmElem); - } @Override public String getAlgorithm() { - return RSA_SHA3_256_MGF1; - } - @Override - public PSSParameterSpec getPSSParameterSpec() { - return SHA3_256_MGF1_PARAMS; + return algorithmURI; } - @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; + return pssParameterSpec; } - @Override - String getJCAAlgorithm() { - return "SHA3-384withRSAandMGF1"; - } - } - 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; - } + /** + * The provider-specific fallback name used when the generic + * {@code RSASSA-PSS} algorithm isn't available (see + * {@link AbstractRSAPSSSignatureMethod#getSignature}). + */ @Override String getJCAAlgorithm() { - return "SHA3-512withRSAandMGF1"; + return jcaFallbackAlgorithm; } } - 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; - } - @Override - String getJCAAlgorithm() { - return "RIPEMD160withRSAandMGF1"; - } - } + /** + * 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 SHA1withDSA extends AbstractDSASignatureMethod { - SHA1withDSA(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; } - 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"; - } - } - static final class SHA256withDSA extends AbstractDSASignatureMethod { - SHA256withDSA(AlgorithmParameterSpec params) - throws InvalidAlgorithmParameterException { - super(params); - } - SHA256withDSA(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 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 { - super(dmElem); - } @Override public String getAlgorithm() { - return ECDSA_SHA1; + return algorithmURI; } - @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; - } @Override String getJCAAlgorithm() { - return "SHA224withECDSAinP1363Format"; - } - @Override - String getJCAFallbackAlgorithm() { - return "SHA224withECDSA"; + return jcaAlgorithm; } - } - 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 jcaFallbackAlgorithm; } } - 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"; - } - } - - 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; - } - @Override - String getJCAAlgorithm() { - return "SHA3-384withECDSAinP1363Format"; + return algorithmURI; } - @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 { + /** + * 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; - EDDSA_ED25519(AlgorithmParameterSpec params) + EDDSASignatureMethod(String algorithmURI, String jcaAlgorithm, + AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - EDDSA_ED25519(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 ED25519; + return algorithmURI; } @Override String getJCAAlgorithm() { - return "Ed25519"; + return jcaAlgorithm; } } - static final class EDDSA_ED448 extends AbstractEDDSASignatureMethod { - EDDSA_ED448(AlgorithmParameterSpec params) + /** + * 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; + + MLDSASignatureMethod(String algorithmURI, String jcaAlgorithm, + AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { super(params); + this.algorithmURI = algorithmURI; + this.jcaAlgorithm = jcaAlgorithm; } - EDDSA_ED448(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 ED448; + return algorithmURI; } @Override String getJCAAlgorithm() { - return "Ed448"; + return jcaAlgorithm; } } } 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..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; @@ -60,11 +61,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. @@ -75,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; @@ -278,6 +287,8 @@ public boolean validate(XMLValidateContext vc) return validationStatus; } + checkForUnsupportedSignatureContext(localSigElem); + // validate the signature boolean sigValidity = sv.validate(vc); if (!sigValidity) { @@ -339,6 +350,33 @@ 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. The element is only defined for ML-DSA, so signatures using any + * other algorithm are not inspected. + */ + private void checkForUnsupportedSignatureContext(Element sigElem) + throws XMLSignatureException + { + String signatureMethodURI = si.getSignatureMethod().getAlgorithm(); + if (sigElem == null || signatureMethodURI == null + || !ML_DSA_ALGORITHMS.contains(signatureMethodURI)) { + 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 +388,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/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java index 49f93514c..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,33 +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 { + } + DOMSignatureMethod.AlgEntry entry = DOMSignatureMethod.lookup(algorithm); + if (entry == null) { throw new NoSuchAlgorithmException("unsupported algorithm"); } + return entry.paramsConstructor.newInstance(params); } @Override 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..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,10 +38,13 @@ */ 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", "EdDSA", "Ed25519", "Ed448", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSASSA-PSS"}; /** @@ -120,8 +125,12 @@ 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) { + 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. } } throw new XMLSecurityException("DEREncodedKeyValue.UnsupportedEncodedKey"); 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 8e1fa9e9d..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; @@ -51,6 +52,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,6 +213,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, 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/2026/08/xmldsig-more#ml-dsa-44"; + + /**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/2026/08/xmldsig-more#ml-dsa-65"; + + /**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/2026/08/xmldsig-more#ml-dsa-87"; + /**Signature - SHA3-224withECDSA */ public static final String ALGO_ID_SIGNATURE_ECDSA_SHA3_224 = @@ -234,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; @@ -851,6 +869,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 @@ -891,6 +910,33 @@ 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. + * 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(); + String signatureMethodURI = signedInfo.getSignatureMethodURI(); + if (signatureElement == null || signatureMethodURI == null + || !ML_DSA_ALGORITHMS.contains(signatureMethodURI)) { + 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. * @@ -945,6 +991,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/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/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/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..4e8042c05 --- /dev/null +++ b/src/main/java/org/apache/xml/security/stax/impl/securityToken/DEREncodedKeyValueSecurityToken.java @@ -0,0 +1,99 @@ +/** + * 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 | 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"); + } + + @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/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 f6c91db07..f5e11a848 100644 --- a/src/main/resources/security-config.xml +++ b/src/main/resources/security-config.xml @@ -321,6 +321,28 @@ RequiredKey="EC" JCEName="RIPEMD160withECDSA"/> + + + + + + + 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"); + } + } +} diff --git a/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureAbstract.java b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureAbstract.java index 235e4ade7..af92f83a3 100644 --- a/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureAbstract.java +++ b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureAbstract.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; +import javax.xml.crypto.KeySelector; import javax.xml.crypto.dsig.*; import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.dom.DOMValidateContext; @@ -149,6 +150,20 @@ protected void assertValidSignatureWithJcpApi(byte[] signedXml, boolean register } } + /** + * Validates a signed document using the given {@link KeySelector} instead of the + * always-embedded-cert {@link KeySelectors.RawX509KeySelector}, and returns the core + * validity result rather than asserting it - for negative tests that expect validation + * to fail (tampered signature, wrong key, etc). + */ + protected boolean validateSignatureWithJcpApi(byte[] signedXml, KeySelector keySelector) throws Exception { + try (InputStream is = new ByteArrayInputStream(signedXml)) { + DOMValidateContext vc = testInstance.getValidateContext(is, keySelector, false); + updateIdReferences(vc, "SignedElement", "id"); + return testInstance.validate(vc); + } + } + public void updateIdReferences(DOMValidateContext vc, String elementName, String idAttributeName) { Document doc = vc.getNode().getOwnerDocument(); NodeList nl = doc.getElementsByTagName(elementName); diff --git a/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java new file mode 100644 index 000000000..3e924b42f --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java @@ -0,0 +1,501 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.javax.xml.crypto.dsig; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Collections; + +import javax.xml.crypto.AlgorithmMethod; +import javax.xml.crypto.KeySelector; +import javax.xml.crypto.KeySelectorException; +import javax.xml.crypto.KeySelectorResult; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +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; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; +import org.apache.xml.security.algorithms.SignatureAlgorithm; +import org.apache.xml.security.c14n.Canonicalizer; +import org.apache.xml.security.signature.ObjectContainer; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.test.dom.TestUtils; +import org.apache.xml.security.test.javax.xml.crypto.KeySelectors; +import org.apache.xml.security.testutils.JDKTestUtils; +import org.apache.xml.security.testutils.SelfSignedCertGenerator; +import org.apache.xml.security.transforms.Transforms; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.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; + +/** + * Tests for ML-DSA (FIPS 204) XML digital signatures via the + * {@code javax.xml.crypto.dsig.XMLSignatureFactory} DOM API. + * + *

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 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. + * + *

On a pre-24 JDK, 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 auxProviderRegistered; + private static KeyStore keyStore; + + @BeforeAll + static void setUp() { + Security.insertProviderAt( + new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI(), 1); + + // 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 { + 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); + 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 (auxProviderRegistered) { + JDKTestUtils.unregisterAuxiliaryProvider(); + } + } + + @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 JDK 24+ or BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + Assertions.assertNotNull(signedXml); + assertValidSignatureWithJcpApi(signedXml, false); + } + + @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 JDK 24+ or BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + + byte[] tamperedXml = flipByteInSignatureValue(signedXml); + + boolean coreValidity = validateSignatureWithJcpApi(tamperedXml, new KeySelectors.RawX509KeySelector()); + Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); + } + + @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 JDK 24+ or BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alias.toUpperCase()); + 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(); + } + + // ===== 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()); + } + + /** + * 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 + * 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; + } + + @Override + char[] getKeyPassword() { + return KEY_PASSWORD; + } +} 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..424a810ee --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSAKeyValueInboundTest.java @@ -0,0 +1,313 @@ +/** + * 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/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), + "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/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), + "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/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 { + 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(); + } + + /** + * 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/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), + "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(...) + 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(); + } +} 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..1aa5620a5 --- /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/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), + "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); + } + } +} 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..dc7354caf --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java @@ -0,0 +1,199 @@ +/** + * 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.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/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), + "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()); + } + + @ParameterizedTest + @CsvSource({ + "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), + "ML-DSA requires BouncyCastle 1.81+"); + + Document document = signWith(sigAlgorithm, jcaAlgorithm); + Element sigElement = tamperSignatureValue(document); + + XMLSignature signature = new XMLSignature(sigElement, ""); + boolean coreValidity = signature.checkSignatureValue(keyPairs.get(jcaAlgorithm).getPublic()); + Assertions.assertFalse(coreValidity, "A tampered SignatureValue must not validate"); + } + + @ParameterizedTest + @CsvSource({ + "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), + "ML-DSA requires BouncyCastle 1.81+"); + + Document document = signWith(sigAlgorithm, jcaAlgorithm); + Element sigElement = (Element) document.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature").item(0); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaAlgorithm, "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 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(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); + + 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; + } +}