All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.idsec.signservice.security.sign.xml.impl.DefaultXMLSignatureValidator Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019-2024 IDsec Solutions AB
 *
 * Licensed 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 se.idsec.signservice.security.sign.xml.impl;

import lombok.extern.slf4j.Slf4j;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.keys.KeyInfo;
import org.apache.xml.security.keys.content.X509Data;
import org.apache.xml.security.keys.content.x509.XMLX509Certificate;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.signature.XMLSignatureException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import se.idsec.signservice.security.certificate.CertificateUtils;
import se.idsec.signservice.security.certificate.CertificateValidationResult;
import se.idsec.signservice.security.certificate.CertificateValidator;
import se.idsec.signservice.security.sign.SignatureValidationResult;
import se.idsec.signservice.security.sign.SignatureValidationResult.Status;
import se.idsec.signservice.security.sign.xml.XMLSignatureLocation;
import se.idsec.signservice.security.sign.xml.XMLSignatureValidator;

import javax.xml.xpath.XPathExpressionException;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Default implementation of the {@link XMLSignatureValidator} interface.
 * 

* Note that this implementation only supports validation of signatures that covers the supplied document. *

* * @author Martin Lindström ([email protected]) * @author Stefan Santesson ([email protected]) */ @Slf4j public class DefaultXMLSignatureValidator implements XMLSignatureValidator { /** XAdES namespace URI. */ private static final String XADES_NAMESPACE = "http://uri.etsi.org/01903/v1.3.2#"; /** A (possibly empty) list of required signer certificates. */ private final List requiredSignerCertificates; /** Optional certificate validator. */ private final CertificateValidator certificateValidator; /** Flag that tells if the validator should handle XAdES signatures. */ protected boolean xadesProcessing = true; /** * Constructor setting up the validator so that no required certificates are configured and no certificate path * validation is performed. This means that no control of the signer certificate will be performed. */ public DefaultXMLSignatureValidator() { this.requiredSignerCertificates = Collections.emptyList(); this.certificateValidator = null; } /** * Constructor setting up the validator to require that the signature is signed using the supplied certificate. * * @param acceptedSignerCertificate required signer certificate */ public DefaultXMLSignatureValidator(final X509Certificate acceptedSignerCertificate) { this(Collections.singletonList(acceptedSignerCertificate)); } /** * Constructor setting up the validator to require that the signature is signed using any of the supplied * certificates. * * @param acceptedSignerCertificates required signer certificates */ public DefaultXMLSignatureValidator(final List acceptedSignerCertificates) { this.requiredSignerCertificates = acceptedSignerCertificates; this.certificateValidator = null; } /** * Constructor setting up the validator to perform a certificate validation of the signer certificate using the * supplied certificate validator instance. * * @param certificateValidator certificate validator instance */ public DefaultXMLSignatureValidator(final CertificateValidator certificateValidator) { this.requiredSignerCertificates = Collections.emptyList(); this.certificateValidator = certificateValidator; } /** {@inheritDoc} */ @Override public List validate(final Document document) throws SignatureException { // First locate all signature elements ... // final NodeList signatureElements = document.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "Signature"); if (signatureElements.getLength() == 0) { throw new SignatureException("Supplied document is not signed"); } final List signatures = new ArrayList<>(); for (int i = 0; i < signatureElements.getLength(); i++) { signatures.add((Element) signatureElements.item(i)); } return this.validate(document, signatures); } /** {@inheritDoc} */ @Override public List validate(final Document document, final XMLSignatureLocation signatureLocation) throws SignatureException { if (signatureLocation == null) { return this.validate(document); } try { final Element signature = signatureLocation.getSignature(document); if (signature == null) { throw new SignatureException("Could not find Signature element"); } return this.validate(document, Collections.singletonList(signature)); } catch (final XPathExpressionException e) { throw new SignatureException(e.getMessage(), e); } } /** * Validates the supplied signatures. * * @param document the document containing the signatures * @param signatures the signatures * @return a list of result objects */ protected List validate(final Document document, final List signatures) { // Get the document ID attribute (and register the ID attributes). // final String signatureUriReference = DefaultXMLSigner.registerIdAttributes(document); // Register ID nodes for XAdES ... // if (this.xadesProcessing) { this.registerXadesIdNodes(document); } // Verify all signatures ... // final List results = new ArrayList<>(); for (final Element signature : signatures) { final DefaultXMLSignatureValidationResult result = this.validateSignature(signature, signatureUriReference); // If we have a cert path validator installed, perform path validation... // if (result.isSuccess() && this.certificateValidator != null) { try { final CertificateValidationResult validatorResult = this.certificateValidator.validate( result.getSignerCertificate(), result.getAdditionalCertificates(), null); result.setCertificateValidationResult(validatorResult); } catch (final CertPathBuilderException e) { final String msg = String.format("Failed to build a path to a trusted root for signer certificate - %s", e.getMessage()); log.error("{}", e.getMessage(), e); result.setError(Status.ERROR_NOT_TRUSTED, msg, e); } catch (final GeneralSecurityException e) { final String msg = String.format("Certificate path validation failure for signer certificate - %s", e.getMessage()); log.error("{}", e.getMessage(), e); result.setError(Status.ERROR_SIGNER_INVALID, msg, e); } } results.add(result); } return results; } /** * Validates the signature value and checks that the signer certificate is accepted. * * @param signature the signature element * @param signatureUriReference the signature URI reference * @return a validation result */ protected DefaultXMLSignatureValidationResult validateSignature(final Element signature, final String signatureUriReference) { final DefaultXMLSignatureValidationResult result = new DefaultXMLSignatureValidationResult(); result.setSignatureElement(signature); try { // Parse the signature element. final XMLSignature xmlSignature = new XMLSignature(signature, ""); // Set the signature algorithm result.setSignatureAlgorithm(xmlSignature.getSignedInfo().getSignatureMethodURI()); // Make sure the signature covers the entire document. // final List uris = this.getSignedInfoReferenceURIs(xmlSignature.getSignedInfo().getElement()); if (!uris.contains(signatureUriReference) && !uris.contains("")) { final String msg = String.format("The Signature contained the reference(s) %s - none of these covers the entire document", uris); log.error(msg); result.setError(Status.ERROR_BAD_FORMAT, msg); return result; } // Locate the certificate that was used to sign ... // PublicKey validationKey = null; if (xmlSignature.getKeyInfo() != null) { final X509Certificate validationCertificate = xmlSignature.getKeyInfo().getX509Certificate(); if (validationCertificate != null) { result.setSignerCertificate(validationCertificate); // Get hold of any other certs (intermediate and roots) result.setAdditionalCertificates( this.getAdditionalCertificates(xmlSignature.getKeyInfo(), validationCertificate)); validationKey = validationCertificate.getPublicKey(); } else { log.info("No certificate found in signature's KeyInfo ..."); validationKey = xmlSignature.getKeyInfo().getPublicKey(); } } else { log.warn("No KeyInfo element found in Signature ..."); } // Check signature ... // if (validationKey == null) { // If we did not find a validation key (or cert) in the key info, we can try using any of the // supplied required signer certificates. But if no certs have been supplied we have to fail. // final String msg = "No certificate or public key found in signature's KeyInfo"; if (this.requiredSignerCertificates.isEmpty()) { log.info("{} - and no required signer certificates available", msg); result.setError(Status.ERROR_BAD_FORMAT, msg); return result; } // Otherwise, lets try to check the signature using the required signer certificates ... // log.debug("{} - using required signer certificates to check signature ...", msg); for (final X509Certificate rc : this.requiredSignerCertificates) { try { if (xmlSignature.checkSignatureValue(rc)) { log.debug("Certificate [{}] verified signature successfully", CertificateUtils.toLogString(rc)); result.setSignerCertificate(rc); result.setStatus(Status.SUCCESS); return result; } } catch (final XMLSignatureException e) { log.error("Certificate [{}] could not be used to validate signature value", CertificateUtils.toLogString(rc), e); } log.debug("Certificate [{}] could not be used to validate signature value", CertificateUtils.toLogString(rc)); } log.info("{} - And none of supplied required signer certificates verified signature", msg); result.setError(Status.ERROR_BAD_FORMAT, msg + " - And none of supplied required signer certificates verified signature"); return result; } else { // The KeyInfo contained cert/key. First verify signature bytes... // try { if (!xmlSignature.checkSignatureValue(validationKey)) { final String msg = "Signature is invalid - signature value did not validate correctly or reference digest comparison failed"; log.info("{}", msg); result.setError(Status.ERROR_INVALID_SIGNATURE, msg); return result; } } catch (final XMLSignatureException e) { final String msg = "Signature is invalid - " + e.getMessage(); log.info("{}", msg, e); result.setError(Status.ERROR_INVALID_SIGNATURE, msg, e); return result; } log.debug("Signature value was successfully validated"); // Next, make sure that the signer is one of the required ... // if (result.getSignerCertificate() == null) { // If the KeyInfo did not contain a signer certificate, but only a key, we check if // we can find a certificate among our required signer certificates that has a matching // key ... // if (this.requiredSignerCertificates.isEmpty()) { // We won't be able to find the signer certificate. If we have a certificate validator // installed, we may fail right now since it requires a subject certificate as input. // Otherwise, this validator is set up to not perform certificate checking ... // if (this.certificateValidator != null) { result.setError(Status.ERROR_SIGNER_INVALID, "Could not find a signer certificate"); return result; } log.info("No certificate checking performed - signature is regarded as valid"); result.setStatus(Status.SUCCESS); } for (final X509Certificate rc : this.requiredSignerCertificates) { if (rc.getPublicKey().equals(validationKey)) { log.debug("Required certificate [{}] matched key found in KeyInfo", CertificateUtils.toLogString(rc)); result.setStatus(Status.SUCCESS); result.setSignerCertificate(rc); return result; } } // If we get here none of the supplied required signer certificates had a public key // that matched the public key that verified the signature. We must fail ... // final String msg = "None of the supplied required certificates matched signing key"; log.info("Signature validation failed - {}", msg); result.setError(Status.ERROR_SIGNER_NOT_ACCEPTED, msg); return result; } else { // OK, this is the most common case. The KeyInfo contained and certificate, and // now we just want to make sure that this certificate is listed among the required // certificates. // if (this.requiredSignerCertificates.isEmpty()) { // If we don't have any required signer certificates, we return success for now // and possibly perform a path validation later on. // result.setStatus(Status.SUCCESS); return result; } else { // Find a matching certificate ... for (final X509Certificate rc : this.requiredSignerCertificates) { if (result.getSignerCertificate().equals(rc)) { log.debug("Required certificate [{}] matched certificate found in KeyInfo", CertificateUtils.toLogString(rc)); result.setStatus(Status.SUCCESS); return result; } } // None of the required signer certificate matched the certificate used to sign - fail. // final String msg = "None of the supplied required certificates matched signing certificate"; log.info("Signature validation failed - {}", msg); result.setError(Status.ERROR_SIGNER_NOT_ACCEPTED, msg); return result; } } } } catch (final XMLSecurityException | SignatureException e) { result.setError(Status.ERROR_BAD_FORMAT, e.getMessage(), e); return result; } } /** * Extracts all certificates from the supplied KeyInfo except for the actual signer certificate. * * @param keyInfo the KeyInfo * @param signerCertificate the signer certificate * @return a list of certificates */ protected List getAdditionalCertificates(final KeyInfo keyInfo, final X509Certificate signerCertificate) { final List additional = new ArrayList<>(); for (int i = 0; i < keyInfo.lengthX509Data(); i++) { try { final X509Data x509data = keyInfo.itemX509Data(i); if (x509data == null) { continue; } for (int j = 0; j < x509data.lengthCertificate(); j++) { final XMLX509Certificate xmlCert = x509data.itemCertificate(j); if (xmlCert != null) { final X509Certificate cert = CertificateUtils.decodeCertificate(xmlCert.getCertificateBytes()); if (!cert.equals(signerCertificate)) { additional.add(cert); } } } } catch (final XMLSecurityException | CertificateException e) { log.error("Failed to extract X509Certificate from KeyInfo", e); } } return additional; } /** {@inheritDoc} */ @Override public boolean isSigned(final Document document) throws IllegalArgumentException { try { final NodeList signatureElements = document.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "Signature"); return signatureElements.getLength() > 0; } catch (final Exception e) { throw new IllegalArgumentException("Invalid document", e); } } /** {@inheritDoc} */ @Override public List getRequiredSignerCertificates() { return this.requiredSignerCertificates; } /** {@inheritDoc} */ @Override public CertificateValidator getCertificateValidator() { return this.certificateValidator; } /** * Sets flag that tells whether this validator should handle XAdES processing. The default is {@code true} * * @param xadesProcessing whether to process XAdES */ public void setXadesProcessing(final boolean xadesProcessing) { this.xadesProcessing = xadesProcessing; } /** * Looks for any {@code xades:SignedProperties} elements and registers an ID attribute for the elements that are * found. * * @param document the document to manipulate */ protected void registerXadesIdNodes(final Document document) { final NodeList xadesSignedProperties = document.getElementsByTagNameNS(XADES_NAMESPACE, "SignedProperties"); for (int i = 0; i < xadesSignedProperties.getLength(); i++) { final Element sp = (Element) xadesSignedProperties.item(i); sp.setIdAttribute("Id", true); } } /** * Utility method for getting hold of the reference URI:s of a {@code SignedInfo} element. * * @param signedInfo the signed info element * @return a list of one or more reference URI:s * @throws SignatureException for unmarshalling errors */ private List getSignedInfoReferenceURIs(final Element signedInfo) throws SignatureException { final NodeList references = signedInfo.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "Reference"); if (references.getLength() == 0) { throw new SignatureException("No Reference element found in SignedInfo of signature"); } final List uris = new ArrayList<>(); for (int i = 0; i < references.getLength(); i++) { final Element reference = (Element) references.item(i); uris.add(reference.getAttribute("URI")); } return uris; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy