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

se.swedenconnect.sigval.pdf.verify.impl.PDFSingleSignatureValidatorImpl Maven / Gradle / Ivy

package se.swedenconnect.sigval.pdf.verify.impl;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSSignedDataParser;
import org.bouncycastle.cms.CMSTypedStream;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationVerifier;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
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.pdf.configuration.PDFAlgorithmRegistry;
import se.idsec.signservice.security.sign.pdf.configuration.PDFObjectIdentifiers;
import se.swedenconnect.sigval.cert.chain.ExtendedCertPathValidatorException;
import se.swedenconnect.sigval.commons.algorithms.DigestAlgorithm;
import se.swedenconnect.sigval.commons.algorithms.DigestAlgorithmRegistry;
import se.swedenconnect.sigval.commons.data.PolicyValidationResult;
import se.swedenconnect.sigval.commons.data.PubKeyParams;
import se.swedenconnect.sigval.commons.data.SigValIdentifiers;
import se.swedenconnect.sigval.commons.data.TimeValidationResult;
import se.swedenconnect.sigval.commons.timestamp.TimeStamp;
import se.swedenconnect.sigval.commons.timestamp.TimeStampPolicyVerifier;
import se.swedenconnect.sigval.commons.timestamp.impl.BasicTimstampPolicyVerifier;
import se.swedenconnect.sigval.commons.utils.GeneralCMSUtils;
import se.swedenconnect.sigval.pdf.data.ExtendedPdfSigValResult;
import se.swedenconnect.sigval.pdf.pdfstruct.PDFSignatureContext;
import se.swedenconnect.sigval.pdf.timestamp.PDFDocTimeStamp;
import se.swedenconnect.sigval.pdf.utils.CMSVerifyUtils;
import se.swedenconnect.sigval.pdf.verify.PDFSingleSignatureValidator;
import se.swedenconnect.sigval.pdf.verify.policy.PDFSignaturePolicyValidator;
import se.swedenconnect.sigval.pdf.verify.policy.impl.BasicPdfSignaturePolicyValidator;
import se.swedenconnect.sigval.svt.claims.PolicyValidationClaims;
import se.swedenconnect.sigval.svt.claims.TimeValidationClaims;
import se.swedenconnect.sigval.svt.claims.ValidationConclusion;

import java.security.MessageDigest;
import java.security.SignatureException;
import java.util.*;
import java.util.logging.Logger;

/**
 * Implements verification of a PDF signature, validating the actual signature and signing certificates
 *
 * @author Martin Lindström ([email protected])
 * @author Stefan Santesson ([email protected])
 */
@Slf4j
public class PDFSingleSignatureValidatorImpl implements PDFSingleSignatureValidator {

  /**
   * List of timestamp policy verifiers. A timestamp is regarded as trusted if all present policy validators returns a positive result
   * If no policy verifiers are provided, then all timestamps issued by a trusted key are regarded as valid
   **/
  @Setter
  private TimeStampPolicyVerifier timeStampPolicyVerifier;

  /** Signature policy verifier */
  private final PDFSignaturePolicyValidator sigPolicyVerifier;

  /** The certificate validator performing certificate path validation */
  private final CertificateValidator certificateValidator;

  /**
   * Constructor
   *
   * @param certificateValidator the validator used to verify signing certificate chains
   */
  public PDFSingleSignatureValidatorImpl(CertificateValidator certificateValidator) {
    this.certificateValidator = certificateValidator;
    this.timeStampPolicyVerifier = new BasicTimstampPolicyVerifier(certificateValidator);
    this.sigPolicyVerifier = new BasicPdfSignaturePolicyValidator();
  }

  /**
   * Constructor
   *
   * @param certificateValidator    the validator used to verify signing certificate chains
   * @param timeStampPolicyVerifier verifier validating time stamps to a defined policy
   */
  public PDFSingleSignatureValidatorImpl(CertificateValidator certificateValidator, TimeStampPolicyVerifier timeStampPolicyVerifier) {
    this.timeStampPolicyVerifier = timeStampPolicyVerifier;
    this.certificateValidator = certificateValidator;
    this.sigPolicyVerifier = new BasicPdfSignaturePolicyValidator();
  }

