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

se.idsec.signservice.integration.process.impl.DefaultSignRequestProcessor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019-2023 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.integration.process.impl;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.annotation.PostConstruct;
import jakarta.xml.bind.JAXBException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import se.idsec.signservice.integration.SignRequestInput;
import se.idsec.signservice.integration.authentication.AuthnRequirements;
import se.idsec.signservice.integration.certificate.SigningCertificateRequirements;
import se.idsec.signservice.integration.config.IntegrationServiceConfiguration;
import se.idsec.signservice.integration.core.DocumentCache;
import se.idsec.signservice.integration.core.Extension;
import se.idsec.signservice.integration.core.error.ErrorCode;
import se.idsec.signservice.integration.core.error.InputValidationException;
import se.idsec.signservice.integration.core.error.SignServiceIntegrationException;
import se.idsec.signservice.integration.core.error.impl.InternalSignServiceIntegrationException;
import se.idsec.signservice.integration.core.impl.CorrelationID;
import se.idsec.signservice.integration.document.ProcessedTbsDocument;
import se.idsec.signservice.integration.document.TbsDocument;
import se.idsec.signservice.integration.document.TbsDocumentProcessor;
import se.idsec.signservice.integration.dss.DssUtils;
import se.idsec.signservice.integration.dss.SignRequestWrapper;
import se.idsec.signservice.integration.process.SignRequestProcessingResult;
import se.idsec.signservice.integration.process.SignRequestProcessor;
import se.idsec.signservice.integration.signmessage.SignMessageMimeType;
import se.idsec.signservice.integration.signmessage.SignMessageParameters;
import se.idsec.signservice.integration.signmessage.SignMessageProcessor;
import se.idsec.signservice.security.sign.xml.XMLSignatureLocation;
import se.idsec.signservice.security.sign.xml.XMLSignatureLocation.ChildPosition;
import se.idsec.signservice.security.sign.xml.XMLSignerResult;
import se.idsec.signservice.security.sign.xml.impl.DefaultXMLSigner;
import se.idsec.signservice.utils.AssertThat;
import se.idsec.signservice.utils.ProtocolVersion;
import se.idsec.signservice.xml.DOMUtils;
import se.swedenconnect.schemas.csig.dssext_1_1.SignRequestExtension;
import se.swedenconnect.schemas.csig.dssext_1_1.SignTaskData;
import se.swedenconnect.schemas.csig.dssext_1_1.SignTasks;
import se.swedenconnect.schemas.saml_2_0.assertion.AudienceRestriction;
import se.swedenconnect.schemas.saml_2_0.assertion.Conditions;
import se.swedenconnect.security.credential.PkiCredential;
import se.swedenconnect.xml.jaxb.JAXBMarshaller;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.xpath.XPathExpressionException;
import java.io.Serial;
import java.security.SignatureException;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;

/**
 * Default implementation of the {@link SignRequestProcessor} interface.
 *
 * @author Martin Lindström ([email protected])
 * @author Stefan Santesson ([email protected])
 */
@Slf4j
public class DefaultSignRequestProcessor implements SignRequestProcessor {

  /** Processors for different TBS documents. */
  private List> tbsDocumentProcessors;

  /** Validator. */
  private final SignRequestInputValidator signRequestInputValidator = new SignRequestInputValidator();

  /** Document cache. */
  private DocumentCache documentCache;

  /** The processor for SignMessages. */
  private SignMessageProcessor signMessageProcessor;

  /** Object factory for DSS objects. */
  private static final se.swedenconnect.schemas.dss_1_0.ObjectFactory dssObjectFactory =
      new se.swedenconnect.schemas.dss_1_0.ObjectFactory();

  /** Object factory for DSS-Ext objects. */
  private static final se.swedenconnect.schemas.csig.dssext_1_1.ObjectFactory dssExtObjectFactory =
      new se.swedenconnect.schemas.csig.dssext_1_1.ObjectFactory();

  /** Needed when signing the sign request. */
  private final XMLSignatureLocation xmlSignatureLocation;

