From b24341b0a50b351b1551312c5bdbe1c9fdf42c9a Mon Sep 17 00:00:00 2001 From: Joze RIHTARSIC Date: Sun, 14 Jun 2026 17:34:50 +0200 Subject: [PATCH 1/5] [SANTUARIO-615] Implements Pre/Post processors XMLdSIG API for extensions. --- src/main/java/module-info.java | 1 + .../SignatureExtensionException.java | 60 +++++ .../extension/SignatureProcessor.java | 57 ++++ .../xml/security/signature/XMLSignature.java | 73 +++++ .../extension/SignatureProcessorTest.java | 253 ++++++++++++++++++ 5 files changed, 444 insertions(+) create mode 100644 src/main/java/org/apache/xml/security/extension/SignatureExtensionException.java create mode 100644 src/main/java/org/apache/xml/security/extension/SignatureProcessor.java create mode 100644 src/test/java/org/apache/xml/security/extension/SignatureProcessorTest.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2bd7642cf..ec88429bb 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -32,6 +32,7 @@ exports org.apache.jcp.xml.dsig.internal.dom; exports org.apache.xml.security; + exports org.apache.xml.security.extension; exports org.apache.xml.security.algorithms; exports org.apache.xml.security.algorithms.implementations; exports org.apache.xml.security.c14n; diff --git a/src/main/java/org/apache/xml/security/extension/SignatureExtensionException.java b/src/main/java/org/apache/xml/security/extension/SignatureExtensionException.java new file mode 100644 index 000000000..5dbec9039 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/SignatureExtensionException.java @@ -0,0 +1,60 @@ +/** + * 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.extension; + +import org.apache.xml.security.signature.XMLSignatureException; + +/** + * Thrown by a {@link SignatureProcessor} when it cannot complete its processing + * and the signing operation must be aborted. + * + *

Extends {@link XMLSignatureException} so callers that already handle the + * standard library exception hierarchy will catch this automatically. + */ +public class SignatureExtensionException extends XMLSignatureException { + + private static final long serialVersionUID = 1L; + + private final String detailMessage; + + /** + * @param message human-readable description of the failure + */ + public SignatureExtensionException(String message) { + super(message); + this.detailMessage = message; + } + + /** + * @param message human-readable description of the failure + * @param cause the underlying exception that triggered this failure + */ + public SignatureExtensionException(String message, Throwable cause) { + super(message); + this.detailMessage = message; + if (cause != null) { + initCause(cause); + } + } + + @Override + public String getMessage() { + return detailMessage; + } +} diff --git a/src/main/java/org/apache/xml/security/extension/SignatureProcessor.java b/src/main/java/org/apache/xml/security/extension/SignatureProcessor.java new file mode 100644 index 000000000..fcde0336a --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/SignatureProcessor.java @@ -0,0 +1,57 @@ +/** + * 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.extension; + +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.signature.XMLSignatureException; + +/** + * Extension point for pluggable pre- and post-signature processing hooks in + * the DOM-based XML Signature implementation. + * + *

Instances are registered on an {@link XMLSignature} via + * {@link XMLSignature#addPreProcessor(SignatureProcessor)} or + * {@link XMLSignature#addPostProcessor(SignatureProcessor)}. + * + *

+ * + *

If a processor throws {@link XMLSignatureException} the signing operation + * is aborted and the exception is propagated to the caller of + * {@link XMLSignature#sign(java.security.Key)}. + * + */ +public interface SignatureProcessor { + + /** + * Called during the {@link XMLSignature#sign(java.security.Key)} lifecycle. + * + * @param signature the signature being created; never {@code null} + * @throws XMLSignatureException if processing fails and signing must be aborted + */ + void processSignature(XMLSignature signature) throws XMLSignatureException; +} 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 658bcaf37..8e1fa9e9d 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -27,12 +27,16 @@ import java.security.PublicKey; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import javax.crypto.SecretKey; import org.apache.xml.security.algorithms.SignatureAlgorithm; import org.apache.xml.security.c14n.Canonicalizer; import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.extension.SignatureProcessor; import org.apache.xml.security.keys.KeyInfo; import org.apache.xml.security.keys.content.X509Data; import org.apache.xml.security.transforms.Transforms; @@ -249,6 +253,10 @@ public final class XMLSignature extends SignatureElementProxy { private static final int MODE_VERIFY = 1; private int state = MODE_SIGN; + + private final List preProcessors = new ArrayList<>(); + private final List postProcessors = new ArrayList<>(); + /** * This creates a new ds:Signature Element and adds an empty * ds:SignedInfo. @@ -631,6 +639,29 @@ public XMLSignature(Element element, String baseURI, boolean secureValidation, P this.state = MODE_VERIFY; } + + /** + * Registers a pre-processor that is invoked before digest values are computed. + * Pre-processors run in registration order. + * + * @param processor the pre-processor to register; must not be {@code null} + */ + public void addPreProcessor(SignatureProcessor processor) { + Objects.requireNonNull(processor, "processor"); + preProcessors.add(processor); + } + + /** + * Registers a post-processor that is invoked after the {@code ds:SignatureValue} + * element has been populated. Post-processors run in registration order. + * + * @param processor the post-processor to register; must not be {@code null} + */ + public void addPostProcessor(SignatureProcessor processor) { + Objects.requireNonNull(processor, "processor"); + postProcessors.add(processor); + } + /** * Sets the Id attribute * @@ -690,6 +721,32 @@ private void setSignatureValueElement(byte[] bytes) { signatureValueElement.appendChild(t); } + /** + * Sets an {@code Id} attribute on the {@code ds:SignatureValue} element so + * it can be referenced from unsigned signature properties (e.g., XAdES-T). + * + * @param id the identifier value; {@code null} removes an existing attribute + */ + public void setSignatureValueId(String id) { + if (id != null) { + signatureValueElement.setAttributeNS(null, Constants._ATT_ID, id); + signatureValueElement.setIdAttributeNS(null, Constants._ATT_ID, true); + } else { + signatureValueElement.removeAttributeNS(null, Constants._ATT_ID); + } + } + + /** + * Returns the {@code Id} attribute value of the {@code ds:SignatureValue} + * element, or {@code null} if none has been set. + * + * @return the identifier, or {@code null} + */ + public String getSignatureValueId() { + String id = signatureValueElement.getAttributeNS(null, Constants._ATT_ID); + return id.isEmpty() ? null : id; + } + /** * Returns the KeyInfo child. If we are in signing mode and the KeyInfo * does not exist yet, it is created on demand and added to the Signature. @@ -794,6 +851,17 @@ public void sign(Key signingKey) throws XMLSignatureException { ); } + + // snapshot the lists so that concurrent registration during sign() cannot + // cause ConcurrentModificationException or skip newly added processors + List preSnapshot = List.copyOf(preProcessors); + List postSnapshot = List.copyOf(postProcessors); + + // invoke pre-processors before digests are computed + for (SignatureProcessor processor : preSnapshot) { + processor.processSignature(this); + } + //Create a SignatureAlgorithm object SignedInfo si = this.getSignedInfo(); SignatureAlgorithm sa = si.getSignatureAlgorithm(); @@ -816,6 +884,11 @@ public void sign(Key signingKey) throws XMLSignatureException { } catch (XMLSecurityException | IOException ex) { throw new XMLSignatureException(ex); } + + // invoke post-processors after the signature value has been set + for (SignatureProcessor processor : postSnapshot) { + processor.processSignature(this); + } } /** diff --git a/src/test/java/org/apache/xml/security/extension/SignatureProcessorTest.java b/src/test/java/org/apache/xml/security/extension/SignatureProcessorTest.java new file mode 100644 index 000000000..c96ed64a3 --- /dev/null +++ b/src/test/java/org/apache/xml/security/extension/SignatureProcessorTest.java @@ -0,0 +1,253 @@ +/** + * 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.extension; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; + +import org.apache.xml.security.Init; +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.signature.XMLSignatureException; +import org.apache.xml.security.test.dom.TestUtils; +import org.apache.xml.security.transforms.Transforms; +import org.apache.xml.security.utils.Constants; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SignatureProcessorTest { + + private static KeyPair rsaKeyPair; + + @BeforeAll + static void setup() throws Exception { + Init.init(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + rsaKeyPair = kpg.generateKeyPair(); + } + + private XMLSignature newSignature(Document doc) throws Exception { + return new XMLSignature(doc, "", XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + } + + @Test + void preProcessorIsInvokedBeforeSignatureValue() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + boolean[] sigValueEmptyInPreProcessor = {false}; + + sig.addPreProcessor(signature -> { + try { + byte[] value = signature.getSignatureValue(); + sigValueEmptyInPreProcessor[0] = (value == null || value.length == 0); + } catch (XMLSignatureException e) { + sigValueEmptyInPreProcessor[0] = true; + } + }); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + sig.sign(rsaKeyPair.getPrivate()); + + assertTrue(sigValueEmptyInPreProcessor[0], + "Pre-processor must be called before SignatureValue is populated"); + } + + @Test + void postProcessorIsInvokedAfterSignatureValue() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + boolean[] sigValueSetInPostProcessor = {false}; + + sig.addPostProcessor(signature -> { + try { + byte[] value = signature.getSignatureValue(); + sigValueSetInPostProcessor[0] = (value != null && value.length > 0); + } catch (XMLSignatureException e) { + sigValueSetInPostProcessor[0] = false; + } + }); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + sig.sign(rsaKeyPair.getPrivate()); + + assertTrue(sigValueSetInPostProcessor[0], + "Post-processor must be called after SignatureValue is populated"); + } + + @Test + void multiplePreProcessorsAreInvokedInRegistrationOrder() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + List invocationOrder = new ArrayList<>(); + sig.addPreProcessor(signature -> invocationOrder.add("pre-1")); + sig.addPreProcessor(signature -> invocationOrder.add("pre-2")); + sig.addPreProcessor(signature -> invocationOrder.add("pre-3")); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + sig.sign(rsaKeyPair.getPrivate()); + + assertEquals(List.of("pre-1", "pre-2", "pre-3"), invocationOrder, + "Pre-processors must be invoked in registration order"); + } + + @Test + void multiplePostProcessorsAreInvokedInRegistrationOrder() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + List invocationOrder = new ArrayList<>(); + sig.addPostProcessor(signature -> invocationOrder.add("post-1")); + sig.addPostProcessor(signature -> invocationOrder.add("post-2")); + sig.addPostProcessor(signature -> invocationOrder.add("post-3")); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + sig.sign(rsaKeyPair.getPrivate()); + + assertEquals(List.of("post-1", "post-2", "post-3"), invocationOrder, + "Post-processors must be invoked in registration order"); + } + + @Test + void preProcessorExceptionAbortsSigningAndPropagates() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + sig.addPreProcessor(signature -> { + throw new SignatureExtensionException("pre-processor failure"); + }); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + + XMLSignatureException thrown = assertThrows(XMLSignatureException.class, + () -> sig.sign(rsaKeyPair.getPrivate()), + "XMLSignatureException from a pre-processor must propagate out of sign()"); + + assertEquals("pre-processor failure", thrown.getMessage()); + } + + @Test + void postProcessorCanReadSignatureValueId() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + sig.setSignatureValueId("sig-value-id-1"); + + String[] idInPostProcessor = {null}; + sig.addPostProcessor(signature -> idInPostProcessor[0] = signature.getSignatureValueId()); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + sig.sign(rsaKeyPair.getPrivate()); + + assertEquals("sig-value-id-1", idInPostProcessor[0], + "Post-processor must read the SignatureValue Id set before signing"); + } + + @Test + void addPreProcessorRejectsNull() throws Exception { + Document doc = TestUtils.newDocument(); + XMLSignature sig = newSignature(doc); + assertThrows(NullPointerException.class, () -> sig.addPreProcessor(null)); + } + + @Test + void addPostProcessorRejectsNull() throws Exception { + Document doc = TestUtils.newDocument(); + XMLSignature sig = newSignature(doc); + assertThrows(NullPointerException.class, () -> sig.addPostProcessor(null)); + } + + @Test + void signatureValueIdRoundTrip() throws Exception { + Document doc = TestUtils.newDocument(); + XMLSignature sig = newSignature(doc); + + assertNull(sig.getSignatureValueId(), "Id must be null before it is set"); + + sig.setSignatureValueId("my-id"); + assertEquals("my-id", sig.getSignatureValueId()); + + sig.setSignatureValueId(null); + assertNull(sig.getSignatureValueId(), "Id must be null after passing null"); + } + + @Test + void signatureRemainsValidWithHooks() throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("http://example.org/", "root"); + doc.appendChild(root); + XMLSignature sig = newSignature(doc); + root.appendChild(sig.getElement()); + + sig.addPreProcessor(signature -> { /* no-op */ }); + sig.addPostProcessor(signature -> { /* no-op */ }); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + sig.addDocument("", transforms, Constants.ALGO_ID_DIGEST_SHA1); + sig.sign(rsaKeyPair.getPrivate()); + + PublicKey publicKey = rsaKeyPair.getPublic(); + assertTrue(sig.checkSignatureValue(publicKey), + "Signature produced with no-op hooks must verify correctly"); + } +} From 5ac3cf18efa78090ee454c4d3fab0f4463daf04d Mon Sep 17 00:00:00 2001 From: Joze RIHTARSIC Date: Mon, 15 Jun 2026 14:47:32 +0200 Subject: [PATCH 2/5] [SANTUARIO-615] Implements XAdES using pre/post signature processors --- src/main/java/module-info.java | 1 + .../jcp/xml/dsig/internal/dom/DOMUtils.java | 81 ++- .../xml/security/extension/xades/Cert.java | 103 ++++ .../extension/xades/QualifyingProperties.java | 57 ++ .../extension/xades/SignedProperties.java | 51 ++ .../xades/SignedSignatureProperties.java | 89 +++ .../extension/xades/SigningCertificate.java | 43 ++ .../extension/xades/XAdESBBValidator.java | 383 +++++++++++++ .../extension/xades/XAdESConstants.java | 43 ++ .../extension/xades/XAdESElementProxy.java | 85 +++ .../xades/XAdESSignatureProcessor.java | 279 +++++++++ .../xades/XAdESValidationResult.java | 84 +++ .../schemas/XAdES01903v132-201601.xsd | 535 ++++++++++++++++++ .../schemas/XAdES01903v141-202107.xsd | 67 +++ .../extension/xades/XAdESSignatureTest.java | 414 ++++++++++++++ .../utils/SelfSignedCertGenerator.java | 274 +++++++++ 16 files changed, 2569 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/apache/xml/security/extension/xades/Cert.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/QualifyingProperties.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/SignedProperties.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/SignedSignatureProperties.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/SigningCertificate.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/XAdESElementProxy.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java create mode 100644 src/main/java/org/apache/xml/security/extension/xades/XAdESValidationResult.java create mode 100644 src/main/resources/bindings/schemas/XAdES01903v132-201601.xsd create mode 100644 src/main/resources/bindings/schemas/XAdES01903v141-202107.xsd create mode 100644 src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java create mode 100644 src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ec88429bb..1ea20ead9 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -33,6 +33,7 @@ exports org.apache.jcp.xml.dsig.internal.dom; exports org.apache.xml.security; exports org.apache.xml.security.extension; + exports org.apache.xml.security.extension.xades; exports org.apache.xml.security.algorithms; exports org.apache.xml.security.algorithms.implementations; exports org.apache.xml.security.c14n; diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMUtils.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMUtils.java index 6955c0d46..21acb83bd 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMUtils.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMUtils.java @@ -22,6 +22,7 @@ package org.apache.jcp.xml.dsig.internal.dom; import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; import java.util.List; import javax.xml.XMLConstants; @@ -39,6 +40,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; /** * Useful static DOM utility methods. @@ -49,6 +51,9 @@ public final class DOMUtils { // class cannot be instantiated private DOMUtils() {} + /** Attribute names treated as XML ID attributes when scanning for ID declarations. */ + private static final List ID_ATTRIBUTE_NAMES = Arrays.asList("Id", "ID", "id"); + /** * Returns the owner document of the specified node. * @@ -92,7 +97,7 @@ public static Element createElement(Document doc, String tag, String nsURI, String prefix) { String qName = (prefix == null || prefix.length() == 0) - ? tag : prefix + ":" + tag; + ? tag : prefix + ":" + tag; return doc.createElementNS(nsURI, qName); } @@ -158,13 +163,13 @@ public static Element getFirstChildElement(Node node) { * equal to {@code localName} */ public static Element getFirstChildElement(Node node, String localName, String namespaceURI) - throws MarshalException + throws MarshalException { return verifyElement(getFirstChildElement(node), localName, namespaceURI); } private static Element verifyElement(Element elem, String localName, String namespaceURI) - throws MarshalException + throws MarshalException { if (elem == null) { throw new MarshalException("Missing " + localName + " element"); @@ -172,9 +177,9 @@ private static Element verifyElement(Element elem, String localName, String name String name = elem.getLocalName(); String namespace = elem.getNamespaceURI(); if (!name.equals(localName) || namespace == null && namespaceURI != null - || namespace != null && !namespace.equals(namespaceURI)) { + || namespace != null && !namespace.equals(namespaceURI)) { throw new MarshalException("Invalid element name: " + - namespace + ":" + name + ", expected " + namespaceURI + ":" + localName); + namespace + ":" + name + ", expected " + namespaceURI + ":" + localName); } return elem; } @@ -225,7 +230,7 @@ public static Element getNextSiblingElement(Node node) { * equal to {@code localName} */ public static Element getNextSiblingElement(Node node, String localName, String namespaceURI) - throws MarshalException + throws MarshalException { return verifyElement(getNextSiblingElement(node), localName, namespaceURI); } @@ -282,7 +287,7 @@ public static String getIdAttributeValue(Element elem, String name) { public static String getNSPrefix(XMLCryptoContext context, String nsURI) { if (context != null) { return context.getNamespacePrefix - (nsURI, context.getDefaultNamespacePrefix()); + (nsURI, context.getDefaultNamespacePrefix()); } else { return null; } @@ -335,29 +340,29 @@ public static void appendChild(Node parent, Node child) { } public static boolean paramsEqual(AlgorithmParameterSpec spec1, - AlgorithmParameterSpec spec2) { + AlgorithmParameterSpec spec2) { if (spec1 == spec2) { return true; } if (spec1 instanceof XPathFilter2ParameterSpec && - spec2 instanceof XPathFilter2ParameterSpec) { + spec2 instanceof XPathFilter2ParameterSpec) { return paramsEqual((XPathFilter2ParameterSpec)spec1, - (XPathFilter2ParameterSpec)spec2); + (XPathFilter2ParameterSpec)spec2); } if (spec1 instanceof ExcC14NParameterSpec && - spec2 instanceof ExcC14NParameterSpec) { + spec2 instanceof ExcC14NParameterSpec) { return paramsEqual((ExcC14NParameterSpec) spec1, - (ExcC14NParameterSpec)spec2); + (ExcC14NParameterSpec)spec2); } if (spec1 instanceof XPathFilterParameterSpec && - spec2 instanceof XPathFilterParameterSpec) { + spec2 instanceof XPathFilterParameterSpec) { return paramsEqual((XPathFilterParameterSpec)spec1, - (XPathFilterParameterSpec)spec2); + (XPathFilterParameterSpec)spec2); } if (spec1 instanceof XSLTTransformParameterSpec && - spec2 instanceof XSLTTransformParameterSpec) { + spec2 instanceof XSLTTransformParameterSpec) { return paramsEqual((XSLTTransformParameterSpec)spec1, - (XSLTTransformParameterSpec)spec2); + (XSLTTransformParameterSpec)spec2); } return false; } @@ -377,8 +382,8 @@ private static boolean paramsEqual(XPathFilter2ParameterSpec spec1, XPathType type = types.get(i); XPathType otype = otypes.get(i); if (!type.getExpression().equals(otype.getExpression()) || - !type.getNamespaceMap().equals(otype.getNamespaceMap()) || - type.getFilter() != otype.getFilter()) { + !type.getNamespaceMap().equals(otype.getNamespaceMap()) || + type.getFilter() != otype.getFilter()) { return false; } } @@ -407,10 +412,10 @@ private static boolean paramsEqual(XSLTTransformParameterSpec spec1, return false; } Node ostylesheetElem = - ((javax.xml.crypto.dom.DOMStructure) ostylesheet).getNode(); + ((javax.xml.crypto.dom.DOMStructure) ostylesheet).getNode(); XMLStructure stylesheet = spec1.getStylesheet(); Node stylesheetElem = - ((javax.xml.crypto.dom.DOMStructure) stylesheet).getNode(); + ((javax.xml.crypto.dom.DOMStructure) stylesheet).getNode(); return nodesEqual(stylesheetElem, ostylesheetElem); } @@ -422,4 +427,40 @@ public static boolean isNamespace(Node node) } return false; } + + /** + * Recursively walks the DOM tree rooted at {@code node} and calls + * {@link Element#setIdAttribute(String, boolean)} for every attribute whose local name + * is one of {@code Id}, {@code ID}, or {@code id}. This is required so that + * {@link Document#getElementById} correctly resolves same-document ID references in + * XAdES-generated structures. + * + * @param node the root node to start from + */ + public static void setIdFlagToIdAttributes(Node node) { + setIdFlagToIdAttributes(node, ID_ATTRIBUTE_NAMES); + } + + /** + * Recursively walks the DOM tree rooted at {@code node} and calls + * {@link Element#setIdAttribute(String, boolean)} for every attribute whose local name + * is in {@code idAttributeNames}. + * + * @param node the root node to start from + * @param idAttributeNames the list of attribute local names to treat as Id attributes + */ + public static void setIdFlagToIdAttributes(Node node, List idAttributeNames) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + for (String idName : idAttributeNames) { + if (element.hasAttribute(idName)) { + element.setIdAttribute(idName, true); + } + } + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + setIdFlagToIdAttributes(children.item(i), idAttributeNames); + } + } + } } diff --git a/src/main/java/org/apache/xml/security/extension/xades/Cert.java b/src/main/java/org/apache/xml/security/extension/xades/Cert.java new file mode 100644 index 000000000..2db543eb9 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/Cert.java @@ -0,0 +1,103 @@ +/** + * 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.extension.xades; + +import org.apache.xml.security.utils.XMLUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.math.BigInteger; + +/** + * Proxy for the {@code xades132:Cert} element. + * + *

Holds certificate identification data: + *

+ * + *
{@code
+ * 
+ *   
+ *     
+ *     base64...
+ *   
+ *   
+ *     CN=...
+ *     12345
+ *   
+ * 
+ * }
+ */ +public class Cert extends XAdESElementProxy { + + public Cert(Document doc) { + super(doc); + } + + @Override + public String getBaseLocalName() { + return "Cert"; + } + + /** + * Appends a {@code } child containing the digest algorithm + * and the base64-encoded digest value of the DER-encoded certificate. + * + * @param digestAlgorithmURI W3C URI of the digest algorithm (e.g. {@code XMLCipher.SHA256}) + * @param digestValue the raw digest bytes + */ + public void setCertDigest(String digestAlgorithmURI, byte[] digestValue) { + Element certDigest = createXAdESChild("CertDigest"); + + Element digestMethod = createDsChild("DigestMethod"); + digestMethod.setAttributeNS(null, "Algorithm", digestAlgorithmURI); + certDigest.appendChild(digestMethod); + + Element digestValueEl = createDsChild("DigestValue"); + digestValueEl.setTextContent(XMLUtils.encodeToString(digestValue)); + certDigest.appendChild(digestValueEl); + + appendSelf(certDigest); + } + + /** + * Appends a {@code } child containing the certificate's + * issuer distinguished name and serial number. + * + * @param issuerName RFC 2253 issuer distinguished name + * @param serialNumber certificate serial number + */ + public void setIssuerSerial(String issuerName, BigInteger serialNumber) { + Element issuerSerial = createXAdESChild("IssuerSerial"); + + Element issuerNameEl = createDsChild("X509IssuerName"); + issuerNameEl.setTextContent(issuerName); + issuerSerial.appendChild(issuerNameEl); + + Element serialEl = createDsChild("X509SerialNumber"); + serialEl.setTextContent(serialNumber.toString()); + issuerSerial.appendChild(serialEl); + + appendSelf(issuerSerial); + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/QualifyingProperties.java b/src/main/java/org/apache/xml/security/extension/xades/QualifyingProperties.java new file mode 100644 index 000000000..44f7d1b88 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/QualifyingProperties.java @@ -0,0 +1,57 @@ +/** + * 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.extension.xades; + +import org.apache.xml.security.utils.Constants; +import org.w3c.dom.Document; + +/** + * Proxy for the {@code xades132:QualifyingProperties} element. + * + *

The root element of the XAdES qualifying properties tree. Carries the + * {@code Target} attribute pointing to the enclosing {@code ds:Signature} Id. + * Also declares {@code xmlns:ds} so that descendant {@code ds:DigestMethod} and + * related elements are well-formed even when this sub-tree is serialised in isolation. + * + *

{@code
+ * 
+ *   ...
+ * 
+ * }
+ */ +public class QualifyingProperties extends XAdESElementProxy { + + public QualifyingProperties(Document doc, String target) { + super(doc); + getElement().setAttributeNS(null, "Target", target); + getElement().setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + } + + @Override + public String getBaseLocalName() { + return XAdESConstants.TAG_QUALIFYING_PROPERTIES; + } + + public void setSignedProperties(SignedProperties sp) { + appendSelf(sp); + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/SignedProperties.java b/src/main/java/org/apache/xml/security/extension/xades/SignedProperties.java new file mode 100644 index 000000000..9bca82da6 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/SignedProperties.java @@ -0,0 +1,51 @@ +/** + * 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.extension.xades; + +import org.w3c.dom.Document; + +/** + * Proxy for the {@code xades132:SignedProperties} element. + * + *

Carries an {@code Id} attribute (registered as an XML ID so that + * {@link org.w3c.dom.Document#getElementById} resolves it) that is referenced + * from the {@code ds:Reference} added by the XAdES pre-processor. + * + *

{@code
+ * 
+ *   ...
+ * 
+ * }
+ */ +public class SignedProperties extends XAdESElementProxy { + + public SignedProperties(Document doc, String id) { + super(doc); + setLocalIdAttribute("Id", id); + } + + @Override + public String getBaseLocalName() { + return "SignedProperties"; + } + + public void setSignedSignatureProperties(SignedSignatureProperties ssp) { + appendSelf(ssp); + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/SignedSignatureProperties.java b/src/main/java/org/apache/xml/security/extension/xades/SignedSignatureProperties.java new file mode 100644 index 000000000..68aaadad4 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/SignedSignatureProperties.java @@ -0,0 +1,89 @@ +/** + * 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.extension.xades; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Proxy for the {@code xades132:SignedSignatureProperties} element. + * + *

Orchestrates the mandatory and optional children for XAdES-B-B: + *

    + *
  • {@code SigningTime} — mandatory, set via {@link #setSigningTime}
  • + *
  • {@code SigningCertificate} — mandatory, set via {@link #setSigningCertificate}
  • + *
  • {@code SignaturePolicyIdentifier/SignaturePolicyImplied} — optional, via {@link #setSignaturePolicyImplied}
  • + *
  • {@code SignatureProductionPlace} — optional, via {@link #setSignatureProductionPlace}
  • + *
+ */ +public class SignedSignatureProperties extends XAdESElementProxy { + + public SignedSignatureProperties(Document doc) { + super(doc); + } + + @Override + public String getBaseLocalName() { + return "SignedSignatureProperties"; + } + + /** Appends a {@code } child with an ISO-8601 timestamp. */ + public void setSigningTime(OffsetDateTime dateTime) { + Element e = createXAdESChild("SigningTime"); + e.setTextContent(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(dateTime)); + appendSelf(e); + } + + public void setSigningCertificate(SigningCertificate sc) { + appendSelf(sc); + } + + /** + * Appends an empty {@code } wrapped in + * {@code }, indicating that the signature + * policy is implied by the signing context. + */ + public void setSignaturePolicyImplied() { + Element policyId = createXAdESChild("SignaturePolicyIdentifier"); + policyId.appendChild(createXAdESChild(XAdESConstants.TAG_SIGNATURE_POLICY_IMPLIED)); + appendSelf(policyId); + } + + /** + * Appends a {@code } child. + * At least one of {@code city} or {@code countryName} must be non-null. + */ + public void setSignatureProductionPlace(String city, String countryName) { + Element place = createXAdESChild("SignatureProductionPlace"); + if (city != null) { + Element cityEl = createXAdESChild("City"); + cityEl.setTextContent(city); + place.appendChild(cityEl); + } + if (countryName != null) { + Element countryEl = createXAdESChild("CountryName"); + countryEl.setTextContent(countryName); + place.appendChild(countryEl); + } + appendSelf(place); + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/SigningCertificate.java b/src/main/java/org/apache/xml/security/extension/xades/SigningCertificate.java new file mode 100644 index 000000000..b9c14c6c4 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/SigningCertificate.java @@ -0,0 +1,43 @@ +/** + * 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.extension.xades; + +import org.w3c.dom.Document; + +/** + * Proxy for the {@code xades132:SigningCertificate} element. + * + *

Contains one or more {@link Cert} children, each carrying a certificate + * digest and issuer/serial information. + */ +public class SigningCertificate extends XAdESElementProxy { + + public SigningCertificate(Document doc) { + super(doc); + } + + @Override + public String getBaseLocalName() { + return "SigningCertificate"; + } + + public void addCert(Cert cert) { + appendSelf(cert); + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java new file mode 100644 index 000000000..efbda115c --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java @@ -0,0 +1,383 @@ +/** + * 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.extension.xades; + +import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.signature.Reference; +import org.apache.xml.security.signature.SignedInfo; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.utils.ClassLoaderUtils; +import org.apache.xml.security.utils.Constants; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.w3c.dom.ls.LSInput; +import org.w3c.dom.ls.LSResourceResolver; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +/** + * Validates XAdES-B-B (Basic Electronic Signature) qualifying properties embedded in an + * {@link XMLSignature}. + * + *

Validation performed

+ *
    + *
  1. Presence check — determines whether {@code xades132:QualifyingProperties} + * is present in the signature's {@code ds:Object} elements. If not present the + * result is reported as {@link XAdESValidationResult#isXAdESPresent()} == {@code false} + * and no further checks are run.
  2. + *
  3. XSD structural validation — validates the {@code QualifyingProperties} subtree + * against the bundled XAdES v1.3.2 schema ({@code XAdES01903v132-201601.xsd}).
  4. + *
  5. Target attribute — {@code QualifyingProperties/@Target} must equal + * {@code "#"} + the signature element {@code Id}.
  6. + *
  7. SignedProperties reference — the signature must contain a + * {@code ds:Reference} whose {@code @Type} equals + * {@link XAdESConstants#REFERENCE_TYPE_SIGNEDPROPERTIES}.
  8. + *
  9. Signing certificate digest — the {@code CertDigest} value inside + * {@code SigningCertificate/Cert} must match the SHA-256 (or configured algorithm) + * digest of the provided signing certificate.
  10. + *
+ * + *

Usage

+ *
{@code
+ * XAdESBBValidator validator = new XAdESBBValidator();
+ * XAdESValidationResult result = validator.validate(signature, signingCertificate);
+ * if (result.isXAdESPresent() && !result.isValid()) {
+ *     result.getViolations().forEach(System.out::println);
+ * }
+ * }
+ * + *

The schema is loaded once at class-load time and reused across instances. + * + * @see + * ETSI EN 319 132-1 (XAdES) + */ +public final class XAdESBBValidator { + + private static final String XADES_SCHEMA_RESOURCE ="bindings/schemas/XAdES01903v141-202107.xsd"; + + /** + * Schema is thread-safe once constructed; load once and share. + * Null if schema loading failed at class init time. + */ + private static final Schema XADES_SCHEMA = loadSchema(); + + private static Schema loadSchema() { + try { + SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + sf.setResourceResolver(new SchemeResourceResolver()); + // load all schema resources from classpath and combine into a single schema + String xadesUri = resourceUri(XADES_SCHEMA_RESOURCE); + try (InputStream xadesIs = ClassLoaderUtils.getResourceAsStream( + XADES_SCHEMA_RESOURCE, XAdESBBValidator.class)) { + return sf.newSchema(new StreamSource(xadesIs, xadesUri)); + } + } catch (SAXException | IOException e) { + // Logged here; validate() reports the violation rather than crashing callers + System.getLogger(XAdESBBValidator.class.getName()) + .log(System.Logger.Level.ERROR, + "Failed to load XAdES schema — XSD validation will be skipped", e); + return null; + } + } + + private static String resourceUri(String path) { + java.net.URL url = ClassLoaderUtils.getResource(path, XAdESBBValidator.class); + if (url == null) { + throw new IllegalStateException("XAdES schema not found on classpath: " + path); + } + return url.toExternalForm(); + } + + /** + * Validates XAdES-B-B properties in {@code signature}. + * + * @param signature the cryptographically verified {@link XMLSignature} + * (core verification must have already succeeded) + * @param signingCertificate the certificate used to create the signature; + * used to check the {@code CertDigest} value + * @return validation result; never {@code null} + */ + public XAdESValidationResult validate(XMLSignature signature, + X509Certificate signingCertificate) { + List violations = new ArrayList<>(); + + Element qualifyingProps = findQualifyingProperties(signature); + if (qualifyingProps == null) { + return XAdESValidationResult.notPresent(); + } + + validateSchema(qualifyingProps, violations); + validateTarget(qualifyingProps, signature, violations); + validateSignedPropertiesReference(signature, violations); + if (signingCertificate != null) { + validateCertDigest(qualifyingProps, signingCertificate, violations); + } + + return new XAdESValidationResult(true, violations); + } + + // ------------------------------------------------------------------------- + // XAdES element discovery + // ------------------------------------------------------------------------- + + private Element findQualifyingProperties(XMLSignature signature) { + Element sigElement = signature.getElement(); + NodeList objects = sigElement.getElementsByTagNameNS( + Constants.SignatureSpecNS, "Object"); + for (int i = 0; i < objects.getLength(); i++) { + Element object = (Element) objects.item(i); + NodeList qpList = object.getElementsByTagNameNS( + XAdESConstants.XADES_V132_NS, + XAdESConstants.TAG_QUALIFYING_PROPERTIES); + if (qpList.getLength() > 0) { + return (Element) qpList.item(0); + } + } + return null; + } + + // ------------------------------------------------------------------------- + // XSD validation + // ------------------------------------------------------------------------- + + private void validateSchema(Element qualifyingProps, List violations) { + if (XADES_SCHEMA == null) { + violations.add("XAdES schema not available — XSD validation skipped"); + return; + } + try { + Validator validator = XADES_SCHEMA.newValidator(); + validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Collect all schema violations rather than stopping at first error + List schemaViolations = new ArrayList<>(); + validator.setErrorHandler(new SchemaErrorCollector(schemaViolations)); + validator.validate(new DOMSource(qualifyingProps)); + violations.addAll(schemaViolations); + } catch (SAXException | IOException e) { + violations.add("XSD validation error: " + e.getMessage()); + } + } + + // ------------------------------------------------------------------------- + // Semantic checks + // ------------------------------------------------------------------------- + + private void validateTarget(Element qualifyingProps, + XMLSignature signature, + List violations) { + String target = qualifyingProps.getAttribute("Target"); + String signatureId = signature.getId(); + if (signatureId == null || signatureId.isBlank()) { + violations.add("QualifyingProperties/@Target validation skipped: " + + "ds:Signature has no Id attribute"); + return; + } + String expected = "#" + signatureId; + if (!expected.equals(target)) { + violations.add("QualifyingProperties/@Target '" + target + + "' does not match expected '" + expected + "'"); + } + } + + private void validateSignedPropertiesReference(XMLSignature signature, + List violations) { + try { + SignedInfo si = signature.getSignedInfo(); + for (int i = 0; i < si.getLength(); i++) { + Reference ref = si.item(i); + if (XAdESConstants.REFERENCE_TYPE_SIGNEDPROPERTIES.equals(ref.getType())) { + return; // found + } + } + } catch (XMLSecurityException e) { + violations.add("Cannot read ds:SignedInfo references: " + e.getMessage()); + return; + } + violations.add("No ds:Reference with @Type='" + + XAdESConstants.REFERENCE_TYPE_SIGNEDPROPERTIES + + "' found — SignedProperties is not covered by the signature"); + } + + private void validateCertDigest(Element qualifyingProps, + X509Certificate signingCertificate, + List violations) { + // Find the first CertDigest inside SigningCertificate/Cert + NodeList certDigestNodes = qualifyingProps.getElementsByTagNameNS( + XAdESConstants.XADES_V132_NS, "CertDigest"); + if (certDigestNodes.getLength() == 0) { + violations.add("No xades132:CertDigest element found in QualifyingProperties"); + return; + } + Element certDigest = (Element) certDigestNodes.item(0); + + String algorithmURI = getChildTextContent(certDigest, + Constants.SignatureSpecNS, "DigestMethod", "Algorithm"); + String digestValueB64 = getChildTextContent(certDigest, + Constants.SignatureSpecNS, "DigestValue", null); + + if (algorithmURI == null || algorithmURI.isBlank()) { + violations.add("CertDigest/ds:DigestMethod/@Algorithm is missing or empty"); + return; + } + if (digestValueB64 == null || digestValueB64.isBlank()) { + violations.add("CertDigest/ds:DigestValue is missing or empty"); + return; + } + + String jceAlgorithm = JCEMapper.translateURItoJCEID(algorithmURI); + if (jceAlgorithm == null) { + violations.add("Unknown digest algorithm URI in CertDigest: " + algorithmURI); + return; + } + + byte[] reportedDigest; + try { + reportedDigest = Base64.getDecoder().decode(digestValueB64.trim()); + } catch (IllegalArgumentException e) { + violations.add("CertDigest/ds:DigestValue is not valid Base64: " + e.getMessage()); + return; + } + + byte[] actualDigest; + try { + byte[] certDer = signingCertificate.getEncoded(); + actualDigest = MessageDigest.getInstance(jceAlgorithm).digest(certDer); + } catch (CertificateEncodingException | NoSuchAlgorithmException e) { + violations.add("Cannot compute signing certificate digest: " + e.getMessage()); + return; + } + + if (!Arrays.equals(actualDigest, reportedDigest)) { + violations.add("CertDigest does not match the digest of the signing certificate " + + "(algorithm=" + algorithmURI + ")"); + } + } + + // ------------------------------------------------------------------------- + // DOM helpers + // ------------------------------------------------------------------------- + + /** + * Returns the text content of a child element, or the value of {@code attributeName} + * on that child if {@code attributeName} is non-null. + */ + private String getChildTextContent(Element parent, String ns, String localName, + String attributeName) { + NodeList children = parent.getElementsByTagNameNS(ns, localName); + if (children.getLength() == 0) { + return null; + } + Element child = (Element) children.item(0); + if (attributeName != null) { + return child.getAttribute(attributeName); + } + return child.getTextContent(); + } + + /** + * The Schema Error Collector + */ + private static final class SchemaErrorCollector implements org.xml.sax.ErrorHandler { + + private final List violations; + + SchemaErrorCollector(List violations) { + this.violations = violations; + } + + @Override + public void warning(org.xml.sax.SAXParseException e) { + violations.add("XSD warning: " + e.getMessage()); + } + + @Override + public void error(org.xml.sax.SAXParseException e) { + violations.add("XSD error: " + e.getMessage()); + } + + @Override + public void fatalError(org.xml.sax.SAXParseException e) throws org.xml.sax.SAXException { + violations.add("XSD fatal error: " + e.getMessage()); + throw e; + } + } + + /** + * LSResourceResolver that loads schema resources from the classpath. Used to resolve the XAdES schema and its + * dependencies (e.g. xmldsig-core-schema.xsd) during XSD validation. The schema files must be located in the + * "bindings/schemas/" directory on the classpath. + */ + private static final class SchemeResourceResolver implements LSResourceResolver { + private static final String resourcePath = "bindings/schemas/"; + @Override + public LSInput resolveResource( + String type, + String namespaceURI, + String publicId, + String systemId, + String baseURI) { + + // systemId is e.g. "xmldsig-core-schema.xsd" + String resource = resourcePath + systemId; + InputStream is = ClassLoaderUtils.getResourceAsStream(resource, XAdESBBValidator.class); + + if (is == null) { + throw new IllegalStateException("Cannot resolve schema: " + systemId); + } + + return new LSInput() { + @Override public Reader getCharacterStream() { return null; } + @Override public void setCharacterStream(Reader characterStream) {} + @Override public InputStream getByteStream() { return is; } + @Override public void setByteStream(InputStream byteStream) {} + @Override public String getStringData() { return null; } + @Override public void setStringData(String stringData) {} + @Override public String getSystemId() { return systemId; } + @Override public void setSystemId(String systemId) {} + @Override public String getPublicId() { return publicId; } + @Override public void setPublicId(String publicId) {} + @Override public String getBaseURI() { return baseURI; } + @Override public void setBaseURI(String baseURI) {} + @Override public String getEncoding() { return "UTF-8"; } + @Override public void setEncoding(String encoding) {} + @Override public boolean getCertifiedText() { return false; } + @Override public void setCertifiedText(boolean certifiedText) {} + }; + } + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java new file mode 100644 index 000000000..35433665b --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java @@ -0,0 +1,43 @@ +/** + * 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.extension.xades; + +/** + * Namespace URIs, prefixes, and element tag constants for XAdES v1.3.2 and v1.4.1. + * + * @see + * ETSI EN 319 132-1 (XAdES) + */ +public final class XAdESConstants { + + private XAdESConstants() { + } + + public static final String XADES_V132_NS = "http://uri.etsi.org/01903/v1.3.2#"; + public static final String XADES_V141_NS = "http://uri.etsi.org/01903/v1.4.1#"; + + public static final String XADES_V132_PREFIX = "xades132"; + public static final String XADES_V141_PREFIX = "xades141"; + + /** Reference type URI identifying a reference that covers {@code ds:SignedProperties}. */ + public static final String REFERENCE_TYPE_SIGNEDPROPERTIES = "http://uri.etsi.org/01903#SignedProperties"; + + public static final String TAG_QUALIFYING_PROPERTIES = "QualifyingProperties"; + public static final String TAG_SIGNATURE_POLICY_IMPLIED = "SignaturePolicyImplied"; +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESElementProxy.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESElementProxy.java new file mode 100644 index 000000000..226e8596a --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESElementProxy.java @@ -0,0 +1,85 @@ +/** + * 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.extension.xades; + +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.ElementProxy; +import org.apache.xml.security.utils.XMLUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Abstract base class for XAdES v1.3.2 DOM element proxies. + * + *

All root elements are created in the {@link XAdESConstants#XADES_V132_NS} namespace + * with the {@link XAdESConstants#XADES_V132_PREFIX} prefix and an explicit + * {@code xmlns:xades132} declaration. + * + *

Child elements within an XAdES structure are created via + * {@link #createXAdESChild(String)} (no xmlns re-declaration) or + * {@link #createDsChild(String)} for elements in the XML Signature namespace. + */ +public abstract class XAdESElementProxy extends ElementProxy { + + protected XAdESElementProxy(Document doc) { + super(doc); + } + + protected XAdESElementProxy(Element element, String baseURI) throws XMLSecurityException { + super(element, baseURI); + } + + @Override + public String getBaseNamespace() { + return XAdESConstants.XADES_V132_NS; + } + + /** + * Creates the root element with an explicit {@code xmlns:xades132} declaration. + * Overrides the base class to always use the XAdES prefix rather than consulting + * the global prefix map. + */ + @Override + protected Element createElementForFamilyLocal(String namespace, String localName) { + Document doc = getDocument(); + String prefix = XAdESConstants.XADES_V132_PREFIX; + Element e = doc.createElementNS(namespace, prefix + ":" + localName); + e.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:" + prefix, namespace); + return e; + } + + /** + * Creates a child element in the XAdES v1.3.2 namespace without adding + * a redundant {@code xmlns:xades132} declaration (inherited from the ancestor root). + */ + protected Element createXAdESChild(String localName) { + return getDocument().createElementNS( + XAdESConstants.XADES_V132_NS, + XAdESConstants.XADES_V132_PREFIX + ":" + localName); + } + + /** + * Creates a child element in the XML Signature namespace using the globally + * registered {@code ds:} prefix (set by {@link XMLUtils#setDsPrefix}). + */ + protected Element createDsChild(String localName) { + return XMLUtils.createElementInSignatureSpace(getDocument(), localName); + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java new file mode 100644 index 000000000..95eaf94d9 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java @@ -0,0 +1,279 @@ +/** + * 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.extension.xades; + +import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.extension.SignatureExtensionException; +import org.apache.xml.security.extension.SignatureProcessor; +import org.apache.xml.security.signature.ObjectContainer; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.signature.XMLSignatureException; +import org.apache.xml.security.stax.impl.util.IDGenerator; +import org.apache.xml.security.transforms.TransformationException; +import org.apache.xml.security.transforms.Transforms; +import org.w3c.dom.Document; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Pre-processor that adds XAdES-B-B (Basic Electronic Signature) qualifying properties + * to an XML signature before digests are computed. + * + *

The processor: + *

    + *
  1. Assigns an {@code Id} to the {@code ds:Signature} element if one is not already set.
  2. + *
  3. Assigns an {@code Id} to the {@code ds:SignatureValue} element (enables XAdES-T extension).
  4. + *
  5. Builds an XAdES {@code QualifyingProperties} structure containing {@code SignedProperties} + * using DOM-based {@link XAdESElementProxy} classes — no JAXB dependency.
  6. + *
  7. Wraps it in a {@code ds:Object} and appends it to the signature.
  8. + *
  9. Adds a {@code ds:Reference} with type {@code SignedProperties} so that + * {@code SignedProperties} is covered by the signature digest.
  10. + *
+ * + *

Create an instance using the {@link Builder}: + *

{@code
+ * XAdESSignatureProcessor xades = XAdESSignatureProcessor.builder(certificate)
+ *         .withSignaturePolicyImplied(true)
+ *         .withSignatureCity("Brussels")
+ *         .build();
+ * sig.addPreProcessor(xades);
+ * }
+ * + * @see + * ETSI EN 319 132-1 (XAdES) + */ +public final class XAdESSignatureProcessor implements SignatureProcessor { + + private static final String ID_PREFIX_SIG = "sig-"; + private static final String ID_PREFIX_SIG_VAL = "sig-val-"; + private static final String ID_PREFIX_SIG_PROP = "sig-prop-"; + + private final X509Certificate certificate; + private final String certificateDigestAlgorithmURI; + private final boolean signaturePolicyImplied; + private final String signatureCity; + private final String signatureCountryName; + private final List referenceTransformAlgorithms; + + private XAdESSignatureProcessor(Builder builder) { + this.certificate = builder.certificate; + this.certificateDigestAlgorithmURI = builder.certificateDigestAlgorithmURI; + this.signaturePolicyImplied = builder.signaturePolicyImplied; + this.signatureCity = builder.signatureCity; + this.signatureCountryName = builder.signatureCountryName; + this.referenceTransformAlgorithms = new ArrayList<>(builder.referenceTransformAlgorithms); + } + + /** + * Creates a builder for configuring an {@link XAdESSignatureProcessor}. + * + * @param certificate the signing certificate; must not be {@code null} + */ + public static Builder builder(X509Certificate certificate) { + return new Builder(certificate); + } + + @Override + public void processSignature(XMLSignature signature) throws XMLSignatureException { + ensureSignatureId(signature); + ensureSignatureValueId(signature); + + String signatureId = signature.getId(); + String signedPropertiesId = IDGenerator.generateID(ID_PREFIX_SIG_PROP); + Document doc = signature.getElement().getOwnerDocument(); + + SignedSignatureProperties ssp = buildSignedSignatureProperties(doc); + + SignedProperties sp = new SignedProperties(doc, signedPropertiesId); + sp.setSignedSignatureProperties(ssp); + + QualifyingProperties qp = new QualifyingProperties(doc, "#" + signatureId); + qp.setSignedProperties(sp); + + ObjectContainer objectContainer = new ObjectContainer(doc); + objectContainer.appendChild(qp.getElement()); + signature.appendObject(objectContainer); + + Transforms transforms = buildReferenceTransforms(doc); + signature.addDocument( + "#" + signedPropertiesId, + transforms, + XMLCipher.SHA256, + null, + XAdESConstants.REFERENCE_TYPE_SIGNEDPROPERTIES); + } + + private SignedSignatureProperties buildSignedSignatureProperties(Document doc) + throws XMLSignatureException { + SignedSignatureProperties ssp = new SignedSignatureProperties(doc); + ssp.setSigningTime(OffsetDateTime.now()); + ssp.setSigningCertificate(buildSigningCertificate(doc)); + if (signaturePolicyImplied) { + ssp.setSignaturePolicyImplied(); + } + if (signatureCity != null || signatureCountryName != null) { + ssp.setSignatureProductionPlace(signatureCity, signatureCountryName); + } + return ssp; + } + + private SigningCertificate buildSigningCertificate(Document doc) throws XMLSignatureException { + String jceAlgorithm = JCEMapper.translateURItoJCEID(certificateDigestAlgorithmURI); + if (jceAlgorithm == null) { + throw new SignatureExtensionException( + "Unknown digest algorithm URI: " + certificateDigestAlgorithmURI); + } + + byte[] certDer; + try { + certDer = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new SignatureExtensionException("Cannot encode signing certificate", e); + } + + byte[] digest; + try { + digest = MessageDigest.getInstance(jceAlgorithm).digest(certDer); + } catch (NoSuchAlgorithmException e) { + throw new SignatureExtensionException( + "Digest algorithm not available: " + certificateDigestAlgorithmURI, e); + } + + Cert cert = new Cert(doc); + cert.setCertDigest(certificateDigestAlgorithmURI, digest); + cert.setIssuerSerial( + certificate.getIssuerX500Principal().getName(), + certificate.getSerialNumber()); + + SigningCertificate sc = new SigningCertificate(doc); + sc.addCert(cert); + return sc; + } + + private void ensureSignatureId(XMLSignature signature) throws XMLSignatureException { + if (isBlank(signature.getId())) { + signature.setId(IDGenerator.generateID(ID_PREFIX_SIG)); + } + } + + private void ensureSignatureValueId(XMLSignature signature) throws XMLSignatureException { + if (isBlank(signature.getSignatureValueId())) { + signature.setSignatureValueId(IDGenerator.generateID(ID_PREFIX_SIG_VAL)); + } + } + + private Transforms buildReferenceTransforms(Document doc) throws XMLSignatureException { + if (referenceTransformAlgorithms.isEmpty()) { + return null; + } + Transforms transforms = new Transforms(doc); + try { + for (String algorithm : referenceTransformAlgorithms) { + transforms.addTransform(algorithm); + } + } catch (TransformationException e) { + throw new XMLSignatureException(e); + } + return transforms; + } + + /** Returns an unmodifiable view of the currently configured reference transform algorithms. */ + public List getReferenceTransformAlgorithms() { + return Collections.unmodifiableList(referenceTransformAlgorithms); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + // ------------------------------------------------------------------------- + // Builder + // ------------------------------------------------------------------------- + + /** + * Fluent builder for {@link XAdESSignatureProcessor}. + */ + public static final class Builder { + + private final X509Certificate certificate; + private String certificateDigestAlgorithmURI = XMLCipher.SHA256; + private boolean signaturePolicyImplied = false; + private String signatureCity; + private String signatureCountryName; + private final List referenceTransformAlgorithms = new ArrayList<>(); + + private Builder(X509Certificate certificate) { + this.certificate = Objects.requireNonNull(certificate, "certificate"); + } + + /** + * Digest algorithm URI used to hash the signing certificate. + * Defaults to {@code XMLCipher.SHA256} if not set. + */ + public Builder withCertificateDigestAlgorithmURI(String uri) { + this.certificateDigestAlgorithmURI = Objects.requireNonNull(uri, "uri"); + return this; + } + + /** + * When {@code true}, includes an empty {@code } element + * indicating the policy is implied by the signing context. + */ + public Builder withSignaturePolicyImplied(boolean signaturePolicyImplied) { + this.signaturePolicyImplied = signaturePolicyImplied; + return this; + } + + /** Optional city to include in {@code SignatureProductionPlace}. */ + public Builder withSignatureCity(String city) { + this.signatureCity = city; + return this; + } + + /** Optional country name to include in {@code SignatureProductionPlace}. */ + public Builder withSignatureCountryName(String countryName) { + this.signatureCountryName = countryName; + return this; + } + + /** + * Adds a canonicalization or transform algorithm URI to apply to the + * {@code SignedProperties} reference before digesting. Algorithms are applied in + * the order they are added. + */ + public Builder addReferenceTransformAlgorithm(String algorithm) { + this.referenceTransformAlgorithms.add(Objects.requireNonNull(algorithm, "algorithm")); + return this; + } + + public XAdESSignatureProcessor build() { + Objects.requireNonNull(certificateDigestAlgorithmURI, "certificateDigestAlgorithmURI"); + return new XAdESSignatureProcessor(this); + } + } +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESValidationResult.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESValidationResult.java new file mode 100644 index 000000000..e1b81f087 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESValidationResult.java @@ -0,0 +1,84 @@ +/** + * 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.extension.xades; + +import java.util.Collections; +import java.util.List; + +/** + * Result of XAdES-B-B validation performed by {@link XAdESBBValidator}. + * + *

A result can represent three distinct outcomes: + *

    + *
  1. {@link #isXAdESPresent()} == {@code false} — no XAdES qualifying properties were found; + * validation was not attempted.
  2. + *
  3. {@link #isXAdESPresent()} == {@code true} and {@link #isValid()} == {@code true} — + * XAdES-B-B properties are present and all checks passed.
  4. + *
  5. {@link #isXAdESPresent()} == {@code true} and {@link #isValid()} == {@code false} — + * XAdES-B-B properties are present but one or more checks failed; + * details are in {@link #getViolations()}.
  6. + *
+ */ +public final class XAdESValidationResult { + + private final boolean xadesPresent; + private final List violations; + + XAdESValidationResult(boolean xadesPresent, List violations) { + this.xadesPresent = xadesPresent; + this.violations = Collections.unmodifiableList(violations); + } + + /** Returns a result indicating no XAdES properties were found. */ + static XAdESValidationResult notPresent() { + return new XAdESValidationResult(false, Collections.emptyList()); + } + + /** + * Returns {@code true} if {@code xades132:QualifyingProperties} was found in + * the signature's {@code ds:Object} elements. + */ + public boolean isXAdESPresent() { + return xadesPresent; + } + + /** + * Returns {@code true} if XAdES is present and all validation checks passed. + * Returns {@code false} if XAdES is absent or if any check failed. + */ + public boolean isValid() { + return xadesPresent && violations.isEmpty(); + } + + /** + * Returns an unmodifiable list of violation messages. + * Empty when {@link #isValid()} is {@code true} or when XAdES is not present. + */ + public List getViolations() { + return violations; + } + + @Override + public String toString() { + if (!xadesPresent) { + return "XAdESValidationResult[xadesPresent=false]"; + } + return "XAdESValidationResult[valid=" + isValid() + ", violations=" + violations + "]"; + } +} diff --git a/src/main/resources/bindings/schemas/XAdES01903v132-201601.xsd b/src/main/resources/bindings/schemas/XAdES01903v132-201601.xsd new file mode 100644 index 000000000..79231a86c --- /dev/null +++ b/src/main/resources/bindings/schemas/XAdES01903v132-201601.xsd @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/bindings/schemas/XAdES01903v141-202107.xsd b/src/main/resources/bindings/schemas/XAdES01903v141-202107.xsd new file mode 100644 index 000000000..4749dcaa7 --- /dev/null +++ b/src/main/resources/bindings/schemas/XAdES01903v141-202107.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java b/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java new file mode 100644 index 000000000..d8ca387af --- /dev/null +++ b/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java @@ -0,0 +1,414 @@ +/** + * 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.extension.xades; + + +import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.algorithms.SignatureAlgorithm; +import org.apache.xml.security.c14n.Canonicalizer; +import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.keys.KeyInfo; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.test.dom.DSNamespaceContext; +import org.apache.xml.security.test.dom.TestUtils; +import org.apache.xml.security.test.dom.signature.XPointerResourceResolver; +import org.apache.xml.security.testutils.JDKTestUtils; +import org.apache.xml.security.transforms.Transforms; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.SelfSignedCertGenerator; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assumptions; +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 javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.jcp.xml.dsig.internal.dom.DOMUtils.setIdFlagToIdAttributes; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests XAdES-B-B signing and structural validation using {@link XAdESSignatureProcessor}. + * Certificates are generated programmatically; no external keystore files are required. + */ +class XAdESSignatureTest { + + static { + if (!org.apache.xml.security.Init.isInitialized()) { + org.apache.xml.security.Init.init(); + } + } + + @BeforeAll + static void setup() { + Security.insertProviderAt(new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI(), 1); + } + + // ----------------------------------------------------------------- + // Parametrised sign + verify tests + // ----------------------------------------------------------------- + + @ParameterizedTest(name = "{0}") + @CsvSource({ + "RSA-2048, SHA256withRSA, http://www.w3.org/2001/04/xmldsig-more#rsa-sha256, RSA, 2048", + "ECDSA-256, SHA256withECDSA, http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256, EC, secp256r1", + "ECDSA-384, SHA384withECDSA, http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384, EC, secp384r1", + "ECDSA-521, SHA512withECDSA, http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512, EC, secp521r1", + "EdDSA-Ed25519, Ed25519, http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519, EdDSA, Ed25519", + "EdDSA-Ed448, Ed448, http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448, EdDSA, Ed448" + }) + void signAndVerify(String label, + String certSigAlgorithm, + String xmlSigAlgorithmURI, + String keyAlgorithm, + String keyParam) throws Exception { + String jceAlgorithm = JCEMapper.translateURItoJCEID(xmlSigAlgorithmURI); + Assumptions.assumeTrue(JDKTestUtils.isAlgorithmSupportedByJDK(jceAlgorithm), + label + " not supported by this JDK"); + + KeyPair keyPair = generateKeyPair(keyAlgorithm, keyParam); + X509Certificate cert = generateSelfSignedCert(keyPair, certSigAlgorithm, "CN=" + label); + + byte[] signed = sign(keyPair.getPrivate(), cert, xmlSigAlgorithmURI); + verify(signed); + } + + // ----------------------------------------------------------------- + // Structural assertions + // ----------------------------------------------------------------- + + @Test + void signedDocumentHasCorrectXAdESStructure() throws Exception { + KeyPair kp = generateRsaKeyPair(); + X509Certificate cert = generateSelfSignedCert(kp, "SHA256withRSA", "CN=StructureTest"); + + byte[] signed = sign(kp.getPrivate(), cert, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + Document doc = parseDocument(signed); + setIdFlagToIdAttributes(doc.getDocumentElement()); + + XPath xpath = newXPath(); + + // 1. One QualifyingProperties element is present + NodeList qpNodes = (NodeList) xpath.evaluate( + "//xades132:QualifyingProperties", doc, XPathConstants.NODESET); + assertEquals(1, qpNodes.getLength(), "Expected exactly one QualifyingProperties"); + + // 2. QualifyingProperties/@Target == "#" + signature Id + Element sigEl = findSignatureElement(doc, xpath); + String sigId = sigEl.getAttribute("Id"); + assertFalse(sigId.isEmpty(), "Signature must have an Id attribute"); + String target = ((Element) qpNodes.item(0)).getAttribute("Target"); + assertEquals("#" + sigId, target, "QualifyingProperties/@Target must reference the signature Id"); + + // 3. ds:Reference with @Type=SignedProperties exists and points to SignedProperties + String spRef = (String) xpath.evaluate( + "//ds:Reference[@Type='" + XAdESConstants.REFERENCE_TYPE_SIGNEDPROPERTIES + "']/@URI", + doc, XPathConstants.STRING); + assertFalse(spRef.isEmpty(), "No ds:Reference with SignedProperties type found"); + String spId = spRef.substring(1); // strip leading '#' + Element spEl = doc.getElementById(spId); + assertNotNull(spEl, "SignedProperties element not found for id=" + spId); + assertEquals("SignedProperties", spEl.getLocalName()); + + // 4. SigningTime is present and parseable + String signingTimeText = (String) xpath.evaluate( + "//xades132:SigningTime", doc, XPathConstants.STRING); + assertFalse(signingTimeText.isBlank(), "SigningTime must be present"); + OffsetDateTime signingTime = OffsetDateTime.parse(signingTimeText); + assertNotNull(signingTime); + + // 5. CertDigest matches SHA-256 of the certificate + String certDigestB64 = (String) xpath.evaluate( + "//xades132:SigningCertificate/xades132:Cert/xades132:CertDigest/ds:DigestValue", + doc, XPathConstants.STRING); + assertFalse(certDigestB64.isBlank(), "CertDigest/DigestValue must be present"); + byte[] actualDigest = MessageDigest.getInstance("SHA-256").digest(cert.getEncoded()); + byte[] reportedDigest = Base64.getDecoder().decode(certDigestB64.trim()); + assertTrue(Arrays.equals(actualDigest, reportedDigest), + "CertDigest must match SHA-256 of the signing certificate"); + } + + // ----------------------------------------------------------------- + // XAdES-B-B validator tests + // ----------------------------------------------------------------- + + @Test + void xadesBBValidatorReportsValidResult() throws Exception { + KeyPair kp = generateRsaKeyPair(); + X509Certificate cert = generateSelfSignedCert(kp, "SHA256withRSA", "CN=ValidatorTest"); + + byte[] signed = sign(kp.getPrivate(), cert, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + Document doc = parseDocument(signed); + setIdFlagToIdAttributes(doc.getDocumentElement()); + + XPath xpath = newXPath(); + Element sigEl = findSignatureElement(doc, xpath); + + XMLSignature sig = new XMLSignature(sigEl, ""); + XAdESValidationResult result = new XAdESBBValidator().validate(sig, cert); + + assertTrue(result.isXAdESPresent(), "XAdES properties must be present"); + assertTrue(result.isValid(), + "XAdES-B-B validation must pass; violations: " + result.getViolations()); + assertTrue(result.getViolations().isEmpty()); + } + + @Test + void xadesBBValidatorDetectsWrongCertDigest() throws Exception { + KeyPair kp = generateRsaKeyPair(); + X509Certificate cert = generateSelfSignedCert(kp, "SHA256withRSA", "CN=WrongDigestTest"); + + byte[] signed = sign(kp.getPrivate(), cert, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + Document doc = parseDocument(signed); + setIdFlagToIdAttributes(doc.getDocumentElement()); + + // Use a different certificate for validation to trigger digest mismatch + KeyPair otherKp = generateRsaKeyPair(); + X509Certificate otherCert = generateSelfSignedCert(otherKp, "SHA256withRSA", "CN=Other"); + + XPath xpath = newXPath(); + Element sigEl = findSignatureElement(doc, xpath); + XMLSignature sig = new XMLSignature(sigEl, ""); + + XAdESValidationResult result = new XAdESBBValidator().validate(sig, otherCert); + + assertTrue(result.isXAdESPresent(), "XAdES properties must be present"); + assertFalse(result.isValid(), "Validation must fail when wrong certificate is provided"); + assertTrue(result.getViolations().stream() + .anyMatch(v -> v.contains("CertDigest")), + "Violation must mention CertDigest; actual: " + result.getViolations()); + } + + @Test + void xadesBBValidatorReportsNotPresentForPlainXmldsig() throws Exception { + // Sign without XAdES processor — no QualifyingProperties + KeyPair kp = generateRsaKeyPair(); + X509Certificate cert = generateSelfSignedCert(kp, "SHA256withRSA", "CN=NoXAdES"); + + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("", "Root"); + doc.appendChild(root); + + XMLSignature sig = new XMLSignature(doc, null, + XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + root.appendChild(sig.getElement()); + sig.addDocument("", null, XMLCipher.SHA256); + sig.addKeyInfo(cert); + sig.sign(kp.getPrivate()); + + XAdESValidationResult result = new XAdESBBValidator().validate(sig, cert); + + assertFalse(result.isXAdESPresent(), "No XAdES should be reported for plain XMLDSig"); + assertFalse(result.isValid()); + } + + // ----------------------------------------------------------------- + // Negative test — tampered SignedProperties must fail verification + // ----------------------------------------------------------------- + + @Test + void tamperedSignedPropertiesCausesVerificationFailure() throws Exception { + KeyPair kp = generateRsaKeyPair(); + X509Certificate cert = generateSelfSignedCert(kp, "SHA256withRSA", "CN=TamperTest"); + + byte[] signed = sign(kp.getPrivate(), cert, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256); + Document doc = parseDocument(signed); + setIdFlagToIdAttributes(doc.getDocumentElement()); + + // Tamper: change the SigningTime text + XPath xpath = newXPath(); + Element signingTimeEl = (Element) xpath.evaluate( + "//xades132:SigningTime", doc, XPathConstants.NODE); + assertNotNull(signingTimeEl, "Need a SigningTime element to tamper"); + signingTimeEl.setTextContent("1970-01-01T00:00:00Z"); + + // Serialise the tampered document + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLUtils.outputDOM(doc.getDocumentElement(), bos); + + // Verification must fail + boolean valid = tryVerify(bos.toByteArray()); + assertFalse(valid, "Tampered SignedProperties must cause verification failure"); + } + + // ----------------------------------------------------------------- + // Helpers — signing + // ----------------------------------------------------------------- + + private byte[] sign(PrivateKey privateKey, X509Certificate cert, + String xmlSigAlgorithmURI) throws Exception { + Document doc = TestUtils.newDocument(); + Element root = doc.createElementNS("", "RootElement"); + doc.appendChild(root); + root.appendChild(doc.createTextNode("Content to sign")); + + Element c14nEl = XMLUtils.createElementInSignatureSpace( + doc, Constants._TAG_CANONICALIZATIONMETHOD); + c14nEl.setAttributeNS(null, Constants._ATT_ALGORITHM, + Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + + SignatureAlgorithm sigAlg = new SignatureAlgorithm(doc, xmlSigAlgorithmURI); + XMLSignature sig = new XMLSignature(doc, null, sigAlg.getElement(), c14nEl); + root.appendChild(sig.getElement()); + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + transforms.addTransform(Transforms.TRANSFORM_C14N_WITH_COMMENTS); + sig.addDocument("", transforms, XMLCipher.SHA256); + sig.addKeyInfo(cert); + + XAdESSignatureProcessor xades = XAdESSignatureProcessor.builder(cert) + .addReferenceTransformAlgorithm(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS) + .build(); + sig.addPreProcessor(xades); + + sig.sign(privateKey); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLUtils.outputDOM(doc.getDocumentElement(), bos); + return bos.toByteArray(); + } + + // ----------------------------------------------------------------- + // Helpers — verification + // ----------------------------------------------------------------- + + private void verify(byte[] signedXml) throws Exception { + assertTrue(tryVerify(signedXml), "Signature verification must succeed"); + } + + private boolean tryVerify(byte[] signedXml) throws Exception { + Document doc = parseDocument(signedXml); + setIdFlagToIdAttributes(doc.getDocumentElement()); + + XPath xpath = newXPath(); + Element sigEl = findSignatureElement(doc, xpath); + + XMLSignature signature = new XMLSignature(sigEl, ""); + signature.addResourceResolver(new XPointerResourceResolver(sigEl)); + + KeyInfo ki = signature.getKeyInfo(); + if (ki == null) { + throw new IllegalStateException("No KeyInfo in signature"); + } + + X509Certificate cert = ki.getX509Certificate(); + boolean coreValid = cert != null + ? signature.checkSignatureValue(cert) + : signature.checkSignatureValue(ki.getPublicKey()); + + if (!coreValid) { + return false; + } + + // Validate XAdES-B-B properties if present + XAdESValidationResult xadesResult = new XAdESBBValidator().validate(signature, cert); + if (xadesResult.isXAdESPresent() && !xadesResult.isValid()) { + throw new AssertionError("XAdES-B-B validation failed: " + xadesResult.getViolations()); + } + + return true; + } + + private Element findSignatureElement(Document doc, XPath xpath) throws Exception { + return (Element) xpath.evaluate("//ds:Signature[1]", doc, XPathConstants.NODE); + } + + // ----------------------------------------------------------------- + // Helpers — XPath + // ----------------------------------------------------------------- + + private XPath newXPath() { + XPath xpath = XPathFactory.newInstance().newXPath(); + Map ns = new HashMap<>(); + ns.put("ds", "http://www.w3.org/2000/09/xmldsig#"); + ns.put("xades132", XAdESConstants.XADES_V132_NS); + xpath.setNamespaceContext(new DSNamespaceContext(ns)); + return xpath; + } + + // ----------------------------------------------------------------- + // Helpers — document parsing + // ----------------------------------------------------------------- + + private Document parseDocument(byte[] xml) throws Exception { + try (InputStream is = new ByteArrayInputStream(xml)) { + return XMLUtils.read(is, false); + } + } + + // ----------------------------------------------------------------- + // Helpers — key & certificate generation + // ----------------------------------------------------------------- + + private KeyPair generateRsaKeyPair() throws Exception { + return generateKeyPair("RSA", "2048"); + } + + private KeyPair generateKeyPair(String algorithm, String param) throws Exception { + switch (algorithm) { + case "RSA": { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(Integer.parseInt(param)); + return kpg.generateKeyPair(); + } + case "EC": { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(new ECGenParameterSpec(param)); + return kpg.generateKeyPair(); + } + case "EdDSA": { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(param); + return kpg.generateKeyPair(); + } + default: + throw new IllegalArgumentException("Unknown key algorithm: " + algorithm); + } + } + + private X509Certificate generateSelfSignedCert(KeyPair keyPair, + String sigAlgorithm, + String subjectDN) throws Exception { + return SelfSignedCertGenerator.generate(keyPair, sigAlgorithm, subjectDN, 365); + } +} diff --git a/src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java b/src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java new file mode 100644 index 000000000..8c3056aef --- /dev/null +++ b/src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java @@ -0,0 +1,274 @@ +/** + * 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.utils; + +import java.io.ByteArrayInputStream; +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.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * 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+)
  • + *
+ * + *

Limitations

+ *
    + *
  • Only the {@code CN} attribute is supported in the subject/issuer DN.
  • + *
  • 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. + */ +public final class SelfSignedCertGenerator { + + private SelfSignedCertGenerator() {} + + /** + * 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 algorithms include a trailing {@code NULL} parameters element (RFC 4055 §3.2). + * ECDSA and EdDSA algorithms omit parameters entirely (RFC 5758, RFC 8410). + */ + private static final Map ALG_IDS; + static { + Map m = new HashMap<>(); + // sha256WithRSAEncryption 1.2.840.113549.1.1.11 (RFC 4055) + m.put("SHA256withRSA", hex("300d06092a864886f70d01010b0500")); + // sha384WithRSAEncryption 1.2.840.113549.1.1.12 + m.put("SHA384withRSA", hex("300d06092a864886f70d01010c0500")); + // sha512WithRSAEncryption 1.2.840.113549.1.1.13 + m.put("SHA512withRSA", hex("300d06092a864886f70d01010d0500")); + // ecdsa-with-SHA256 1.2.840.10045.4.3.2 (RFC 5758) + m.put("SHA256withECDSA", hex("300a06082a8648ce3d040302")); + // ecdsa-with-SHA384 1.2.840.10045.4.3.3 + m.put("SHA384withECDSA", hex("300a06082a8648ce3d040303")); + // ecdsa-with-SHA512 1.2.840.10045.4.3.4 + m.put("SHA512withECDSA", hex("300a06082a8648ce3d040304")); + // id-Ed25519 1.3.101.112 (RFC 8410) + m.put("Ed25519", hex("300506032b6570")); + // id-Ed448 1.3.101.113 + m.put("Ed448", hex("300506032b6571")); + ALG_IDS = Collections.unmodifiableMap(m); + } + + /** + * 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 — only the {@code CN} attribute is used, + * e.g. {@code "CN=Test Certificate"} + * @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 = seq(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) { + // [0] EXPLICIT INTEGER 2 → version v3 + byte[] version = new byte[]{(byte) 0xA0, 0x03, 0x02, 0x01, 0x02}; + // 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 seq(cat(version, serial, algId, name, validity, name, spki)); + } + + private static byte[] buildValidity(int validityDays) { + Instant notBefore = Instant.now(); + Instant notAfter = notBefore.plusSeconds((long) validityDays * 86_400L); + return seq(cat(utcTime(notBefore), utcTime(notAfter))); + } + + // ------------------------------------------------------------------------- + // DN encoding — only CN attribute (OID 2.5.4.3) supported + // ------------------------------------------------------------------------- + + /** + * Encodes a Name containing a single CN attribute. + * + *
+     * Name ::= SEQUENCE OF SET OF SEQUENCE { OID, value }
+     * 
+ */ + private static byte[] encodeName(String dn) { + // OID for commonName (2.5.4.3): 06 03 55 04 03 + byte[] cnOid = hex("0603550403"); + byte[] cnValue = tlv(0x0C, extractCN(dn).getBytes(StandardCharsets.UTF_8)); // UTF8String + return seq(set(seq(cat(cnOid, cnValue)))); + } + + /** Extracts the CN value from a DN string such as {@code "CN=My Test,O=Acme"}. */ + private static String extractCN(String dn) { + for (String part : dn.split(",")) { + String trimmed = part.strip(); + if (trimmed.regionMatches(true, 0, "CN=", 0, 3)) { + return trimmed.substring(3).strip(); + } + } + return dn; // fallback: treat the whole string as the CN value + } + + // ------------------------------------------------------------------------- + // DER / ASN.1 primitives + // ------------------------------------------------------------------------- + + private static byte[] seq(byte[] content) { return tlv(0x30, content); } + private static byte[] set(byte[] content) { return tlv(0x31, 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(0x02, value.toByteArray()); + } + + private static byte[] bitString(byte[] value) { + // Prefix with 0x00 = "0 unused bits in the final octet" + byte[] content = new byte[1 + value.length]; + content[0] = 0x00; + System.arraycopy(value, 0, content, 1, value.length); + return tlv(0x03, content); + } + + // 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(0x17, 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; + } + + /** Decodes a lowercase or uppercase hex string to bytes. */ + private static byte[] hex(String s) { + byte[] b = new byte[s.length() / 2]; + for (int i = 0; i < b.length; i++) { + b[i] = (byte) Integer.parseInt(s, i * 2, i * 2 + 2, 16); + } + return b; + } +} From 953f8008e15addf9f81c016d7d39589ba2db89dd Mon Sep 17 00:00:00 2001 From: Joze RIHTARSIC Date: Tue, 16 Jun 2026 07:52:21 +0200 Subject: [PATCH 3/5] [SANTUARIO-615] Implements secure digest algorithm validation for XAdES --- .../extension/xades/XAdESBBValidator.java | 30 ++++++++++++------ .../extension/xades/XAdESConstants.java | 20 +++++++++++- .../xades/XAdESSignatureProcessor.java | 31 +++++++++++-------- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java index efbda115c..4873e737b 100644 --- a/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESBBValidator.java @@ -18,7 +18,7 @@ */ package org.apache.xml.security.extension.xades; -import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.signature.Reference; import org.apache.xml.security.signature.SignedInfo; @@ -40,8 +40,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -87,7 +85,9 @@ */ public final class XAdESBBValidator { - private static final String XADES_SCHEMA_RESOURCE ="bindings/schemas/XAdES01903v141-202107.xsd"; + private static final String XADES_SCHEMA_RESOURCE = + "bindings/schemas/XAdES01903v132-201601.xsd"; + /** * Schema is thread-safe once constructed; load once and share. @@ -106,7 +106,7 @@ private static Schema loadSchema() { XADES_SCHEMA_RESOURCE, XAdESBBValidator.class)) { return sf.newSchema(new StreamSource(xadesIs, xadesUri)); } - } catch (SAXException | IOException e) { + } catch (SAXException | IOException e) { // Logged here; validate() reports the violation rather than crashing callers System.getLogger(XAdESBBValidator.class.getName()) .log(System.Logger.Level.ERROR, @@ -123,6 +123,17 @@ private static String resourceUri(String path) { return url.toExternalForm(); } + + private final boolean secureValidation; + + public XAdESBBValidator() { + this(true); + } + + public XAdESBBValidator(boolean secureValidation) { + this.secureValidation = secureValidation; + } + /** * Validates XAdES-B-B properties in {@code signature}. * @@ -259,9 +270,8 @@ private void validateCertDigest(Element qualifyingProps, return; } - String jceAlgorithm = JCEMapper.translateURItoJCEID(algorithmURI); - if (jceAlgorithm == null) { - violations.add("Unknown digest algorithm URI in CertDigest: " + algorithmURI); + if (secureValidation && !XAdESConstants.APPROVED_CERT_DIGEST_ALGORITHM_URIS.contains(algorithmURI)) { + violations.add("CertDigest uses a weak or disallowed digest algorithm: " + algorithmURI); return; } @@ -276,8 +286,8 @@ private void validateCertDigest(Element qualifyingProps, byte[] actualDigest; try { byte[] certDer = signingCertificate.getEncoded(); - actualDigest = MessageDigest.getInstance(jceAlgorithm).digest(certDer); - } catch (CertificateEncodingException | NoSuchAlgorithmException e) { + actualDigest = MessageDigestAlgorithm.getDigestInstance(algorithmURI).digest(certDer); + } catch (CertificateEncodingException | XMLSecurityException e) { violations.add("Cannot compute signing certificate digest: " + e.getMessage()); return; } diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java index 35433665b..8da75e168 100644 --- a/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java @@ -18,8 +18,12 @@ */ package org.apache.xml.security.extension.xades; +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; + +import java.util.Set; + /** - * Namespace URIs, prefixes, and element tag constants for XAdES v1.3.2 and v1.4.1. + * Namespace URIs, prefixes, element tag constants, and approved algorithm sets for XAdES v1.3.2 and v1.4.1. * * @see * ETSI EN 319 132-1 (XAdES) @@ -29,6 +33,20 @@ public final class XAdESConstants { private XAdESConstants() { } + /** + * Algorithm URIs accepted for certificate digest in XAdES {@code CertDigest}. + * Only SHA-2 and SHA-3 variants with at least 256-bit output are permitted; + * MD5, SHA-1, SHA-224, and other weak/deprecated algorithms are rejected. + * Used by both {@link XAdESSignatureProcessor} (signing) and {@link XAdESBBValidator} (validation) + */ + public static final Set APPROVED_CERT_DIGEST_ALGORITHM_URIS = Set.of( + MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256, + MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA384, + MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA512, + MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA3_256, + MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA3_384, + MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA3_512); + public static final String XADES_V132_NS = "http://uri.etsi.org/01903/v1.3.2#"; public static final String XADES_V141_NS = "http://uri.etsi.org/01903/v1.4.1#"; diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java index 95eaf94d9..df8d655ca 100644 --- a/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java @@ -18,8 +18,9 @@ */ package org.apache.xml.security.extension.xades; -import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.extension.SignatureExtensionException; import org.apache.xml.security.extension.SignatureProcessor; import org.apache.xml.security.signature.ObjectContainer; @@ -30,8 +31,6 @@ import org.apache.xml.security.transforms.Transforms; import org.w3c.dom.Document; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.time.OffsetDateTime; @@ -143,12 +142,6 @@ private SignedSignatureProperties buildSignedSignatureProperties(Document doc) } private SigningCertificate buildSigningCertificate(Document doc) throws XMLSignatureException { - String jceAlgorithm = JCEMapper.translateURItoJCEID(certificateDigestAlgorithmURI); - if (jceAlgorithm == null) { - throw new SignatureExtensionException( - "Unknown digest algorithm URI: " + certificateDigestAlgorithmURI); - } - byte[] certDer; try { certDer = certificate.getEncoded(); @@ -158,8 +151,8 @@ private SigningCertificate buildSigningCertificate(Document doc) throws XMLSigna byte[] digest; try { - digest = MessageDigest.getInstance(jceAlgorithm).digest(certDer); - } catch (NoSuchAlgorithmException e) { + digest = MessageDigestAlgorithm.getDigestInstance(certificateDigestAlgorithmURI).digest(certDer); + } catch (XMLSecurityException e) { throw new SignatureExtensionException( "Digest algorithm not available: " + certificateDigestAlgorithmURI, e); } @@ -175,13 +168,13 @@ private SigningCertificate buildSigningCertificate(Document doc) throws XMLSigna return sc; } - private void ensureSignatureId(XMLSignature signature) throws XMLSignatureException { + private void ensureSignatureId(XMLSignature signature) { if (isBlank(signature.getId())) { signature.setId(IDGenerator.generateID(ID_PREFIX_SIG)); } } - private void ensureSignatureValueId(XMLSignature signature) throws XMLSignatureException { + private void ensureSignatureValueId(XMLSignature signature){ if (isBlank(signature.getSignatureValueId())) { signature.setSignatureValueId(IDGenerator.generateID(ID_PREFIX_SIG_VAL)); } @@ -220,6 +213,7 @@ private static boolean isBlank(String value) { */ public static final class Builder { + private boolean allowWeakAlgorithms = false; private final X509Certificate certificate; private String certificateDigestAlgorithmURI = XMLCipher.SHA256; private boolean signaturePolicyImplied = false; @@ -261,6 +255,12 @@ public Builder withSignatureCountryName(String countryName) { return this; } + /** When {@code true}, allows weak digest algorithms (e.g. SHA-1) to be used for certificate digest. */ + public Builder withAllowWeakAlgorithms(boolean allowWeakAlgorithms) { + this.allowWeakAlgorithms = allowWeakAlgorithms; + return this; + } + /** * Adds a canonicalization or transform algorithm URI to apply to the * {@code SignedProperties} reference before digesting. Algorithms are applied in @@ -273,6 +273,11 @@ public Builder addReferenceTransformAlgorithm(String algorithm) { public XAdESSignatureProcessor build() { Objects.requireNonNull(certificateDigestAlgorithmURI, "certificateDigestAlgorithmURI"); + if (!this.allowWeakAlgorithms && !XAdESConstants.APPROVED_CERT_DIGEST_ALGORITHM_URIS.contains(certificateDigestAlgorithmURI)) { + throw new IllegalArgumentException( + "certificateDigestAlgorithmURI uses a weak or disallowed digest algorithm: " + + certificateDigestAlgorithmURI); + } return new XAdESSignatureProcessor(this); } } From e60115d8e3d546a34704f916bf462846a9515409 Mon Sep 17 00:00:00 2001 From: Joze RIHTARSIC Date: Fri, 26 Jun 2026 20:01:41 +0200 Subject: [PATCH 4/5] [SANTUARIO-615] Update test util SelfSignedCertGenerator --- .../extension/xades/XAdESSignatureTest.java | 2 +- .../SelfSignedCertGenerator.java | 196 +++++++++++++----- 2 files changed, 142 insertions(+), 56 deletions(-) rename src/test/java/org/apache/xml/security/{utils => testutils}/SelfSignedCertGenerator.java (60%) diff --git a/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java b/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java index d8ca387af..99f14e1c0 100644 --- a/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java +++ b/src/test/java/org/apache/xml/security/extension/xades/XAdESSignatureTest.java @@ -31,7 +31,7 @@ import org.apache.xml.security.testutils.JDKTestUtils; import org.apache.xml.security.transforms.Transforms; import org.apache.xml.security.utils.Constants; -import org.apache.xml.security.utils.SelfSignedCertGenerator; +import org.apache.xml.security.testutils.SelfSignedCertGenerator; import org.apache.xml.security.utils.XMLUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java b/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java similarity index 60% rename from src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java rename to src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java index 8c3056aef..26182b024 100644 --- a/src/test/java/org/apache/xml/security/utils/SelfSignedCertGenerator.java +++ b/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java @@ -6,9 +6,9 @@ * 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 @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.xml.security.utils; +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; @@ -28,8 +29,6 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; /** @@ -40,7 +39,7 @@ * 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}
  • @@ -59,7 +58,8 @@ */ public final class SelfSignedCertGenerator { - private SelfSignedCertGenerator() {} + private SelfSignedCertGenerator() { + } /** * Pre-encoded DER bytes for the {@code AlgorithmIdentifier} of each supported @@ -69,27 +69,15 @@ private SelfSignedCertGenerator() {} *

    RSA algorithms include a trailing {@code NULL} parameters element (RFC 4055 §3.2). * ECDSA and EdDSA algorithms omit parameters entirely (RFC 5758, RFC 8410). */ - private static final Map ALG_IDS; - static { - Map m = new HashMap<>(); - // sha256WithRSAEncryption 1.2.840.113549.1.1.11 (RFC 4055) - m.put("SHA256withRSA", hex("300d06092a864886f70d01010b0500")); - // sha384WithRSAEncryption 1.2.840.113549.1.1.12 - m.put("SHA384withRSA", hex("300d06092a864886f70d01010c0500")); - // sha512WithRSAEncryption 1.2.840.113549.1.1.13 - m.put("SHA512withRSA", hex("300d06092a864886f70d01010d0500")); - // ecdsa-with-SHA256 1.2.840.10045.4.3.2 (RFC 5758) - m.put("SHA256withECDSA", hex("300a06082a8648ce3d040302")); - // ecdsa-with-SHA384 1.2.840.10045.4.3.3 - m.put("SHA384withECDSA", hex("300a06082a8648ce3d040303")); - // ecdsa-with-SHA512 1.2.840.10045.4.3.4 - m.put("SHA512withECDSA", hex("300a06082a8648ce3d040304")); - // id-Ed25519 1.3.101.112 (RFC 8410) - m.put("Ed25519", hex("300506032b6570")); - // id-Ed448 1.3.101.113 - m.put("Ed448", hex("300506032b6571")); - ALG_IDS = Collections.unmodifiableMap(m); - } + private static final Map ALG_IDS = Map.of( + "SHA256withRSA", encodeAlgorithmIdentifier("1.2.840.113549.1.1.11"), + "SHA384withRSA", encodeAlgorithmIdentifier("1.2.840.113549.1.1.12"), + "SHA512withRSA", encodeAlgorithmIdentifier("1.2.840.113549.1.1.13"), + "SHA256withECDSA", encodeAlgorithmIdentifier("1.2.840.10045.4.3.2"), + "SHA384withECDSA", encodeAlgorithmIdentifier("1.2.840.10045.4.3.3"), + "SHA512withECDSA", encodeAlgorithmIdentifier("1.2.840.10045.4.3.4"), + "Ed25519", encodeAlgorithmIdentifier("1.3.101.112"), + "Ed448", encodeAlgorithmIdentifier("1.3.101.113")); /** * Generates a self-signed X.509 v3 certificate. @@ -115,9 +103,9 @@ public static X509Certificate generate(KeyPair keyPair, } // 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); + 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()); @@ -125,7 +113,7 @@ public static X509Certificate generate(KeyPair keyPair, byte[] sigBytes = signer.sign(); // Certificate ::= SEQUENCE { TBSCertificate, AlgorithmIdentifier, BIT STRING } - byte[] certDer = seq(cat(tbs, algId, bitString(sigBytes))); + byte[] certDer = sequence(cat(tbs, algId, bitString(sigBytes))); return (X509Certificate) CertificateFactory.getInstance("X.509") .generateCertificate(new ByteArrayInputStream(certDer)); @@ -153,18 +141,18 @@ public static X509Certificate generate(KeyPair keyPair, private static byte[] buildTbs(byte[] algId, byte[] name, byte[] spki, int validityDays) { // [0] EXPLICIT INTEGER 2 → version v3 - byte[] version = new byte[]{(byte) 0xA0, 0x03, 0x02, 0x01, 0x02}; + byte[] version = new byte[]{(byte) 0xA0, 0x03, 0x02, 0x01, 0x02}; // Serial: milliseconds since epoch — unique enough for test certs - byte[] serial = integer(BigInteger.valueOf(System.currentTimeMillis())); + byte[] serial = integer(BigInteger.valueOf(System.currentTimeMillis())); byte[] validity = buildValidity(validityDays); // issuer == subject for self-signed - return seq(cat(version, serial, algId, name, validity, name, spki)); + return sequence(cat(version, serial, algId, name, validity, name, spki)); } private static byte[] buildValidity(int validityDays) { Instant notBefore = Instant.now(); - Instant notAfter = notBefore.plusSeconds((long) validityDays * 86_400L); - return seq(cat(utcTime(notBefore), utcTime(notAfter))); + Instant notAfter = notBefore.plusSeconds(validityDays * 86_400L); + return sequence(cat(utcTime(notBefore), utcTime(notAfter))); } // ------------------------------------------------------------------------- @@ -180,12 +168,14 @@ private static byte[] buildValidity(int validityDays) { */ private static byte[] encodeName(String dn) { // OID for commonName (2.5.4.3): 06 03 55 04 03 - byte[] cnOid = hex("0603550403"); + byte[] cnOid = encodeOid("2.5.4.3"); byte[] cnValue = tlv(0x0C, extractCN(dn).getBytes(StandardCharsets.UTF_8)); // UTF8String - return seq(set(seq(cat(cnOid, cnValue)))); + return sequence(set(sequence(cat(cnOid, cnValue)))); } - /** Extracts the CN value from a DN string such as {@code "CN=My Test,O=Acme"}. */ + /** + * Extracts the CN value from a DN string such as {@code "CN=My Test,O=Acme"}. + */ private static String extractCN(String dn) { for (String part : dn.split(",")) { String trimmed = part.strip(); @@ -200,8 +190,68 @@ private static String extractCN(String dn) { // DER / ASN.1 primitives // ------------------------------------------------------------------------- - private static byte[] seq(byte[] content) { return tlv(0x30, content); } - private static byte[] set(byte[] content) { return tlv(0x31, content); } + /** + * 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.

    + * + *

    Use in X.509:
    + * Distinguished Names (DNs), RelativeDistinguishedNames (RDNs), and + * AttributeTypeAndValue pairs are all encoded using SEQUENCE structures. For example, + * an AttributeTypeAndValue is defined as:

    + * + *
    +     * 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(0x30, 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(0x31, content); + } private static byte[] integer(BigInteger value) { // toByteArray() produces two's-complement big-endian; positive integers may @@ -211,11 +261,7 @@ private static byte[] integer(BigInteger value) { } private static byte[] bitString(byte[] value) { - // Prefix with 0x00 = "0 unused bits in the final octet" - byte[] content = new byte[1 + value.length]; - content[0] = 0x00; - System.arraycopy(value, 0, content, 1, value.length); - return tlv(0x03, content); + return tlv(0x03, cat(new byte[]{0x00}, value)); // 0x00 = zero unused bits } // UTCTime covers 2000–2049 (yy < 50 → 20yy). Sufficient for short-lived test certs. @@ -244,11 +290,13 @@ private static byte[] tlv(int tag, byte[] value) { 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); + System.arraycopy(value, 0, out, 1 + lenBytes.length, len); return out; } - /** Concatenates byte arrays. */ + /** + * Concatenates byte arrays. + */ private static byte[] cat(byte[]... parts) { int total = 0; for (byte[] p : parts) { @@ -263,12 +311,50 @@ private static byte[] cat(byte[]... parts) { return buf; } - /** Decodes a lowercase or uppercase hex string to bytes. */ - private static byte[] hex(String s) { - byte[] b = new byte[s.length() / 2]; - for (int i = 0; i < b.length; i++) { - b[i] = (byte) Integer.parseInt(s, i * 2, i * 2 + 2, 16); + /** + * Encode oid as certificate algorithm identifier. + * @param oid + * @return + */ + public static byte[] encodeAlgorithmIdentifier(String oid) { + // RFC 8410: EdDSA parameters MUST be absent; RSA/ECDSA require a NULL (RFC 4055/5758) + byte[] nullParam = oid.startsWith("1.3.101.11") ? new byte[0] : new byte[]{0x05, 0x00}; + return sequence(cat(encodeOid(oid), nullParam)); + } + + /** + * 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(0x06, 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 b; + return result; } } From a3a9ecbbca129f1f427f075eab11896291c2b6b1 Mon Sep 17 00:00:00 2001 From: Joze RIHTARSIC Date: Fri, 26 Jun 2026 20:37:24 +0200 Subject: [PATCH 5/5] [SANTUARIO-615] Update test util SelfSignedCertGenerator --- .../testutils/SelfSignedCertGenerator.java | 187 +++++++++++++----- 1 file changed, 141 insertions(+), 46 deletions(-) diff --git a/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java b/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java index 26182b024..88108ae6d 100644 --- a/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java +++ b/src/test/java/org/apache/xml/security/testutils/SelfSignedCertGenerator.java @@ -34,7 +34,7 @@ /** * 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 + *

    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 @@ -47,9 +47,16 @@ *

  • EdDSA — {@code Ed25519}, {@code Ed448} (requires Java 15+)
  • *
* + *

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

*
    - *
  • Only the {@code CN} attribute is supported in the subject/issuer DN.
  • *
  • No X.509 extensions are added (basic-constraints, key-usage, etc.).
  • *
  • Validity dates use UTCTime, which covers years 2000–2049.
  • *
@@ -61,6 +68,79 @@ 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"; + + // ------------------------------------------------------------------------- + // 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 @@ -70,14 +150,14 @@ private SelfSignedCertGenerator() { * ECDSA and EdDSA algorithms omit parameters entirely (RFC 5758, RFC 8410). */ private static final Map ALG_IDS = Map.of( - "SHA256withRSA", encodeAlgorithmIdentifier("1.2.840.113549.1.1.11"), - "SHA384withRSA", encodeAlgorithmIdentifier("1.2.840.113549.1.1.12"), - "SHA512withRSA", encodeAlgorithmIdentifier("1.2.840.113549.1.1.13"), - "SHA256withECDSA", encodeAlgorithmIdentifier("1.2.840.10045.4.3.2"), - "SHA384withECDSA", encodeAlgorithmIdentifier("1.2.840.10045.4.3.3"), - "SHA512withECDSA", encodeAlgorithmIdentifier("1.2.840.10045.4.3.4"), - "Ed25519", encodeAlgorithmIdentifier("1.3.101.112"), - "Ed448", encodeAlgorithmIdentifier("1.3.101.113")); + "SHA256withRSA", encodeAlgorithmIdentifier(OID_SHA256_WITH_RSA), + "SHA384withRSA", encodeAlgorithmIdentifier(OID_SHA384_WITH_RSA), + "SHA512withRSA", encodeAlgorithmIdentifier(OID_SHA512_WITH_RSA), + "SHA256withECDSA", encodeAlgorithmIdentifier(OID_SHA256_WITH_ECDSA), + "SHA384withECDSA", encodeAlgorithmIdentifier(OID_SHA384_WITH_ECDSA), + "SHA512withECDSA", encodeAlgorithmIdentifier(OID_SHA512_WITH_ECDSA), + "Ed25519", encodeAlgorithmIdentifier(OID_ED25519), + "Ed448", encodeAlgorithmIdentifier(OID_ED448)); /** * Generates a self-signed X.509 v3 certificate. @@ -85,8 +165,8 @@ private SelfSignedCertGenerator() { * @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 — only the {@code CN} attribute is used, - * e.g. {@code "CN=Test Certificate"} + * @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 @@ -140,13 +220,11 @@ public static X509Certificate generate(KeyPair keyPair, */ private static byte[] buildTbs(byte[] algId, byte[] name, byte[] spki, int validityDays) { - // [0] EXPLICIT INTEGER 2 → version v3 - byte[] version = new byte[]{(byte) 0xA0, 0x03, 0x02, 0x01, 0x02}; // 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(version, serial, algId, name, validity, name, spki)); + return sequence(cat(TBS_VERSION_V3, serial, algId, name, validity, name, spki)); } private static byte[] buildValidity(int validityDays) { @@ -156,7 +234,7 @@ private static byte[] buildValidity(int validityDays) { } // ------------------------------------------------------------------------- - // DN encoding — only CN attribute (OID 2.5.4.3) supported + // DN encoding — CN, C, O, OU attributes (RFC 4519) // ------------------------------------------------------------------------- /** @@ -167,23 +245,46 @@ private static byte[] buildValidity(int validityDays) { * */ private static byte[] encodeName(String dn) { - // OID for commonName (2.5.4.3): 06 03 55 04 03 - byte[] cnOid = encodeOid("2.5.4.3"); - byte[] cnValue = tlv(0x0C, extractCN(dn).getBytes(StandardCharsets.UTF_8)); // UTF8String - return sequence(set(sequence(cat(cnOid, cnValue)))); - } - - /** - * Extracts the CN value from a DN string such as {@code "CN=My Test,O=Acme"}. - */ - private static String extractCN(String dn) { + ByteArrayOutputStream rdns = new ByteArrayOutputStream(); for (String part : dn.split(",")) { String trimmed = part.strip(); - if (trimmed.regionMatches(true, 0, "CN=", 0, 3)) { - return trimmed.substring(3).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 dn; // fallback: treat the whole string as the CN value + return sequence(rdns.toByteArray()); } // ------------------------------------------------------------------------- @@ -195,12 +296,6 @@ private static String extractCN(String dn) { * *

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

The DER tag for a SEQUENCE is 0x30.

- * - *

Use in X.509:
- * Distinguished Names (DNs), RelativeDistinguishedNames (RDNs), and - * AttributeTypeAndValue pairs are all encoded using SEQUENCE structures. For example, - * an AttributeTypeAndValue is defined as:

- * *
      * AttributeTypeAndValue ::= SEQUENCE {
      *   type   OBJECT IDENTIFIER,
@@ -220,10 +315,9 @@ private static String extractCN(String dn) {
      * @return the DER‑encoded SEQUENCE (tag 0x30 + length + content)
      */
     private static byte[] sequence(byte[] content) {
-        return tlv(0x30, content);
+        return tlv(TAG_SEQUENCE, content);
     }
 
-
     /**
      * Encodes the provided content as an ASN.1 DER SET value.
      *
@@ -250,18 +344,18 @@ private static byte[] sequence(byte[] content) {
      * @return the DER-encoded SET (tag 0x31 + length + content)
      */
     private static byte[] set(byte[] content) {
-        return tlv(0x31, 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(0x02, value.toByteArray());
+        return tlv(TAG_INTEGER, value.toByteArray());
     }
 
     private static byte[] bitString(byte[] value) {
-        return tlv(0x03, cat(new byte[]{0x00}, value)); // 0x00 = zero unused bits
+        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.
@@ -269,7 +363,7 @@ private static byte[] bitString(byte[] value) {
             DateTimeFormatter.ofPattern("yyMMddHHmmss'Z'").withZone(ZoneOffset.UTC);
 
     private static byte[] utcTime(Instant instant) {
-        return tlv(0x17, UTC_TIME_FMT.format(instant).getBytes(StandardCharsets.US_ASCII));
+        return tlv(TAG_UTC_TIME, UTC_TIME_FMT.format(instant).getBytes(StandardCharsets.US_ASCII));
     }
 
     /**
@@ -317,9 +411,10 @@ private static byte[] cat(byte[]... parts) {
      * @return
      */
     public static byte[] encodeAlgorithmIdentifier(String oid) {
-        // RFC 8410: EdDSA parameters MUST be absent; RSA/ECDSA require a NULL (RFC 4055/5758)
-        byte[] nullParam = oid.startsWith("1.3.101.11") ? new byte[0] : new byte[]{0x05, 0x00};
-        return sequence(cat(encodeOid(oid), nullParam));
+        // RFC 8410 §6: all OIDs under arc 1.3.101 (X25519, X448, Ed25519, Ed448) MUST omit parameters.
+        // RFC 4055 §3.2: RSA signature algorithms MUST include a NULL parameters element.
+        byte[] params = oid.startsWith("1.3.101.") ? new byte[0] : DER_NULL;
+        return sequence(cat(encodeOid(oid), params));
     }
 
     /**
@@ -335,7 +430,7 @@ public static byte[] encodeOid(String oid) {
             byte[] arc = encodeBase128(Long.parseLong(parts[i]));
             body.write(arc, 0, arc.length);
         }
-        return tlv(0x06, body.toByteArray());
+        return tlv(TAG_OID, body.toByteArray());
     }
 
     /**
@@ -357,4 +452,4 @@ private static byte[] encodeBase128(long value) {
         }
         return result;
     }
-}
+}
\ No newline at end of file