  /**
   * Constructor
   *
   * @param certificateValidator        the validator used to verify signing certificate chains
   * @param timeStampPolicyVerifier     verifier validating time stamps to a defined policy
   * @param pdfSignaturePolicyValidator verifier of the signature results according to a defined policy
   */
  public PDFSingleSignatureValidatorImpl(CertificateValidator certificateValidator, PDFSignaturePolicyValidator pdfSignaturePolicyValidator,
    TimeStampPolicyVerifier timeStampPolicyVerifier) {
    this.certificateValidator = certificateValidator;
    this.sigPolicyVerifier = pdfSignaturePolicyValidator;
    this.timeStampPolicyVerifier = timeStampPolicyVerifier;
  }

  /** {@inheritDoc} */
  @Override
  public ExtendedPdfSigValResult verifySignature(PDSignature signature, byte[] pdfDocument,
    List documentTimestamps, PDFSignatureContext signatureContext) throws Exception {
    ExtendedPdfSigValResult sigResult = new ExtendedPdfSigValResult();
    sigResult.setPdfSignature(signature);
    sigResult.setSignedData(signature.getContents(pdfDocument));
    sigResult.setCoversDocument(signatureContext.isCoversWholeDocument(signature));
    byte[] unsignedDocument = null;
    try {
      unsignedDocument = signatureContext.getSignedDocument(signature);
    }
    catch (Exception ex) {
      log.debug("The document signed by this signature is not available");
    }
    sigResult.setSignedDocument(unsignedDocument);
    CMSSignedDataParser cmsSignedDataParser = CMSVerifyUtils.getCMSSignedDataParser(signature, pdfDocument);
    CMSTypedStream signedContent = cmsSignedDataParser.getSignedContent();
    signedContent.drain();
    GeneralCMSUtils.CMSSigCerts CMSSigCerts = GeneralCMSUtils.extractCertificates(cmsSignedDataParser);
    SignerInformation signerInformation = cmsSignedDataParser.getSignerInfos().iterator().next();
    sigResult.setSignerCertificate(CMSSigCerts.getSigCert());
    sigResult.setSignatureCertificateChain(CMSSigCerts.getChain());
    X509CertificateHolder certHolder = new X509CertificateHolder(CMSSigCerts.getSigCert().getEncoded());
    SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(certHolder);

    // Verify signature value against document data
    try {
      sigResult.setStatus(signerInformation.verify(signerInformationVerifier)
        ? SignatureValidationResult.Status.SUCCESS
        : SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
    }
    catch (Exception ex) {
      sigResult.setStatus(SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
      sigResult.setException(ex);
      sigResult.setStatusMessage("Signature validation failure: " + ex.getMessage());
      log.debug("Signature validation failure {}", ex.getMessage());
      return sigResult;
    }

    // Get algorithms and public key related data
    PubKeyParams pkParams = GeneralCMSUtils.getPkParams(sigResult.getSignerCertificate().getPublicKey());
    sigResult.setPubKeyParams(pkParams);

    ASN1ObjectIdentifier signAlgoOid = new ASN1ObjectIdentifier(signerInformation.getEncryptionAlgOID());
    ASN1ObjectIdentifier digestAlgoOid = new ASN1ObjectIdentifier(signerInformation.getDigestAlgOID());
    sigResult.setCmsSignatureAlgo(signAlgoOid);
    sigResult.setCmsDigestAlgo(digestAlgoOid);
    String algorithmURI = null;
    try {
      algorithmURI = PDFAlgorithmRegistry.getAlgorithmURI(signAlgoOid, digestAlgoOid);
    }
    catch (Exception ex) {
      sigResult.setStatus(SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
      sigResult.setException(ex);
      sigResult.setStatusMessage("Signature was signed with unsupported algorithms");
      log.debug("Signature was signed with unsupported algorithms: Signature algo {}, Digest algo {}", signAlgoOid, digestAlgoOid);
      return sigResult;
    }
    sigResult.setSignatureAlgorithm(algorithmURI);
    AttributeTable signedAttributes = signerInformation.getSignedAttributes();
    Attribute cmsAlgoProtAttr = signedAttributes.get(PKCSObjectIdentifiers.id_aa_cmsAlgorithmProtect);
    //    Attribute cmsAlgoProtAttr = signedAttributes.get(new ASN1ObjectIdentifier(PDFObjectIdentifiers.ID_AA_CMS_ALGORITHM_PROTECTION));
    CMSVerifyUtils.getCMSAlgoritmProtectionData(cmsAlgoProtAttr, sigResult);

    // Check algorithm consistency
    if (!CMSVerifyUtils.checkAlgoritmConsistency(sigResult)) {
      sigResult.setStatus(SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
      sigResult.setException(new SignatureException("Signature algorithm mismatch in CMS algorithm protection extension"));
      sigResult.setStatusMessage("Signature algorithm mismatch in CMS algorithm protection extension");
      log.debug("CMS algo protection mismatch: Signature algo {}, Digest algo {}, CMS-AP Signature algo {}, CMS-AP Digest algo {}",
        signAlgoOid, digestAlgoOid, sigResult.getCmsAlgoProtectionSigAlgo(), sigResult.getCmsAlgoProtectionDigestAlgo());
      return sigResult;
    }

    // Check Pades properties
    if (sigResult.isSuccess()) {
      verifyPadesProperties(signerInformation, sigResult);
    }

    //Check claimed signing time
    sigResult.setClaimedSigningTime(
      getClaimedSigningTime(signature.getSignDate(), signedAttributes.get(PKCSObjectIdentifiers.pkcs_9_at_signingTime)));

    // Verify timestamps
    try {
      sigResult.setTimeValidationResults(checkTimeStamps(signerInformation));
    }
    catch (Exception ex) {
      sigResult.setTimeValidationResults(new ArrayList<>());
      Logger.getLogger(PDFSingleSignatureValidator.class.getName()).warning("Error parsing signature timestamps: " + ex.getMessage());
    }

    // Add timestamp results
    addVerifiedTimes(sigResult, documentTimestamps);

    try {
      CertificateValidationResult validationResult = certificateValidator.validate(sigResult.getSignerCertificate(),
        sigResult.getSignatureCertificateChain(), null);
      sigResult.setCertificateValidationResult(validationResult);
    }
    catch (Exception ex) {
      if (ex instanceof ExtendedCertPathValidatorException) {
        ExtendedCertPathValidatorException extEx = (ExtendedCertPathValidatorException) ex;
        sigResult.setCertificateValidationResult(extEx.getPathValidationResult());
        sigResult.setStatusMessage(extEx.getMessage());
      }
      else {
        sigResult.setStatusMessage("Signer certificate failed path validation");
      }
      //sigResult.setSuccess(false);
      //sigResult.setStatus(SignatureValidationResult.Status.ERROR_SIGNER_INVALID);
      sigResult.setException(ex);
    }

    // Let the signature policy verifier determine the final result path validation
    // The signature policy verifier may accept a revoked cert if signature is timestamped
    PolicyValidationResult policyValidationResult = sigPolicyVerifier.validatePolicy(sigResult, signatureContext);
    PolicyValidationClaims policyValidationClaims = policyValidationResult.getPolicyValidationClaims();
    if (!policyValidationClaims.getRes().equals(ValidationConclusion.PASSED)) {
      sigResult.setStatus(policyValidationResult.getStatus());
      sigResult.setStatusMessage(policyValidationClaims.getMsg());
      sigResult.setException(new SignatureException(policyValidationClaims.getMsg()));
    }
    sigResult.setValidationPolicyResultList(Arrays.asList(policyValidationClaims));

    return sigResult;
  }

  /**
   * Extracts the claimed signing time from a PDF signature
   *
   * @param dictionalyDignDate    the signing time obtained from the signature dictionary or null of no such time exist
   * @param signedAttrSigningTime the signing time attribute from signed attributes
   * @return signing time in milliseconds from epoc time
   */
  private Date getClaimedSigningTime(Calendar dictionalyDignDate, Attribute signedAttrSigningTime) {
    if (signedAttrSigningTime == null && dictionalyDignDate == null) {
      log.debug("No time information available as claimed signing time");
      return null;
    }
    if (signedAttrSigningTime == null) {
      log.debug("No claimed signing time in signed attributes. Using time from signature dictionary");
      return dictionalyDignDate.getTime();
    }
    ASN1Encodable[] attributeValues = signedAttrSigningTime.getAttributeValues();
    try {
      ASN1UTCTime signingTime = ASN1UTCTime.getInstance(attributeValues[0]);
      log.debug("Found UTC claimed signing time in signed attributes");
      return signingTime.getAdjustedDate();
    }
    catch (Exception ex) {
      log.debug("Unable to extract UTCTime from signing time signed attributes. Attempting Generalized time");
    }
    try {
      ASN1GeneralizedTime signingTime = ASN1GeneralizedTime.getInstance(attributeValues[0]);
      log.debug("Found Generalized time claimed signing time in signed attributes");
      return signingTime.getDate();
    }
    catch (Exception ex) {
      log.debug("Unable to extract time information from signing time signed attributes.");
    }
    return null;
  }

  /** {@inheritDoc} */
  @Override public List verifyDocumentTimestamps(List documentTimestampSignatures, byte[] pdfDocument) {
    List docTimeStampList = new ArrayList<>();
    for (PDSignature sig : documentTimestampSignatures) {
      try {
        PDFDocTimeStamp docTs = new PDFDocTimeStamp(sig, pdfDocument, timeStampPolicyVerifier);
        docTimeStampList.add(docTs);
      }
      catch (Exception e) {
        log.warn("Exception while processing document timestamp" + e.getMessage());
      }
    }
    return docTimeStampList;
  }

  /** {@inheritDoc} */
  @Override public CertificateValidator getCertificateValidator() {
    return certificateValidator;
  }

  /**
   * Validates the timestamp embedded inside the target signature
   *
   * @param signerInformation signerInformation holding the timestamp
   * @return a list of timestamps found in the signature data
   * @throws Exception on errors
   */
  private List checkTimeStamps(final SignerInformation signerInformation)
    throws Exception {
    AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
    if (unsignedAttributes == null) {
      return new ArrayList<>();
    }
    ASN1EncodableVector timeStampsASN1 = unsignedAttributes.getAll(new ASN1ObjectIdentifier(PDFObjectIdentifiers.ID_TIMESTAMP_ATTRIBUTE));
    if (timeStampsASN1.size() == 0) {
      return new ArrayList<>();
    }
    List timeStampList = new ArrayList<>();
    for (int i = 0; i < timeStampsASN1.size(); i++) {
      Attribute tsAttribute = Attribute.getInstance(timeStampsASN1.get(i));
      byte[] tsContentInfoBytes = ContentInfo.getInstance(tsAttribute.getAttrValues().getObjectAt(0).toASN1Primitive()).getEncoded("DER");
      TimeStamp timeStamp = new TimeStamp(tsContentInfoBytes, signerInformation.getSignature(), timeStampPolicyVerifier);
      timeStampList.add(new TimeValidationResult(null, timeStamp.getCertificateValidationResult(), timeStamp));
    }
    return timeStampList;
  }

  /**
   * Add verified timestamps to the signature validation results
   *
   * @param directVerifyResult the verification results including result data from time stamps embedded in the signature
   * @param docTimeStampList   list of document timestamps provided with this signed PDF document
   */
  private void addVerifiedTimes(ExtendedPdfSigValResult directVerifyResult, final List docTimeStampList) {
    List timeValidationResults = new ArrayList<>();

    // Loop through direct validation results and add signature timestamp results
    for (TimeValidationResult result : directVerifyResult.getTimeValidationResults()) {
      TimeStamp timeStamp = result.getTimeStamp();
      TimeValidationClaims timeValidationClaims = getVerifiedTimeFromTimeStamp(timeStamp,
        SigValIdentifiers.TIME_VERIFICATION_TYPE_SIG_TIMESTAMP);
      if (timeValidationClaims != null) {
        timeValidationResults.add(new TimeValidationResult(
          timeValidationClaims, timeStamp.getCertificateValidationResult(), timeStamp));
      }
    }

    // Loop through document timestamps
    for (PDFDocTimeStamp docTimeStamp : docTimeStampList) {
      TimeValidationClaims timeValidationClaims = getVerifiedTimeFromTimeStamp(docTimeStamp,
        SigValIdentifiers.TIME_VERIFICATION_TYPE_PDF_DOC_TIMESTAMP);
      if (timeValidationClaims != null) {
        timeValidationResults.add(new TimeValidationResult(
          timeValidationClaims, docTimeStamp.getCertificateValidationResult(), docTimeStamp));
      }
    }

    directVerifyResult.setTimeValidationResults(timeValidationResults);
  }

  private TimeValidationClaims getVerifiedTimeFromTimeStamp(final TimeStamp pdfTimeStamp, final String type) {
    try {
      TimeValidationClaims timeValidationClaims = TimeValidationClaims.builder()
        .id(pdfTimeStamp.getTstInfo().getSerialNumber().getValue().toString(16))
        .iss(pdfTimeStamp.getSigCert().getSubjectX500Principal().toString())
        .time(pdfTimeStamp.getTstInfo().getGenTime().getDate().getTime() / 1000)
        .type(type)
        .val(pdfTimeStamp.getPolicyValidationClaimsList())
        .build();
      return timeValidationClaims;
    }
    catch (Exception ex) {
      return null;
    }
  }

  /**
   * Verifies the PAdES properties of this signature
   *
   * @param signer    SignerInformation of this signature
   * @param sigResult signature result object for this signature
   */
  public void verifyPadesProperties(final SignerInformation signer, ExtendedPdfSigValResult sigResult) {
    try {
      AttributeTable signedAttributes = signer.getSignedAttributes();
      Attribute essSigningCertV2Attr = signedAttributes.get(new ASN1ObjectIdentifier(PDFObjectIdentifiers.ID_AA_SIGNING_CERTIFICATE_V2));
      Attribute signingCertAttr = signedAttributes.get(new ASN1ObjectIdentifier(PDFObjectIdentifiers.ID_AA_SIGNING_CERTIFICATE_V1));

      if (essSigningCertV2Attr == null && signingCertAttr == null) {
        sigResult.setEtsiAdes(false);
        sigResult.setInvalidSignCert(false);
        return;
      }

      //Start assuming that PAdES validation is non-successful
      sigResult.setEtsiAdes(true);
      sigResult.setInvalidSignCert(true);
      sigResult.setStatus(SignatureValidationResult.Status.ERROR_SIGNER_INVALID);

      DEROctetString certHashOctStr = null;
      DigestAlgorithm hashAlgo = null;

      if (essSigningCertV2Attr != null) {
        ASN1Sequence essCertIDv2Sequence = GeneralCMSUtils.getESSCertIDSequence(essSigningCertV2Attr);
        /**
         * ESSCertIDv2 ::=  SEQUENCE {
         *   hashAlgorithm           AlgorithmIdentifier
         *                   DEFAULT {algorithm id-sha256},
         *   certHash                 Hash,
         *   issuerSerial             IssuerSerial OPTIONAL
         * }
         *
         */
        // BUG Fix 190121. Hash algorithm is optional and defaults to SHA256. Fixed from being treated as mandatory.
        int certHashIndex = 0;
        if (essCertIDv2Sequence.getObjectAt(0) instanceof ASN1Sequence) {
          // Hash algo identifier id present. Get specified value and set certHashIndex index to 1.
          ASN1Sequence algoSeq = (ASN1Sequence) essCertIDv2Sequence.getObjectAt(0); //Holds sequence of OID and algo params
          ASN1ObjectIdentifier algoOid = (ASN1ObjectIdentifier) algoSeq.getObjectAt(0);
          hashAlgo = DigestAlgorithmRegistry.get(algoOid);
          certHashIndex = 1;
        }
        else {
          // Hash algo identifier is not present. Set hash algo to the default SHA-256 value and keep certHashIndex index = 0.
          hashAlgo = DigestAlgorithmRegistry.get(DigestAlgorithm.ID_SHA256);
        }
        certHashOctStr = (DEROctetString) essCertIDv2Sequence.getObjectAt(certHashIndex);
      }
      else {
        if (signingCertAttr != null) {
          ASN1Sequence essCertIDSequence = GeneralCMSUtils.getESSCertIDSequence(signingCertAttr);
          /**
           * ESSCertID ::=  SEQUENCE {
           *      certHash                 Hash,
           *      issuerSerial             IssuerSerial OPTIONAL
           * }
           */
          certHashOctStr = (DEROctetString) essCertIDSequence.getObjectAt(0);
          hashAlgo = DigestAlgorithmRegistry.get(DigestAlgorithm.ID_SHA1);
        }
      }

      if (hashAlgo == null || certHashOctStr == null) {
        sigResult.setStatusMessage("Unsupported hash algo for ESS-SigningCertAttributeV2");
        return;
      }

      MessageDigest md = hashAlgo.getInstance();
      md.update(sigResult.getSignerCertificate().getEncoded());
      byte[] certHash = md.digest();

      //            //Debug
      //            String certHashStr = String.valueOf(Base64Coder.encode(certHash));
      //            String expectedCertHashStr = String.valueOf(Base64Coder.encode(certHashOctStr.getOctets()));
      if (!Arrays.equals(certHash, certHashOctStr.getOctets())) {
        sigResult.setStatusMessage("Cert Hash mismatch");
        return;
      }

      //PadES validation was successful
      sigResult.setInvalidSignCert(false);
      sigResult.setStatus(SignatureValidationResult.Status.SUCCESS);

    }
    catch (Exception e) {
      sigResult.setStatusMessage("Exception while examining Pades signed cert attr: " + e.getMessage());
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy