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
+ * 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());
+
+ Listds: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