  /**
   * The default version to use. If not set, section 3.1 of "DSS Extension for Federated Central Signing Services"
   * states that version 1.1 is the default. So, if the {@code defaultVersion} is not set, we don't include the version
   * unless a feature that requires a higher version is used.
   */
  private ProtocolVersion defaultVersion;

  /** Version 1.4. */
  private static final ProtocolVersion VERSION_1_4 = new ProtocolVersion("1.4");

  /** Constants for conditions. */
  private static final javax.xml.datatype.Duration ONE_MINUTE_BACK;
  private static final javax.xml.datatype.Duration FIVE_MINUTES_FORWARD;

  static {
    try {
      ONE_MINUTE_BACK = DatatypeFactory.newInstance().newDuration(-60000L);
      FIVE_MINUTES_FORWARD = DatatypeFactory.newInstance().newDuration(300000L);
    }
    catch (final DatatypeConfigurationException e) {
      throw new SecurityException(e);
    }
  }

  /**
   * Constructor.
   */
  public DefaultSignRequestProcessor() {
    try {
      this.xmlSignatureLocation = new XMLSignatureLocation("/*/*[local-name()='OptionalInputs']", ChildPosition.LAST);
    }
    catch (final XPathExpressionException e) {
      log.error("Failed to setup XPath for signature inclusion", e);
      throw new SecurityException("Failed to setup XPath for signature inclusion", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public SignRequestInput preProcess(final SignRequestInput signRequestInput,
      final IntegrationServiceConfiguration config, final String callerId) throws InputValidationException {

    // First validate ...
    //
    this.signRequestInputValidator.validateObject(signRequestInput, "signRequestInput", config);

    // Then apply default values ...
    //
    final SignRequestInput.SignRequestInputBuilder inputBuilder = signRequestInput.toBuilder();

    if (StringUtils.isBlank(signRequestInput.getCorrelationId())) {
      log.debug("No correlation ID provided in SignRequestInput, using '{}'", CorrelationID.id());
      inputBuilder.correlationId(CorrelationID.id());
    }

    // Policy
    if (signRequestInput.getPolicy() == null) {
      log.debug("{}: No policy given in input, using '{}'", CorrelationID.id(), config.getPolicy());
      inputBuilder.policy(config.getPolicy());
    }

    // SignRequesterID
    if (signRequestInput.getSignRequesterID() == null) {
      log.debug("{}: No signRequesterID given in input, using '{}'", CorrelationID.id(),
          config.getDefaultSignRequesterID());
      inputBuilder.signRequesterID(config.getDefaultSignRequesterID());
    }

    // ReturnUrl
    if (signRequestInput.getReturnUrl() == null) {
      log.debug("{}: No returnUrl given in input, using '{}'", CorrelationID.id(), config.getDefaultReturnUrl());
      inputBuilder.returnUrl(config.getDefaultReturnUrl());
    }

    // DestinationUrl
    if (signRequestInput.getDestinationUrl() == null) {
      log.debug("{}: No destinationUrl given in input, using '{}'", CorrelationID.id(),
          config.getDefaultDestinationUrl());
      inputBuilder.destinationUrl(config.getDefaultDestinationUrl());
    }

    // SignatureAlgorithm
    if (signRequestInput.getSignatureAlgorithm() == null) {
      log.debug("{}: No signatureAlgorithm given in input, using '{}'", CorrelationID.id(),
          config.getDefaultSignatureAlgorithm());
      inputBuilder.signatureAlgorithm(config.getDefaultSignatureAlgorithm());
    }

    // AuthnRequirements
    //
    final AuthnRequirements authnRequirements = signRequestInput.getAuthnRequirements() != null
        ? signRequestInput.getAuthnRequirements().toBuilder().build()
        : new AuthnRequirements();

    String authnServiceID = authnRequirements.getAuthnServiceID();
    if (authnServiceID == null) {
      log.debug("{}: No authnRequirements.authnServiceID given in input, using '{}'", CorrelationID.id(),
          config.getDefaultAuthnServiceID());
      authnRequirements.setAuthnServiceID(config.getDefaultAuthnServiceID());
      authnServiceID = config.getDefaultAuthnServiceID();
    }
    if (authnRequirements.getAuthnContextClassRefs() == null
        || authnRequirements.getAuthnContextClassRefs().isEmpty()) {
      log.debug("{}: No authnRequirements.authnContextClassRefs given in input, using '{}'",
          CorrelationID.id(), config.getDefaultAuthnContextRef());
      authnRequirements.setAuthnContextClassRefs(Collections.singletonList(config.getDefaultAuthnContextRef()));
    }
    if (authnRequirements.getRequestedSignerAttributes() == null
        || authnRequirements.getRequestedSignerAttributes().isEmpty()) {
      log.info("{}: No requested signer attributes specified - \"anonymous signature\"", CorrelationID.id());
    }
    inputBuilder.authnRequirements(authnRequirements);

    // SigningCertificateRequirements
    //
    if (signRequestInput.getCertificateRequirements() == null) {
      log.debug("{}: No certificateRequirements given in input, using {}", CorrelationID.id(),
          config.getDefaultCertificateRequirements());
      inputBuilder.certificateRequirements(config.getDefaultCertificateRequirements());
    }
    else {
      if (signRequestInput.getCertificateRequirements().getCertificateType() == null) {
        log.debug("{}: No certificateRequirements.certificateType given in input, using {}",
            CorrelationID.id(), config.getDefaultCertificateRequirements().getCertificateType());
        final SigningCertificateRequirements scr = signRequestInput.getCertificateRequirements();
        scr.setCertificateType(config.getDefaultCertificateRequirements().getCertificateType());
        inputBuilder.certificateRequirements(scr);
      }
      if (signRequestInput.getCertificateRequirements().getAttributeMappings() == null
          || signRequestInput.getCertificateRequirements().getAttributeMappings().isEmpty()) {
        log.debug("{}: No certificateRequirements.certificateType given in input, using {}",
            CorrelationID.id(), config.getDefaultCertificateRequirements().getAttributeMappings());
        final SigningCertificateRequirements scr = signRequestInput.getCertificateRequirements();
        scr.setAttributeMappings(config.getDefaultCertificateRequirements().getAttributeMappings());
        inputBuilder.certificateRequirements(scr);
      }
    }

    // TbsDocuments
    //
    // For each document that is to be signed, invoke a matching processor and pre-process it ...
    //
    inputBuilder.clearTbsDocuments();
    int pos = 0;
    for (final TbsDocument doc : signRequestInput.getTbsDocuments()) {
      final String fieldName = "signRequestInput.tbsDocuments[" + pos++ + "]";
      final TbsDocumentProcessor processor = this.tbsDocumentProcessors.stream()
          .filter(p -> p.supports(doc))
          .findFirst()
          .orElseThrow(() -> new InputValidationException(fieldName,
              String.format("Document of type '%s' is not supported", doc.getMimeType())));

      final ProcessedTbsDocument processedTbsDocument = processor.preProcess(
          doc, signRequestInput, config, this.documentCache, callerId, fieldName);

      if (processedTbsDocument.getDocumentObject() != null) {
        final Extension ext = processedTbsDocument.getTbsDocument().getExtension();
        final DocumentExtension docExt = ext == null ? new DocumentExtension() : new DocumentExtension(ext);
        docExt.setDocument(processedTbsDocument.getDocumentObject());
        processedTbsDocument.getTbsDocument().setExtension(docExt);
      }

      inputBuilder.tbsDocument(processedTbsDocument.getTbsDocument());
    }

    // SignMessageParameters
    //
    if (signRequestInput.getSignMessageParameters() != null) {
      SignMessageParameters.SignMessageParametersBuilder smpBuilder = null;
      if (signRequestInput.getSignMessageParameters().getMimeType() == null) {
        log.debug("{}: No signMessageParameters.mimeType given in input, using {}",
            CorrelationID.id(), SignMessageMimeType.TEXT.getMimeType());
        smpBuilder = signRequestInput.getSignMessageParameters().toBuilder();
        smpBuilder.mimeType(SignMessageMimeType.TEXT);
      }
      if (signRequestInput.getSignMessageParameters().getMustShow() == null) {
        log.debug("{}: signMessageParameters.mustShow not set, defaulting to false", CorrelationID.id());
        if (smpBuilder == null) {
          smpBuilder = signRequestInput.getSignMessageParameters().toBuilder();
        }
        smpBuilder.mustShow(false);
      }
      if (signRequestInput.getSignMessageParameters().isPerformEncryption()
          && signRequestInput.getSignMessageParameters().getDisplayEntity() == null) {
        log.debug("{}: signMessageParameters.displayEntity is not set in input, defaulting to {}",
            CorrelationID.id(), authnServiceID);
        if (smpBuilder == null) {
          smpBuilder = signRequestInput.getSignMessageParameters().toBuilder();
        }
        smpBuilder.displayEntity(authnServiceID);
      }
      if (smpBuilder != null) {
        inputBuilder.signMessageParameters(smpBuilder.build());
      }
    }

    return inputBuilder.build();
  }

  /** {@inheritDoc} */
  @Override
  public SignRequestProcessingResult process(
      final SignRequestInput signRequestInput, final String requestID, final IntegrationServiceConfiguration config)
      throws SignServiceIntegrationException {

    // Start building the SignRequest ...
    //
    final SignRequestWrapper signRequest = new SignRequestWrapper(dssObjectFactory.createSignRequest());
    signRequest.setProfile(DssUtils.DSS_PROFILE);
    signRequest.setRequestID(requestID);

    final SignRequestExtension signRequestExtension = dssExtObjectFactory.createSignRequestExtension();

    // Version
    //
    if (this.defaultVersion != null) {
      signRequestExtension.setVersion(this.defaultVersion.toString());
    }

    // RequestTime
    //
    signRequestExtension.setRequestTime(getNow());

    // Conditions
    //
    final Conditions conditions = new Conditions();
    final XMLGregorianCalendar currentTime = getNow();
    // TODO: make configurable
    final XMLGregorianCalendar notBefore = (XMLGregorianCalendar) currentTime.clone();
    notBefore.add(ONE_MINUTE_BACK);
    conditions.setNotBefore(notBefore);
    final XMLGregorianCalendar notAfter = (XMLGregorianCalendar) currentTime.clone();
    notAfter.add(FIVE_MINUTES_FORWARD);
    conditions.setNotOnOrAfter(notAfter);

    final AudienceRestriction audienceRestriction = new AudienceRestriction();
    audienceRestriction.getAudiences().add(signRequestInput.getReturnUrl());
    conditions.getConditionsAndAudienceRestrictionsAndOneTimeUses().add(audienceRestriction);

    signRequestExtension.setConditions(conditions);

    // Signer
    //
    if (signRequestInput.getAuthnRequirements().getRequestedSignerAttributes() != null
        && !signRequestInput.getAuthnRequirements().getRequestedSignerAttributes().isEmpty()) {

      signRequestExtension.setSigner(
          DssUtils.toAttributeStatement(signRequestInput.getAuthnRequirements().getRequestedSignerAttributes()));
    }

    // IdentityProvider
    //
    signRequestExtension
        .setIdentityProvider(DssUtils.toEntity(signRequestInput.getAuthnRequirements().getAuthnServiceID()));

    // AuthnProfile
    //
    if (!StringUtils.isBlank(signRequestInput.getAuthnRequirements().getAuthnProfile())) {
      if (signRequestExtension.getVersion() != null && VERSION_1_4.compareTo(signRequestExtension.getVersion()) > 0) {
        log.info("AuthnProfile is set. Setting version of SignRequest to 1.4 ...");
        signRequestExtension.setVersion("1.4");
        signRequestExtension.setAuthnProfile(signRequestInput.getAuthnRequirements().getAuthnProfile());
      }
    }

    // SignRequester
    //
    signRequestExtension.setSignRequester(DssUtils.toEntity(signRequestInput.getSignRequesterID()));

    // SignService
    //
    signRequestExtension.setSignService(DssUtils.toEntity(config.getSignServiceID()));

    // Requested signature algorithm
    //
    signRequestExtension.setRequestedSignatureAlgorithm(signRequestInput.getSignatureAlgorithm());

    // CertRequestProperties
    //
    if (signRequestInput.getAuthnRequirements().getAuthnContextClassRefs().size() > 1) {
      if (signRequestExtension.getVersion() != null && VERSION_1_4.compareTo(signRequestExtension.getVersion()) > 0) {
        log.info(
            "More that one AuthnContextClassRef URI is assigned to AuthnRequirements. Setting version of SignRequest to 1.4 ...");
        signRequestExtension.setVersion("1.4");
      }
    }
    signRequestExtension.setCertRequestProperties(DssUtils.toCertRequestProperties(
        signRequestInput.getCertificateRequirements(),
        signRequestInput.getAuthnRequirements().getAuthnContextClassRefs()));

    // SignMessage
    //
    if (signRequestInput.getSignMessageParameters() != null) {
      if (this.signMessageProcessor == null) {
        final String msg =
            "No signMessageProcessor has been configured - Cannot process request holding SignMessageParameters";
        log.error(msg);
        throw new InternalSignServiceIntegrationException(new ErrorCode.Code("config"), msg);
      }
      signRequestExtension
          .setSignMessage(this.signMessageProcessor.create(signRequestInput.getSignMessageParameters(), config));
    }

    // Install the sign request extension ...
    //
    signRequest.setSignRequestExtension(signRequestExtension);

    // Invoke all TBS processors ...
    //
    final SignTasks signTasks = dssExtObjectFactory.createSignTasks();
    for (final TbsDocument doc : signRequestInput.getTbsDocuments()) {
      final TbsDocumentProcessor processor = this.tbsDocumentProcessors.stream()
          .filter(p -> p.supports(doc))
          .findFirst()
          .orElseThrow(() -> new InternalSignServiceIntegrationException(new ErrorCode.Code("config"),
              "Could not find document processor"));

      final Object cachedDocument = doc.getExtension() != null && doc.getExtension() instanceof DocumentExtension
          ? ((DocumentExtension) doc.getExtension()).getDocument()
          : null;
      if (cachedDocument != null) {
        // Clean up cached object. We don't want to save it to the session ...
        if (doc.getExtension().isEmpty()) {
          doc.setExtension(null);
        }
        else {
          doc.setExtension(new Extension(doc.getExtension()));
        }
      }

      final SignTaskData signTaskData =
          processor.process(new ProcessedTbsDocument(doc, cachedDocument), signRequestInput
              .getSignatureAlgorithm(), config);
      signTasks.getSignTaskDatas().add(signTaskData);
    }

    // Install the documents ...
    //
    signRequest.setSignTasks(signTasks);

    // Sign the document ...
    //
    final Document signedSignRequest =
        this.signSignRequest(signRequest, signRequestInput.getCorrelationId(), config.getSigningCredential());

    if (log.isTraceEnabled()) {
      log.trace("{}: Created SignRequest: {}", signRequestInput.getCorrelationId(),
          DOMUtils.prettyPrint(signedSignRequest));
    }

    // Transform and Base64-encode the message.
    //
    return new SignRequestProcessingResult(signRequest, DOMUtils.nodeToBase64(signedSignRequest));
  }

  /**
   * Signs the supplied {@code SignRequest} message.
   *
   * @param signRequest the SignRequest message to sign
   * @param correlationID the correlation ID for this operation
   * @param signingCredential the signing credential to use
   * @return a signed document
   * @throws InternalSignServiceIntegrationException for signature errors
   */
  protected Document signSignRequest(
      final SignRequestWrapper signRequest, final String correlationID, final PkiCredential signingCredential)
      throws InternalSignServiceIntegrationException {

    log.debug("{}: Signing SignRequest '{}' ...", correlationID, signRequest.getRequestID());

    try {
      // First marshall the JAXB to a DOM document ...
      //
      final Document signRequestDocument = JAXBMarshaller.marshall(signRequest.getWrappedSignRequest());

      log.debug("Signing: {}", DOMUtils.prettyPrint(signRequestDocument));

      // Get a signer and sign the message ...
      //
      final DefaultXMLSigner signer = new DefaultXMLSigner(signingCredential);
      signer.setSignatureLocation(this.xmlSignatureLocation);
      signer.setXPathTransform(null);
      final XMLSignerResult signerResult = signer.sign(signRequestDocument);
      log.debug("{}: SignRequest '{}' successfully signed", correlationID, signRequest.getRequestID());

      return signerResult.getSignedDocument();
    }
    catch (final JAXBException | SignatureException e) {
      log.error("{}: Error during signing of SignRequest - {}", correlationID, e.getMessage(), e);
      throw new InternalSignServiceIntegrationException(new ErrorCode.Code("signing"),
          "Error during signing of SignRequest", e);
    }
  }

  /** {@inheritDoc} */
  @Override
  public List> getTbsDocumentProcessors() {
    return Collections.unmodifiableList(this.tbsDocumentProcessors);
  }

  /**
   * Sets the list of TBS document processors.
   *
   * @param tbsDocumentProcessors the document processors
   */
  public void setTbsDocumentProcessors(final List> tbsDocumentProcessors) {
    this.tbsDocumentProcessors = tbsDocumentProcessors;
  }

  /**
   * Assigns the sign message processor to use.
   *
   * @param signMessageProcessor the sign message processor
   */
  public void setSignMessageProcessor(final SignMessageProcessor signMessageProcessor) {
    this.signMessageProcessor = signMessageProcessor;
  }

  /**
   * Assigns the document cache to use.
   *
   * @param documentCache the document cache
   */
  public void setDocumentCache(final DocumentCache documentCache) {
    this.documentCache = documentCache;
  }

  /**
   * Assigns the default version to use. If not set, section 3.1 of "DSS Extension for Federated Central Signing
   * Services" states that version 1.1 is the default. So, if the {@code defaultVersion} is not set, we don't include
   * the version unless a feature that requires a higher version is used.
   *
   * @param defaultVersion the version to default to
   */
  public void setDefaultVersion(final String defaultVersion) {
    if (defaultVersion != null) {
      this.defaultVersion = new ProtocolVersion(defaultVersion);
    }
  }

  /**
   * Returns the current time in XML time format.
   *
   * @return the current time
   */
  protected static XMLGregorianCalendar getNow() {
    try {
      final GregorianCalendar gregorianCalendar = new GregorianCalendar();
      final DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
      return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar);
    }
    catch (final DatatypeConfigurationException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Ensures that all required properties have been assigned.
   *
   * 

* Note: If executing in a Spring Framework environment this method is automatically invoked after all properties have * been assigned. Otherwise it should be explicitly invoked. *

* * @throws Exception if not all settings are correct */ @PostConstruct public void afterPropertiesSet() throws Exception { AssertThat.isNotEmpty(this.tbsDocumentProcessors, "At least one TBS document processor must be configured"); if (this.signMessageProcessor == null) { log.warn("No signMessageProcessor assigned - Processor will not be able to process the SignMessage extension"); } } /** * We extend the {@link Extension} class so that we can save a non-string object as an extension during the * processing. */ private static class DocumentExtension extends Extension { @Serial private static final long serialVersionUID = -7525964206819771980L; /** The non-string document object that is stored. */ @JsonIgnore @Getter @Setter private Object document; /** * Default constructor. */ public DocumentExtension() { super(); } /** * Copy constructor. * * @param extension the extension to initialize the object with */ public DocumentExtension(final Extension extension) { super(extension); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy