diff --git a/pom.xml b/pom.xml index e91197507..18595d7cc 100644 --- a/pom.xml +++ b/pom.xml @@ -366,6 +366,7 @@ bindings/schemas/xenc-schema.xsd bindings/schemas/xenc-schema-11.xsd bindings/schemas/rsa-pss.xsd + bindings/schemas/XAdES01903v141-202107.xsd ${basedir}/src/main/resources/bindings/ @@ -377,6 +378,7 @@ security-config.xjb xop.xjb rsa-pss.xjb + xades.xjb ${basedir}/src/main/resources/bindings/bindings.cat false diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index cbd67652d..163ca5c0e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -63,6 +63,8 @@ opens org.apache.xml.security; opens org.apache.xml.security.binding.excc14n; opens org.apache.xml.security.binding.xmldsig; + opens org.apache.xml.security.binding.xmldsig.xades.v132; + opens org.apache.xml.security.binding.xmldsig.xades.v141; opens org.apache.xml.security.binding.xmldsig11; opens org.apache.xml.security.binding.xmlenc; opens org.apache.xml.security.binding.xmlenc11; @@ -74,4 +76,6 @@ opens org.apache.xml.security.keys.storage.implementations; opens org.apache.xml.security.transforms.implementations; opens org.apache.xml.security.utils.resolver.implementations; + opens org.apache.xml.security.utils.jaxb; + exports org.apache.xml.security.extension.exceptions; } 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..d52cdd673 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 @@ -34,11 +34,13 @@ import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec; import javax.xml.crypto.dsig.spec.XPathType; import javax.xml.crypto.dsig.spec.XSLTTransformParameterSpec; +import javax.xml.namespace.QName; -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBElement; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import org.w3c.dom.*; /** * Useful static DOM utility methods. @@ -46,6 +48,10 @@ */ public final class DOMUtils { + // most common attributes for id attribute names in XML + private static final List ID_ATTRIBUTE_NAMES = List.of("Id", "ID", "id"); + + // class cannot be instantiated private DOMUtils() {} @@ -422,4 +428,65 @@ public static boolean isNamespace(Node node) } return false; } + + /** + * This method convert JAXB object to XML Element value and add it to the + * target node. The root object of JAXB object has name as defined in + * objectQName. + * The method also sets ID flag to IDs in the XML structure. + * + * @param target the node to which the XML structure should be added + * @param obj the object to be converted to Element value + * @param objectQName the QName of the object root element + * @return the created XML structure as a Node + * @throws JAXBException if an error occurs during the marshalling + */ + public static Node objectToXMLStructure(Node target, Object obj, QName objectQName) throws JAXBException { + + JAXBContext jc = JAXBContext.newInstance(obj.getClass()); + Marshaller jaxbMarshaller = jc.createMarshaller(); + JAXBElement jaxbElement = new JAXBElement( + objectQName, + obj.getClass(), obj); + jaxbMarshaller.marshal(jaxbElement, target); + // set idness to all elements so that they can be used as references + setIdFlagToIdAttributes(target); + return target.getFirstChild(); + } + + /** + * This method declares all attributes with names: ID, id Id to be a + * user-determined ID attribute. This affects the value of + * Attr.isId and the behavior of + * Document.getElementById. + * + * @param n Node to start from setting id attributes as Id attribute + */ + public static void setIdFlagToIdAttributes(Node n) { + setIdFlagToIdAttributes(n, ID_ATTRIBUTE_NAMES); + } + + /** + * This method declares all attributes with names in idAttributes to be a + * user-determined ID attribute. This affects the value of + * Attr.isId and the behavior of + * Document.getElementById. + * + * @param n Node to start from setting id attributes as Id attribute + * @param idAttributes List of attribute names to be set as Id attribute + */ + public static void setIdFlagToIdAttributes(Node n, List idAttributes) { + if (n.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) n; + idAttributes.forEach(id -> { + if (e.hasAttribute(id)) { + e.setIdAttribute(id, true); + } + }); + NodeList l = e.getChildNodes(); + for (int i = 0; i < l.getLength(); i++) { + setIdFlagToIdAttributes(l.item(i), idAttributes); + } + } + } } 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..e8d4aa2fe --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/SignatureProcessor.java @@ -0,0 +1,42 @@ +/** + * 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; + +/** + * This interface is responsible for processing signature. The implementation of + * this interface can be uses as pre-processors to add to the signature and + * additional data such as XAdES QualifyingProperties for the XAdES basic + * signatures profile. + * The implementation can be used as post-processors to add update the signatures + * after the signature has been generated. An example the Timestamp (TSA) of the + * signature, or automatic registration of the signature hast to blockchain ledger. + */ +public interface SignatureProcessor { + + /** + * Process the signature. + * + * @param signature the XMLSignature instance to be processed + * @throws XMLSignatureException if an error occurs while processing the signature + */ + void processSignature(XMLSignature signature) throws XMLSignatureException; +} diff --git a/src/main/java/org/apache/xml/security/extension/exceptions/ExtensionException.java b/src/main/java/org/apache/xml/security/extension/exceptions/ExtensionException.java new file mode 100644 index 000000000..e8c2d75d0 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/exceptions/ExtensionException.java @@ -0,0 +1,47 @@ +/** + * 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.exceptions; + +/** + * This Exception is thrown while extension processing fails. + * + */ +public class ExtensionException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructor ExtensionException + * + * @param message the message to display when this exception is thrown + */ + public ExtensionException(String message) { + super(message); + } + + /** + * Constructor ExtensionException + * + * @param message the message to display when this exception is thrown + * @param cause the cause of this exception + */ + public ExtensionException(String message, Throwable cause) { + super(message, cause); + } +} 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..7c322098b --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESConstants.java @@ -0,0 +1,40 @@ +/** + * 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; + +/** + * Constants for the XAdES specification. + */ +public final class XAdESConstants { + private XAdESConstants() { + } + + // XAdES namespace and prefix + 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"; + + /** SignedProperties reference type **/ + public static final String REFERENCE_TYPE_SIGNEDPROPERTIES = "http://uri.etsi.org/01903#SignedProperties"; + + /** Tag of Element CanonicalizationMethod **/ + public static final String _TAG_QUALIFYINGPROPERTIES = "QualifyingProperties"; + +} diff --git a/src/main/java/org/apache/xml/security/extension/xades/XAdESQualifyingPropertiesBuilder.java b/src/main/java/org/apache/xml/security/extension/xades/XAdESQualifyingPropertiesBuilder.java new file mode 100644 index 000000000..fbf5b69c3 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESQualifyingPropertiesBuilder.java @@ -0,0 +1,280 @@ +/** + * 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.binding.xmldsig.DigestMethodType; +import org.apache.xml.security.binding.xmldsig.X509IssuerSerialType; +import org.apache.xml.security.binding.xmldsig.xades.v132.*; +import org.apache.xml.security.extension.exceptions.ExtensionException; + +import javax.xml.datatype.DatatypeConfigurationException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.OffsetDateTime; + + +/** + * This class is used to build XAdES QualifyingProperties with compliance to + * XAdES-B-B (Basic Electronic Signature) structure. The lowest and simplest + * version just containing the SignedInfo, SignatureValue, KeyInfo and + * SignedProperties. This form extends the definition of an electronic signature + * to conform to the identified signature policy. + *

+ * The XAdESQualifyingPropertiesBuilder adds the following elements to [XMLDSIG]: + *

+ * QualifyingProperties
+ *     SignedProperties
+ *         SignedSignatureProperties
+ *             SigningTime
+ *             SigningCertificate
+ *             SignaturePolicyIdentifier
+ *             SignatureProductionPlace?
+ * 
+ *

+ * @see XAdES + * @see + * ETSI TS 101 903 V1.4.2 + */ +public class XAdESQualifyingPropertiesBuilder { + + String signatureId; + String xadesSignaturePropertiesId; + X509Certificate signingCertificate; + String certificateDigestMethodURI; + String signaturePolicy; + String signatureCity; + String signatureCountryName; + + protected XAdESQualifyingPropertiesBuilder() { + } + + /** + * Create a new instance of XAdESQualifyingPropertiesBuilder + * + * @return XAdESQualifyingPropertiesBuilder + */ + public static XAdESQualifyingPropertiesBuilder create() { + return new XAdESQualifyingPropertiesBuilder(); + } + + /** + * Set the signature identifier to be targeted from element + * QualifyingProperties/@Target + * + * @param signatureId - signature identifier + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withSignatureId(String signatureId) { + this.signatureId = signatureId; + return this; + } + + + /** + * Set the identifier for the XAdES SignatureProperties element + * + * @param xadesSignaturePropertiesId - XAdES SignatureProperties identifier + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withXAdESSignaturePropertiesId(String xadesSignaturePropertiesId) { + this.xadesSignaturePropertiesId = xadesSignaturePropertiesId; + return this; + } + + /** + * Set the signing certificate + * + * @param signingCertificate - signing certificate to be included in the + * XAdES QualifyingProperties + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withSigningCertificate(X509Certificate signingCertificate) { + this.signingCertificate = signingCertificate; + return this; + } + + /** + * Set the digest method URI for the certificate + * + * @param digestURI - digest method URI for calculating the certificate digest + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withCertificateDigestMethodURI(String digestURI) { + this.certificateDigestMethodURI = digestURI; + return this; + } + + /** + * Set the signature policy + * + * @param signaturePolicy - signature policy to be included in the + * XAdES QualifyingProperties + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withSignaturePolicy(String signaturePolicy) { + this.signaturePolicy = signaturePolicy; + return this; + } + + /** + * Set the city where the signature was created (Optional) + * + * @param signatureCity - city where the signature was created + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withSignatureCity(String signatureCity) { + this.signatureCity = signatureCity; + return this; + } + + /** + * Set the country name where the signature was created (Optional) + * + * @param signatureCountryName - country name where the signature was created + * @return XAdESQualifyingPropertiesBuilder for continued configuration + */ + public XAdESQualifyingPropertiesBuilder withSignatureCountryName(String signatureCountryName) { + this.signatureCountryName = signatureCountryName; + return this; + } + + /** + * Build the XAdES QualifyingProperties + * + * @return QualifyingPropertiesType + * @throws CertificateEncodingException + * @throws NoSuchAlgorithmException + * @throws DatatypeConfigurationException + */ + public QualifyingPropertiesType build() throws ExtensionException { + return createXAdESQualifyingProperties(signatureId, + xadesSignaturePropertiesId, + signingCertificate, + certificateDigestMethodURI, + signaturePolicy, + signatureCity, + signatureCountryName); + } + + + /** + * Method creates Signature/Object/QualifyingProperties/*SignedProperties* for signed certificate + * + * @param strSigPropId signed properties id + * @param cert, signing certificate + * @param digestUri digest method code (JCA provider code and W3c - URI) + * @param signatureReason value for: SignaturePolicyIdentifier/SignaturePolicyImplied - The + * signature policy is a set of rules for the creation and validation of an electronic signature, + * under which the signature can be determined to be valid. A given legal/contractual context may + * recognize a particular signature policy as meeting its requirements. + * @param sigCity city where signature was created + * @param sigCountryName country name where signature was created + * @return XAdES data structure: SignedProperties + */ + private SignedPropertiesType createSignedProperties(String strSigPropId, + X509Certificate cert, + String digestUri, + String signatureReason, + String sigCity, + String sigCountryName) throws ExtensionException { + SignedPropertiesType sp = new SignedPropertiesType(); + + sp.setId(strSigPropId); + CertIDListType scert = new CertIDListType(); + CertIDType sit = new CertIDType(); + DigestAlgAndValueType dt = new DigestAlgAndValueType(); + + MessageDigest md; + try { + md = MessageDigest.getInstance(JCEMapper.translateURItoJCEID(digestUri)); + } catch (NoSuchAlgorithmException ex) { + throw new ExtensionException("Message digest ["+digestUri+"] is not supported!", ex); + } + + byte[] der; + try { + der = cert.getEncoded(); + } catch (CertificateEncodingException ex) { + throw new ExtensionException("Certificate encoding error!", ex); + } + md.update(der); + dt.setDigestValue(md.digest()); + dt.setDigestMethod(new DigestMethodType()); + dt.getDigestMethod().setAlgorithm(digestUri); + sit.setCertDigest(dt); + sit.setIssuerSerial(new X509IssuerSerialType()); + sit.getIssuerSerial().setX509IssuerName(cert.getIssuerDN().getName()); + sit.getIssuerSerial().setX509SerialNumber(cert.getSerialNumber()); + SignedSignaturePropertiesType ssp = new SignedSignaturePropertiesType(); + ssp.setSigningTime(OffsetDateTime.now()); + ssp.setSigningCertificate(scert); + if (signatureReason != null){ + ssp.setSignaturePolicyIdentifier(new SignaturePolicyIdentifierType()); + ssp.getSignaturePolicyIdentifier().setSignaturePolicyImplied(signatureReason); + } + + if (sigCity != null || sigCountryName != null) { + ssp.setSignatureProductionPlace(new SignatureProductionPlaceType()); + ssp.getSignatureProductionPlace().setCity(sigCity); + ssp.getSignatureProductionPlace().setCountryName(sigCountryName); + } + + scert.getCert().add(sit); + sp.setSignedSignatureProperties(ssp); + return sp; + } + + + /** + * Method creates XAdESQualifyingProperties. Object QualifyingProperties must be stored into + * XMLdSIg Signature/Object element. + * + * @param sigId - signature id to which QualifyingProperties targets + * @param strSigPropId - id for created SignedProperties (part of QualifyingProperties) which must + * be signed + * @param cert, signing certificate + * @param digestURI digest method code (JCA provider code and W3c - URI) + * @param signaturePolicy - value for: SignaturePolicyIdentifier/SignaturePolicyImplied - The + * signature policy is a set of rules for the creation and validation ofan electronic signature, + * under which the signature can be determined to be valid. A given legal/contractual context may + * recognize a particular signature policy as meeting its requirements. + * @param signatureCity - city where signature was created + * @param signatureCountryName - country name where signature was created + * @return + * @throws CertificateEncodingException + * @throws NoSuchAlgorithmException + */ + public QualifyingPropertiesType createXAdESQualifyingProperties(String sigId, String strSigPropId, + X509Certificate cert, String digestURI, + String signaturePolicy, + String signatureCity, + String signatureCountryName) + throws ExtensionException { + + QualifyingPropertiesType qt = new QualifyingPropertiesType(); + qt.setTarget("#" + sigId); + qt.setSignedProperties(createSignedProperties(strSigPropId, cert, digestURI, signaturePolicy, + signatureCity, signatureCountryName)); + + return qt; + } +} 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..7b2b4a1f2 --- /dev/null +++ b/src/main/java/org/apache/xml/security/extension/xades/XAdESSignatureProcessor.java @@ -0,0 +1,166 @@ +/** + * 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 jakarta.xml.bind.JAXBException; +import org.apache.xml.security.binding.xmldsig.xades.v132.QualifyingPropertiesType; +import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.extension.SignatureProcessor; +import org.apache.xml.security.extension.exceptions.ExtensionException; +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 javax.xml.namespace.QName; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.jcp.xml.dsig.internal.dom.DOMUtils.objectToXMLStructure; +import static org.apache.xml.security.extension.xades.XAdESConstants.*; +import static org.apache.xml.security.extension.xades.XAdESQualifyingPropertiesBuilder.create; + +/** + * This class is responsible for pre-processing XAdES signature. + * It adds XAdES QualifyingProperties to the signature to be signed. + */ +public 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-"; + + // XAdES data + private X509Certificate signatureCertificate; + private String certificateDigestMethodURI; + private String signaturePolicy; + private String signatureCity; + private String signatureCountryName; + // List of transformations to be done before digesting + List referenceTransformAlgorithms = new ArrayList<>(); + + public XAdESSignatureProcessor(X509Certificate x509) { + this(x509, XMLCipher.SHA256, null, null, null); + } + + public XAdESSignatureProcessor(X509Certificate x509, String certificateDigestMethodURI, String signaturePolicy, String signatureCity, String signatureCountryName) { + this.signatureCertificate = x509; + this.certificateDigestMethodURI = certificateDigestMethodURI; + this.signaturePolicy = signaturePolicy; + this.signatureCity = signatureCity; + this.signatureCountryName = signatureCountryName; + } + + public void processSignature(XMLSignature signature) throws XMLSignatureException { + + if (isEmptyString(signature.getId())) { + signature.setId(IDGenerator.generateID(ID_PREFIX_SIG)); + } + // set signature value id to be ready for XAdES-T extension + if (isEmptyString(signature.getSignatureValueId())) { + signature.setSignatureValueId(IDGenerator.generateID(ID_PREFIX_SIG_VAL)); + } + + String strSigId = signature.getId(); + String strXAdESSigPropId = IDGenerator.generateID(ID_PREFIX_SIG_PROP); + Document doc = signature.getElement().getOwnerDocument(); + // set xades data + QualifyingPropertiesType qualifyingProperties; + + try { + // create XAdES QualifyingProperties + qualifyingProperties = create() + .withSignatureId(strSigId) + .withXAdESSignaturePropertiesId(strXAdESSigPropId) + .withSigningCertificate(signatureCertificate) + .withCertificateDigestMethodURI(certificateDigestMethodURI) + .withSignaturePolicy(signaturePolicy) + .withSignatureCity(signatureCity) + .withSignatureCountryName(signatureCountryName) + .build(); + } catch (ExtensionException e) { + throw new XMLSignatureException(e); + } + + // add XAdES QualifyingProperties to the signature + ObjectContainer objectContainer = new ObjectContainer(doc); + try { + objectContainer.appendChild(objectToXMLStructure(objectContainer.getElement(), + qualifyingProperties, + new QName(XADES_V132_NS, _TAG_QUALIFYINGPROPERTIES, XADES_V132_PREFIX))); + } catch (JAXBException e) { + throw new XMLSignatureException(e); + } + + signature.appendObject(objectContainer); + // add reference to the signed properties + Transforms transforms = getReferenceTransform(doc); + signature.addDocument("#" + strXAdESSigPropId, transforms, XMLCipher.SHA256, + null, REFERENCE_TYPE_SIGNEDPROPERTIES); + } + + private static boolean isEmptyString(String str) { + return str == null || str.isEmpty(); + } + + /** + * This method returns the reference transform algorithm. + * Optional list of transformations to be done before digesting + * + * @return the reference transform algorithm + */ + public List getReferenceTransformsAlgorithm() { + return referenceTransformAlgorithms; + } + + public void addReferenceTransformAlgorithm(String referenceTransformAlgorithm) { + this.referenceTransformAlgorithms.add(referenceTransformAlgorithm); + } + + public void removeReferenceTransformAlgorithm(String referenceTransformAlgorithm) { + this.referenceTransformAlgorithms.remove(referenceTransformAlgorithm); + } + + /** + * This method returns the Transforms object with the reference transform + * algorithm set. + * + * @param doc the document in which the Transforms object is created + * @return the Transforms object with the transform algorithm set or + * null if no transform algorithm is not set + * @throws XMLSignatureException if an error occurs during the creation of the Transforms object + */ + private Transforms getReferenceTransform(Document doc) throws XMLSignatureException { + if (referenceTransformAlgorithms.isEmpty()) { + return null; + } + Transforms transforms = new Transforms(doc); + try { + for (String referenceTransformAlgorithm : referenceTransformAlgorithms) { + transforms.addTransform(referenceTransformAlgorithm); + } + } catch (TransformationException e) { + throw new XMLSignatureException(e); + } + return transforms; + } +} 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 6736ad0af..41d0772d6 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,15 @@ import java.security.PublicKey; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.List; 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; @@ -213,6 +216,13 @@ public final class XMLSignature extends SignatureElementProxy { private static final Logger LOG = System.getLogger(XMLSignature.class.getName()); + // list of SignatureProcessor which are invoked before the signature is generated + // as example signature metadata generation such as XAdES + List preProcessors = new ArrayList<>(); + // list of SignatureProcessor which are invoked after the signature is generated + // as example timestamping + List postProcessors = new ArrayList<>(); + /** ds:Signature.ds:SignedInfo element */ private final SignedInfo signedInfo; @@ -614,6 +624,27 @@ public XMLSignature(Element element, String baseURI, boolean secureValidation, P this.state = MODE_VERIFY; } + /** + * Add signature processor which is invoked before the signature is generated. Purpose of the + * pre-processor is to validate and make everything ready before signature is executed as + * to add additional data to the signature. + * + * @param processor the signature pre-processor + */ + public void addPreProcessor(SignatureProcessor processor) { + preProcessors.add(processor); + } + + /** + * Add signature processor which is invoked after the signature is generated. Example of post-processor + * is timestamping of the signature. + * + * @param processor the signature processor + */ + public void addPostProcessor(SignatureProcessor processor) { + postProcessors.add(processor); + } + /** * Sets the Id attribute * @@ -622,6 +653,9 @@ public XMLSignature(Element element, String baseURI, boolean secureValidation, P public void setId(String id) { if (id != null) { setLocalIdAttribute(Constants._ATT_ID, id); + getElement().setIdAttributeNS(null, Constants._ATT_ID, true); + } else { + getElement().removeAttributeNS(null, Constants._ATT_ID); } } @@ -631,7 +665,41 @@ public void setId(String id) { * @return the Id attribute */ public String getId() { - return getLocalAttribute(Constants._ATT_ID); + return getElement().hasAttribute(Constants._ATT_ID)? + getLocalAttribute(Constants._ATT_ID):null; + } + + /** + * Sets the Id attribute to the SignatureValue Element. If the SignatureValue does not exist + * the attribute is not set. + */ + public void setSignatureValueId(String id) { + + if (signatureValueElement == null) { + // should not happen since the SignatureValue element is created in the constructor + LOG.log(Level.WARNING, "SignatureValue element is not available, cannot set id attribute [{}]", id); + return; + } + if (id == null) { + signatureValueElement.removeAttributeNS(null, Constants._ATT_ID); + return; + } + // set the attribute + signatureValueElement.setAttributeNS(null, Constants._ATT_ID, id); + signatureValueElement.setIdAttributeNS(null, Constants._ATT_ID, true); + } + /** + * Returns the Id attribute from the SignatureValue Element. If the SignatureValue does not exist + * or does not have an Id attribute, null is returned. + * + * @return the Id attribute from the SignatureValue Element + */ + public String getSignatureValueId() { + if (signatureValueElement == null) { + return null; + } + Attr signatureValueAttr = signatureValueElement.getAttributeNodeNS(null, Constants._ATT_ID); + return signatureValueAttr == null ? null : signatureValueAttr.getValue(); } /** @@ -781,6 +849,9 @@ public void sign(Key signingKey) throws XMLSignatureException { ); } + for (SignatureProcessor preProcessor : preProcessors) { + preProcessor.processSignature(this); + } //Create a SignatureAlgorithm object SignedInfo si = this.getSignedInfo(); SignatureAlgorithm sa = si.getSignatureAlgorithm(); @@ -803,6 +874,9 @@ public void sign(Key signingKey) throws XMLSignatureException { } catch (XMLSecurityException | IOException ex) { throw new XMLSignatureException(ex); } + for (SignatureProcessor p : postProcessors) { + p.processSignature(this); + } } /** diff --git a/src/main/java/org/apache/xml/security/utils/jaxb/DatatypeConverter.java b/src/main/java/org/apache/xml/security/utils/jaxb/DatatypeConverter.java new file mode 100644 index 000000000..ad63a9eb9 --- /dev/null +++ b/src/main/java/org/apache/xml/security/utils/jaxb/DatatypeConverter.java @@ -0,0 +1,112 @@ +/** + * 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.jaxb; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static java.time.format.DateTimeFormatter.ISO_DATE; +import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; + +/** + * Utility class for converting date and time values to and from string. The utility is used by JAXB adapters. + */ +public class DatatypeConverter { + @FunctionalInterface + private interface ConvertToOffsetDateTime { + OffsetDateTime method(String string); + } + + private static final System.Logger LOG = System.getLogger(DatatypeConverter.class.getName()); + + private static final List PARSER_FORMATS = Arrays.asList( + value -> OffsetDateTime.parse(value, ISO_DATE_TIME), + value -> { + LocalDateTime ldt = LocalDateTime.parse(value, ISO_DATE_TIME); + return ldt.atZone(ZoneId.systemDefault()).toOffsetDateTime(); + }, + value -> OffsetDateTime.parse(value, ISO_DATE), + value -> { + LocalDate ldt = LocalDate.parse(value, ISO_DATE); + return ldt.atStartOfDay(ZoneId.systemDefault()).toOffsetDateTime(); + }); + + protected DatatypeConverter() { + } + + public static OffsetDateTime parseDateTime(String dateTimeValue) { + String trimmedValue = trimToNull(dateTimeValue); + if (trimmedValue == null) { + return null; + } + + OffsetDateTime dateTime = PARSER_FORMATS.stream() + .map(parser -> parseDateTime(trimmedValue, parser)) + .filter(Objects::nonNull) + .findFirst().orElse(null); + + if (dateTime == null) { + LOG.log(System.Logger.Level.WARNING, "Can not parse date value [{}]. Value ingored!", + trimmedValue); + } + return dateTime; + } + + private static OffsetDateTime parseDateTime(String value, ConvertToOffsetDateTime parser) { + // first try to pase offset + try { + return parser.method(value); + } catch (DateTimeParseException ex) { + LOG.log(System.Logger.Level.WARNING, "Can not parse date [{}], Error: [{}]!", + value, ex.getMessage()); + } + return null; + } + + public static String printDateTime(OffsetDateTime value) { + return value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + public static String printDate(OffsetDateTime value) { + return value.format(DateTimeFormatter.ISO_OFFSET_DATE); + } + + + /** + * Returns a none empty string whose value is this string, with all leading + * and trailing space removed, otherwise returns null. + * + * @param value the string to be trimmed + * @return the trimmed (not empty) string or null + */ + private static String trimToNull(String value) { + if (value == null) { + return null; + } + String result = value.trim(); + return result.isEmpty() ? null : result; + } +} diff --git a/src/main/java/org/apache/xml/security/utils/jaxb/OffsetDateAdapter.java b/src/main/java/org/apache/xml/security/utils/jaxb/OffsetDateAdapter.java new file mode 100644 index 000000000..83d349046 --- /dev/null +++ b/src/main/java/org/apache/xml/security/utils/jaxb/OffsetDateAdapter.java @@ -0,0 +1,39 @@ +/** + * 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.jaxb; + +import jakarta.xml.bind.annotation.adapters.XmlAdapter; + +import java.time.OffsetDateTime; + +/** + * Purpose of the class it to provide OffsetDateTime to string and string + * to OffsetDateTime conversion + */ +public class OffsetDateAdapter + extends XmlAdapter +{ + public OffsetDateTime unmarshal(String value) { + return (DatatypeConverter.parseDateTime(value)); + } + + public String marshal(OffsetDateTime value) { + return (DatatypeConverter.printDate(value)); + } +} diff --git a/src/main/java/org/apache/xml/security/utils/jaxb/OffsetDateTimeAdapter.java b/src/main/java/org/apache/xml/security/utils/jaxb/OffsetDateTimeAdapter.java new file mode 100644 index 000000000..e17eafbce --- /dev/null +++ b/src/main/java/org/apache/xml/security/utils/jaxb/OffsetDateTimeAdapter.java @@ -0,0 +1,39 @@ +/** + * 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.jaxb; + +import jakarta.xml.bind.annotation.adapters.XmlAdapter; + +import java.time.OffsetDateTime; + +/** + * Purpose of the class it to provide OffsetDateTime to string and string to OffsetDateTime conversion + * + */ +public class OffsetDateTimeAdapter + extends XmlAdapter +{ + public OffsetDateTime unmarshal(String value) { + return (DatatypeConverter.parseDateTime(value)); + } + + public String marshal(OffsetDateTime value) { + return (DatatypeConverter.printDateTime(value)); + } +} 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..975201f04 --- /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..5e393c82d --- /dev/null +++ b/src/main/resources/bindings/schemas/XAdES01903v141-202107.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/bindings/xades.xjb b/src/main/resources/bindings/xades.xjb new file mode 100644 index 000000000..6e70756a5 --- /dev/null +++ b/src/main/resources/bindings/xades.xjb @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/xml/security/test/dom/signature/XAdESSignatureTest.java b/src/test/java/org/apache/xml/security/test/dom/signature/XAdESSignatureTest.java new file mode 100644 index 000000000..e373617e6 --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/dom/signature/XAdESSignatureTest.java @@ -0,0 +1,196 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.dom.signature; + +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.extension.xades.XAdESSignatureProcessor; +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.testutils.JDKTestUtils; +import org.apache.xml.security.transforms.Transforms; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.w3c.dom.Element; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.cert.X509Certificate; + +import static org.apache.jcp.xml.dsig.internal.dom.DOMUtils.setIdFlagToIdAttributes; + + +class XAdESSignatureTest { + + static { + if (!org.apache.xml.security.Init.isInitialized()) { + org.apache.xml.security.Init.init(); + } + } + + private static final String ECDSA_KS = + "src/test/resources/org/apache/xml/security/samples/input/keystore-chain.p12"; + private static final String ECDSA_KS_PASSWORD = "security"; + public static final String ECDSA_KS_TYPE = "PKCS12"; + + + @BeforeAll + public static void beforeAll() { + Security.insertProviderAt + (new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI(), 1); + // Since JDK 15, the ECDSA algorithms are supported in the default java JCA provider. + // Add BouncyCastleProvider only for java versions before JDK 15. + boolean isNotJDK15up; + try { + int javaVersion = Integer.getInteger("java.specification.version", 0); + isNotJDK15up = javaVersion < 15; + } catch (NumberFormatException ex) { + isNotJDK15up = true; + } + + if (isNotJDK15up && Security.getProvider("BC") == null) { + // Use reflection to add new BouncyCastleProvider + try { + Class bouncyCastleProviderClass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider bouncyCastleProvider = (Provider) bouncyCastleProviderClass.getConstructor().newInstance(); + Security.addProvider(bouncyCastleProvider); + } catch (ReflectiveOperationException e) { + // BouncyCastle not installed, ignore + System.out.println("BouncyCastle not installed!"); + } + } + } + + @ParameterizedTest + @CsvSource({"rsa2048, http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "ed25519, http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519", + "ed448, http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448", + "secp256r1, http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", + "secp384r1, http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", + "secp521r1, http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"}) + void testXAdESSignatuire(String alias,String signatureAlgorithm) throws Exception { + String jceAlgorithm = JCEMapper.translateURItoJCEID(signatureAlgorithm); + Assumptions.assumeTrue(JDKTestUtils.isAlgorithmSupportedByJDK(jceAlgorithm)); + + KeyStore keyStore = KeyStore.getInstance(ECDSA_KS_TYPE); + keyStore.load(Files.newInputStream(Paths.get(ECDSA_KS)), ECDSA_KS_PASSWORD.toCharArray()); + + PrivateKey privateKey = + (PrivateKey) keyStore.getKey(alias, ECDSA_KS_PASSWORD.toCharArray()); + + doVerify(doSign(privateKey, (X509Certificate) keyStore.getCertificate(alias), + null, signatureAlgorithm, alias)); + } + + + private byte[] doSign(PrivateKey privateKey, X509Certificate x509, PublicKey publicKey, + String sigAlgURI, String alias) throws Exception { + + // generate test document for signing element + org.w3c.dom.Document doc = TestUtils.newDocument(); + doc.appendChild(doc.createComment(" Comment before ")); + Element root = doc.createElementNS("", "RootElement"); + doc.appendChild(root); + root.appendChild(doc.createTextNode("Some simple text\n")); + + Element canonElem = + XMLUtils.createElementInSignatureSpace(doc, Constants._TAG_CANONICALIZATIONMETHOD); + canonElem.setAttributeNS( + null, Constants._ATT_ALGORITHM, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS + ); + + SignatureAlgorithm signatureAlgorithm = + new SignatureAlgorithm(doc, sigAlgURI); + XMLSignature sig = + new XMLSignature(doc, null, signatureAlgorithm.getElement(), canonElem); + root.appendChild(sig.getElement()); + doc.appendChild(doc.createComment(" Comment after ")); + + + Transforms transforms = new Transforms(doc); + transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + transforms.addTransform(Transforms.TRANSFORM_C14N_WITH_COMMENTS); + sig.addDocument("", transforms, XMLCipher.SHA256); + + if (x509 != null) { + sig.addKeyInfo(x509); + } else { + sig.addKeyInfo(publicKey); + } + // create XAdES processor + XAdESSignatureProcessor xadesProcessor = new XAdESSignatureProcessor(x509); + xadesProcessor.addReferenceTransformAlgorithm(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + sig.addPreProcessor(xadesProcessor); + + sig.sign(privateKey); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + //XMLUtils.outputDOMc14nWithComments(doc, bos); + XMLUtils.outputDOM(doc.getDocumentElement(), bos); + + Files.write(Paths.get("target/XAdES-" + alias + ".xml"), bos.toByteArray()); + return bos.toByteArray(); + } + + private void doVerify(byte[] signedXml) throws Exception { + try (InputStream is = new ByteArrayInputStream(signedXml)) { + doVerify(is); + } + } + + private void doVerify(InputStream is) throws Exception { + org.w3c.dom.Document doc = XMLUtils.read(is, false); + setIdFlagToIdAttributes(doc.getDocumentElement()); + + XPathFactory xpf = XPathFactory.newInstance(); + XPath xpath = xpf.newXPath(); + xpath.setNamespaceContext(new DSNamespaceContext()); + + String expression = "//ds:Signature[1]"; + Element sigElement = + (Element) xpath.evaluate(expression, doc, XPathConstants.NODE); + XMLSignature signature = new XMLSignature(sigElement, ""); + + signature.addResourceResolver(new XPointerResourceResolver(sigElement)); + + KeyInfo ki = signature.getKeyInfo(); + if (ki == null) { + throw new RuntimeException("No keyinfo"); + } + X509Certificate cert = signature.getKeyInfo().getX509Certificate(); + if (cert != null) { + Assertions.assertTrue(signature.checkSignatureValue(cert)); + } else { + Assertions.assertTrue(signature.checkSignatureValue(signature.getKeyInfo().getPublicKey())); + } + } +} diff --git a/src/test/resources/org/apache/xml/security/samples/input/keystore-chain.p12 b/src/test/resources/org/apache/xml/security/samples/input/keystore-chain.p12 new file mode 100644 index 000000000..4cc24ee5e Binary files /dev/null and b/src/test/resources/org/apache/xml/security/samples/input/keystore-chain.p12 differ