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

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

There is a newer version: 2.2.0
Show 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.commons.lang3.StringUtils;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.keys.content.X509Data;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.transforms.params.XPathContainer;
import org.apache.xml.security.utils.Constants;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import se.idsec.signservice.security.sign.xml.XMLSignatureLocation;
import se.idsec.signservice.security.sign.xml.XMLSigner;
import se.idsec.signservice.security.sign.xml.XMLSignerResult;
import se.swedenconnect.security.algorithms.AlgorithmPredicates;
import se.swedenconnect.security.algorithms.AlgorithmRegistry;
import se.swedenconnect.security.algorithms.AlgorithmRegistrySingleton;
import se.swedenconnect.security.algorithms.SignatureAlgorithm;
import se.swedenconnect.security.credential.PkiCredential;

import javax.xml.xpath.XPathExpressionException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Objects;
import java.util.Optional;

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

* If the signature algorithm is not explicitly set the OpenSAML {@code SignatureSigningConfiguration} system * configuration will be used to obtain a default. *

* * @author Martin Lindström ([email protected]) * @author Stefan Santesson ([email protected]) */ @Slf4j public class DefaultXMLSigner implements XMLSigner { /** The default canonicalization method - required Exclusive Canonicalization (omits comments). */ public static final String DEFAULT_CANONICALIZATION_TRANSFORM = Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS; /** The default XPath transform (don't include Signature elements). */ public static final String DEFAULT_XPATH_TRANSFORM = "not(ancestor-or-self::*[local-name()='Signature' and namespace-uri()='http://www.w3.org/2000/09/xmldsig#'])"; /** The signing credential. */ private final PkiCredential signingCredential; /** * An indicator that tells where in the document the resulting Signature element should be inserted. If not set, the * default "insert as the last child of the document root element" will be used. */ private XMLSignatureLocation signatureLocation = new XMLSignatureLocation(); /** The digest algorithm. */ private String digestAlgorithm; /** The signature algorithm. */ private String signatureAlgorithm; /** The URI for that canonicalization method. Default is {@value #DEFAULT_CANONICALIZATION_TRANSFORM}. */ private String canonicalizationTransform = DEFAULT_CANONICALIZATION_TRANSFORM; /** * If set, includes the given XPath expression in an XPath transform. The default is * {@value #DEFAULT_XPATH_TRANSFORM}. */ private String xPathTransform = DEFAULT_XPATH_TRANSFORM; /** * Should the certificate chain/path be included in the signature (if available from * {@link PkiCredential#getCertificateChain()}). The default is {@code false} (only the entity certificate is * included). */ private boolean includeCertificateChain = false; /** * Should an ID attribute be written to the resulting ds:Signature element. Default is {@code false}. */ private boolean includeSignatureId = false; /** The algorithm registry. */ private AlgorithmRegistry algorithmRegistry; /** For generating ID:s. */ private static final SecureRandom random = new SecureRandom(String.valueOf(System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8)); /** * Constructor. * * @param signingCredential the signing credential to use */ public DefaultXMLSigner(final PkiCredential signingCredential) { this.signingCredential = Objects.requireNonNull(signingCredential, "signingCredential must not be null"); } /** * Creates a builder for {@code DefaultXMLSigner} objects. * * @param signingCredential the signing credential to use * @return a builder instance */ public static DefaultXMLSignerBuilder builder(final PkiCredential signingCredential) { return new DefaultXMLSignerBuilder(signingCredential); } /** {@inheritDoc} */ @Override public XMLSignerResult sign(final Document document) throws SignatureException { try { // Create Signature ... // final XMLSignature signature = new XMLSignature(document, "", this.getSignatureAlgorithm(), this.canonicalizationTransform); // Insert the Signature element into the document. // this.signatureLocation.insertSignature(signature.getElement(), document); // Setup transforms // final Transforms transforms = new Transforms(document); transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); transforms.addTransform(this.canonicalizationTransform); if (StringUtils.isNotEmpty(this.xPathTransform)) { final XPathContainer xpath = new XPathContainer(document); xpath.setXPathNamespaceContext(XMLSignature.getDefaultPrefix(Constants.SignatureSpecNS), Constants.SignatureSpecNS); xpath.setXPath(this.xPathTransform); transforms.addTransform(Transforms.TRANSFORM_XPATH, xpath.getElementPlusReturns()); } // Get the ID reference. // final String signatureUriReference = registerIdAttributes(document); // Add the document to sign to the signature // signature.addDocument(signatureUriReference, transforms, this.getDigestAlgorithm()); // Add signature ID. // if (this.includeSignatureId) { signature.setId("id-" + (new BigInteger(128, random)).toString(16)); } // Set KeyInfo // if (this.includeCertificateChain && this.signingCredential.getCertificateChain().size() > 1) { final X509Data x509data = new X509Data(document); for (final X509Certificate c : this.signingCredential.getCertificateChain()) { x509data.addCertificate(c); } signature.getKeyInfo().add(x509data); } else if (this.signingCredential.getCertificate() != null) { signature.addKeyInfo(this.signingCredential.getCertificate()); } else if (this.signingCredential.getPublicKey() != null) { signature.addKeyInfo(this.signingCredential.getPublicKey()); } // Finally, sign the document. // signature.sign(this.signingCredential.getPrivateKey()); final DefaultXMLSignerResult result = new DefaultXMLSignerResult(signature); result.setSignerCertificate(this.signingCredential.getCertificate()); if (this.includeCertificateChain) { result.setSignerCertificateChain(this.signingCredential.getCertificateChain()); } return result; } catch (final XMLSecurityException e) { final String msg = String.format("Error while creating Signature - %s", e.getMessage()); log.error("{}", msg, e); throw new SignatureException(msg, e); } catch (final XPathExpressionException e) { final String msg = String.format("Failed to find location where to insert Signature - %s", e.getMessage()); log.error("{}", msg, e); throw new SignatureException(msg, e); } } /** {@inheritDoc} */ @Override public PkiCredential getSigningCredential() { return this.signingCredential; } /** * Sets the indicator that tells where in the document the resulting Signature element should be inserted. If not set, * the default "insert as the last child of the document root element" will be used. * * @param signatureLocation location indicator */ public void setSignatureLocation(final XMLSignatureLocation signatureLocation) { if (signatureLocation != null) { this.signatureLocation = signatureLocation; } } /** * Assigns the URI for the signature algorithm to be used. * * @param signatureAlgorithm the signature algorithm URI * @throws NoSuchAlgorithmException if the algorithm is not supported (or blacklisted) * @throws SignatureException if the signature algorithm can not be used by the current signature credential */ public void setSignatureAlgorithm(final String signatureAlgorithm) throws NoSuchAlgorithmException, SignatureException { // Assert that the signature algorithm is supported. // final SignatureAlgorithm algorithm = this.getAlgorithmRegistry().getAlgorithm(signatureAlgorithm, SignatureAlgorithm.class); if (algorithm == null) { final String msg = String.format("Algorithm '%s' is not supported as a signing algorithm", signatureAlgorithm); log.error("{}", msg); throw new NoSuchAlgorithmException(msg); } if (algorithm.getUri().equals(XMLSignature.ALGO_ID_SIGNATURE_RSA_PSS)) { final String msg = String.format("Incomplete algorithm '%s' - missing parameters", signatureAlgorithm); log.error("{}", msg); throw new NoSuchAlgorithmException(msg); } // Make sure that it isn't blacklisted. // if (algorithm.isBlacklisted()) { final String msg = String.format("Signature algorithm '%s' is black listed according to the system configuration", signatureAlgorithm); log.error("{}", msg); throw new NoSuchAlgorithmException(msg); } // Check that the supplied credential is consistent with the supplied algorithm's specified key algorithm. // if (!algorithm.getKeyType().equals(this.signingCredential.getPrivateKey().getAlgorithm())) { final String msg = String.format( "Signature algorithm '%s' can not be used together with configured signing credential", signatureAlgorithm); log.error("{}", msg); throw new SignatureException(msg); } this.signatureAlgorithm = signatureAlgorithm; this.digestAlgorithm = algorithm.getMessageDigestAlgorithm().getUri(); } /** * Gets the signature algorithm to use. *

* If the digest algorithm is not explicitly set, the default signature algorithm given by the * {@link AlgorithmRegistry} will be used. *

* * @return the signature algorithm URI */ public String getSignatureAlgorithm() { if (this.signatureAlgorithm != null) { return this.signatureAlgorithm; } final SignatureAlgorithm alg = this.getAlgorithmRegistry().getAlgorithm( AlgorithmPredicates.fromKeyType(this.signingCredential.getPrivateKey().getAlgorithm()), SignatureAlgorithm.class); if (alg != null) { this.signatureAlgorithm = alg.getUri(); this.digestAlgorithm = alg.getMessageDigestAlgorithm().getUri(); log.info("Using signature algorithm '{}' as the default", this.signatureAlgorithm); return this.signatureAlgorithm; } else { // Should never happen throw new SecurityException("No default signature algorithm found"); } } /** * Gets the digest algorithm to use. * * @return the digest algorithm URI */ public String getDigestAlgorithm() { if (this.digestAlgorithm != null) { return this.digestAlgorithm; } // Will set the digest algorithm as well ... this.getSignatureAlgorithm(); return this.digestAlgorithm; } /** * Assigns the canonicalization method to use. Default is {@value #DEFAULT_CANONICALIZATION_TRANSFORM}. * * @param canonicalizationTransform canonicalization method URI */ public void setCanonicalizationTransform(final String canonicalizationTransform) { if (StringUtils.isNotEmpty(canonicalizationTransform)) { this.canonicalizationTransform = canonicalizationTransform; } } /** * Sets the XPath expression to be used in an XPath transform. The default is {@value #DEFAULT_XPATH_TRANSFORM}. If * {@code null}, no XPath transform is used. * * @param xPathTransform XPath expression */ public void setXPathTransform(final String xPathTransform) { this.xPathTransform = xPathTransform; } /** * Sets whether the certificate chain/path be included in the signature (if available from * {@link PkiCredential#getCertificateChain()}). The default is {@code false} (only the entity certificate is * included). * * @param includeCertificateChain whether the certificate chain should be included */ public void setIncludeCertificateChain(final boolean includeCertificateChain) { this.includeCertificateChain = includeCertificateChain; } /** * Should an ID attribute be written to the resulting ds:Signature element. Default is {@code true}. * * @param includeSignatureId whether an ID attribute should be written to the Signature element */ public void setIncludeSignatureId(final boolean includeSignatureId) { this.includeSignatureId = includeSignatureId; } /** * Assigns the {@link AlgorithmRegistry} to use. If not assigned, the registry configured for * {@link AlgorithmRegistrySingleton} will be used. * * @param algorithmRegistry the registry to use */ public void setAlgorithmRegistry(final AlgorithmRegistry algorithmRegistry) { this.algorithmRegistry = algorithmRegistry; } /** * Gets the {@link AlgorithmRegistry} to use. * * @return the AlgorithmRegistry */ private AlgorithmRegistry getAlgorithmRegistry() { if (this.algorithmRegistry == null) { this.algorithmRegistry = AlgorithmRegistrySingleton.getInstance(); } return this.algorithmRegistry; } /** * Looks for an ID reference in the root element, and if found, registers it using the * {@link Element#setIdAttribute(String, boolean)} method. * * @param document the document * @return the signature URI reference ("" if no ID is found) */ public static String registerIdAttributes(final Document document) { final Element rootElement = document.getDocumentElement(); String signatureUriReference = Optional.ofNullable(rootElement.getAttributeNodeNS(null, "ID")) .map(Attr::getValue) .orElse(null); if (StringUtils.isNotEmpty(signatureUriReference)) { rootElement.setIdAttribute("ID", true); } else { signatureUriReference = Optional.ofNullable(rootElement.getAttributeNodeNS(null, "Id")) .map(Attr::getValue) .orElse(null); if (StringUtils.isNotEmpty(signatureUriReference)) { rootElement.setIdAttribute("Id", true); } } return StringUtils.isEmpty(signatureUriReference) ? "" : (signatureUriReference.trim().startsWith("#") ? signatureUriReference.trim() : "#" + signatureUriReference.trim()); } /** * Builder for {@link DefaultXMLSigner} objects. */ public static class DefaultXMLSignerBuilder { /** The object being built. */ private final DefaultXMLSigner signer; /** * Constructor. * * @param signingCredential the signing credential to use */ public DefaultXMLSignerBuilder(final PkiCredential signingCredential) { this.signer = new DefaultXMLSigner(signingCredential); } /** * Builds the signer object. * * @return the DefaultXMLSigner object */ public DefaultXMLSigner build() { return this.signer; } /** * See {@link DefaultXMLSigner#setSignatureLocation(XMLSignatureLocation)}. * * @param signatureLocation location indicator * @return the builder */ public DefaultXMLSignerBuilder signatureLocation(final XMLSignatureLocation signatureLocation) { this.signer.setSignatureLocation(signatureLocation); return this; } /** * See {@link DefaultXMLSigner#setSignatureAlgorithm(String)}. * * @param signatureAlgorithm the signature algorithm URI * @return the builder * @throws NoSuchAlgorithmException if the algorithm is not supported (or blacklisted) * @throws SignatureException if the signature algorithm can not be used by the current signature credential */ public DefaultXMLSignerBuilder signatureAlgorithm(final String signatureAlgorithm) throws NoSuchAlgorithmException, SignatureException { this.signer.setSignatureAlgorithm(signatureAlgorithm); return this; } /** * See {@link DefaultXMLSigner#setCanonicalizationTransform(String)}. * * @param canonicalizationTransform canonicalization method URI * @return the builder */ public DefaultXMLSignerBuilder canonicalizationTransform(final String canonicalizationTransform) { this.signer.setCanonicalizationTransform(canonicalizationTransform); return this; } /** * See {@link DefaultXMLSigner#setXPathTransform(String)}. * * @param xPathTransform XPath expression * @return the builder */ public DefaultXMLSignerBuilder xPathTransform(final String xPathTransform) { this.signer.setXPathTransform(xPathTransform); return this; } /** * See {@link DefaultXMLSigner#setIncludeCertificateChain(boolean)}. * * @param includeCertificateChain whether the certificate chain should be included * @return the builder */ public DefaultXMLSignerBuilder includeCertificateChain(final boolean includeCertificateChain) { this.signer.setIncludeCertificateChain(includeCertificateChain); return this; } /** * See {@link DefaultXMLSigner#setIncludeSignatureId(boolean)}. * * @param includeSignatureId whether an ID attribute should be written to the Signature element * @return the builder */ public DefaultXMLSignerBuilder includeSignatureId(final boolean includeSignatureId) { this.signer.setIncludeSignatureId(includeSignatureId); return this; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy