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

com.itextpdf.signatures.LtvVerification Maven / Gradle / Ivy

There is a newer version: 9.0.0
Show newest version
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2024 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
 */
package com.itextpdf.signatures;

import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
import com.itextpdf.commons.bouncycastle.IBouncyCastleFactory;
import com.itextpdf.commons.bouncycastle.asn1.IDEROctetString;
import com.itextpdf.commons.bouncycastle.asn1.ocsp.IOCSPResponse;
import com.itextpdf.commons.bouncycastle.asn1.ocsp.IOCSPResponseStatus;
import com.itextpdf.commons.bouncycastle.asn1.ocsp.IResponseBytes;
import com.itextpdf.commons.bouncycastle.cert.ocsp.AbstractOCSPException;
import com.itextpdf.commons.bouncycastle.cert.ocsp.IBasicOCSPResp;
import com.itextpdf.commons.bouncycastle.operator.AbstractOperatorCreationException;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.source.ByteBuffer;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.CompressionConstants;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfCatalog;
import com.itextpdf.kernel.pdf.PdfDeveloperExtension;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfIndirectReference;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.signatures.OID.X509Extensions;
import com.itextpdf.signatures.exceptions.SignExceptionMessageConstant;
import com.itextpdf.signatures.logs.SignLogMessageConstant;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CRLException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Add verification according to PAdES-LTV (part 4).
 */
public class LtvVerification {

    private static final IBouncyCastleFactory BOUNCY_CASTLE_FACTORY = BouncyCastleFactoryCreator.getFactory();

    private static final Logger LOGGER = LoggerFactory.getLogger(LtvVerification.class);

    private final PdfDocument document;
    private final SignatureUtil sgnUtil;
    private final Map validated = new HashMap<>();
    private boolean used = false;
    private String securityProviderCode = null;
    private RevocationDataNecessity revocationDataNecessity = RevocationDataNecessity.OPTIONAL;
    private IIssuingCertificateRetriever issuingCertificateRetriever = new DefaultIssuingCertificateRetriever();

    /**
     * What type of verification to include.
     */
    public enum Level {
        /**
         * Include only OCSP.
         */
        OCSP,
        /**
         * Include only CRL.
         */
        CRL,
        /**
         * Include both OCSP and CRL.
         */
        OCSP_CRL,
        /**
         * Include CRL only if OCSP can't be read.
         */
        OCSP_OPTIONAL_CRL
    }

    /**
     * Options for how many certificates to include.
     */
    public enum CertificateOption {
        /**
         * Include verification just for the signing certificate.
         */
        SIGNING_CERTIFICATE,
        /**
         * Include verification for the whole chain of certificates.
         */
        WHOLE_CHAIN,
        /**
         * Include verification for the whole certificates chain, certificates used to create OCSP responses,
         * CRL response certificates and timestamp certificates included in the signatures.
         */
        ALL_CERTIFICATES
    }

    /**
     * Certificate inclusion in the DSS and VRI dictionaries in the CERT and CERTS
     * keys.
     */
    public enum CertificateInclusion {
        /**
         * Include certificates in the DSS and VRI dictionaries.
         */
        YES,
        /**
         * Do not include certificates in the DSS and VRI dictionaries.
         */
        NO
    }

    /**
     * Option to determine whether revocation information is required for the signing certificate.
     */
    public enum RevocationDataNecessity {
        /**
         * Require revocation information for the signing certificate.
         */
        REQUIRED_FOR_SIGNING_CERTIFICATE,
        /**
         * Revocation data for the signing certificate may be optional.
         */
        OPTIONAL
    }

    /**
     * The verification constructor. This class should only be created with
     * PdfStamper.getLtvVerification() otherwise the information will not be
     * added to the Pdf.
     *
     * @param document The {@link PdfDocument} to apply the validation to.
     */
    public LtvVerification(PdfDocument document) {
        this.document = document;
        this.sgnUtil = new SignatureUtil(document);
    }

    /**
     * The verification constructor. This class should only be created with
     * PdfStamper.getLtvVerification() otherwise the information will not be
     * added to the Pdf.
     *
     * @param document             The {@link PdfDocument} to apply the validation to.
     * @param securityProviderCode Security provider to use
     */
    public LtvVerification(PdfDocument document, String securityProviderCode) {
        this(document);
        this.securityProviderCode = securityProviderCode;
    }

    /**
     * Sets {@link RevocationDataNecessity} option to specify the necessity of revocation data.
     *
     * 

* Default value is {@link RevocationDataNecessity#OPTIONAL}. * * @param revocationDataNecessity {@link RevocationDataNecessity} value to set * * @return this {@link LtvVerification} instance. */ public LtvVerification setRevocationDataNecessity(RevocationDataNecessity revocationDataNecessity) { this.revocationDataNecessity = revocationDataNecessity; return this; } /** * Sets {@link IIssuingCertificateRetriever} instance needed to get CRL issuer certificates (using AIA extension). * *

* Default value is {@link DefaultIssuingCertificateRetriever}. * * @param issuingCertificateRetriever {@link IIssuingCertificateRetriever} instance to set * * @return this {@link LtvVerification} instance. */ public LtvVerification setIssuingCertificateRetriever(IIssuingCertificateRetriever issuingCertificateRetriever) { this.issuingCertificateRetriever = issuingCertificateRetriever; return this; } /** * Add verification for a particular signature. * * @param signatureName the signature to validate (it may be a timestamp) * @param ocsp the interface to get the OCSP * @param crl the interface to get the CRL * @param certOption options as to how many certificates to include * @param level the validation options to include * @param certInclude certificate inclusion options * * @return true if a validation was generated, false otherwise * * @throws GeneralSecurityException when requested cryptographic algorithm or security provider * is not available * @throws IOException signals that an I/O exception has occurred */ public boolean addVerification(String signatureName, IOcspClient ocsp, ICrlClient crl, CertificateOption certOption, Level level, CertificateInclusion certInclude) throws IOException, GeneralSecurityException { if (used) { throw new IllegalStateException(SignExceptionMessageConstant.VERIFICATION_ALREADY_OUTPUT); } PdfPKCS7 pk = sgnUtil.readSignatureData(signatureName, securityProviderCode); LOGGER.info("Adding verification for " + signatureName); Certificate[] certificateChain = pk.getCertificates(); X509Certificate signingCert = pk.getSigningCertificate(); ValidationData validationData = new ValidationData(); Set processedCerts = new HashSet<>(); addRevocationDataForChain(signingCert, certificateChain, ocsp, crl, level, certInclude, certOption, validationData, processedCerts); if (certOption == CertificateOption.ALL_CERTIFICATES) { Certificate[] timestampCertsChain = pk.getTimestampCertificates(); addRevocationDataForChain(signingCert, timestampCertsChain, ocsp, crl, level, certInclude, certOption, validationData, processedCerts); } if (certInclude == CertificateInclusion.YES) { for (X509Certificate processedCert : processedCerts) { validationData.certs.add(processedCert.getEncoded()); } } if (validationData.crls.size() == 0 && validationData.ocsps.size() == 0) { return false; } validated.put(getSignatureHashKey(signatureName), validationData); return true; } /** * Adds verification to the signature. * * @param signatureName name of the signature * @param ocsps collection of DER-encoded BasicOCSPResponses * @param crls collection of DER-encoded CRLs * @param certs collection of DER-encoded certificates * * @return boolean * * @throws IOException signals that an I/O exception has occurred * @throws GeneralSecurityException when requested cryptographic algorithm or security provider * is not available */ public boolean addVerification(String signatureName, Collection ocsps, Collection crls, Collection certs) throws IOException, GeneralSecurityException { if (used) { throw new IllegalStateException(SignExceptionMessageConstant.VERIFICATION_ALREADY_OUTPUT); } ValidationData vd = new ValidationData(); if (ocsps != null) { for (byte[] ocsp : ocsps) { vd.ocsps.add(LtvVerification.buildOCSPResponse(ocsp)); } } if (crls != null) { vd.crls.addAll(crls); } if (certs != null) { vd.certs.addAll(certs); } validated.put(getSignatureHashKey(signatureName), vd); return true; } /** * Merges the validation with any validation already in the document or creates a new one. */ public void merge() { if (used || validated.size() == 0) { return; } used = true; PdfDictionary catalog = document.getCatalog().getPdfObject(); PdfObject dss = catalog.get(PdfName.DSS); if (dss == null) { createDss(); } else { updateDss(); } } /** * Converts an array of bytes to a String of hexadecimal values * * @param bytes a byte array * * @return the same bytes expressed as hexadecimal values */ public static String convertToHex(byte[] bytes) { ByteBuffer buf = new ByteBuffer(); for (byte b : bytes) { buf.appendHex(b); } return PdfEncodings.convertToString(buf.toByteArray(), null).toUpperCase(); } /** * Get the issuing certificate for a child certificate. * * @param cert the certificate for which we search the parent * @param certs an array with certificates that contains the parent * * @return the parent certificate */ X509Certificate getParent(X509Certificate cert, Certificate[] certs) { X509Certificate parent; for (Certificate certificate : certs) { parent = (X509Certificate) certificate; if (!cert.getIssuerX500Principal().equals(parent.getSubjectX500Principal())) { continue; } try { cert.verify(parent.getPublicKey()); return parent; } catch (Exception e) { // do nothing } } return null; } private void addRevocationDataForChain(X509Certificate signingCert, Certificate[] certChain, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, Set processedCerts) throws CertificateException, IOException, CRLException { Certificate[] fullChain = certOption == CertificateOption.ALL_CERTIFICATES ? retrieveMissingCertificates(certChain) : certChain; for (Certificate certificate : fullChain) { X509Certificate cert = (X509Certificate) certificate; LOGGER.info(MessageFormatUtil.format("Certificate: {0}", BOUNCY_CASTLE_FACTORY.createX500Name(cert))); if ((certOption == CertificateOption.SIGNING_CERTIFICATE && !cert.equals(signingCert)) || processedCerts.contains(cert)) { continue; } addRevocationDataForCertificate(signingCert, fullChain, cert, ocsp, crl, level, certInclude, certOption, validationData, processedCerts); } } private void addRevocationDataForCertificate(X509Certificate signingCert, Certificate[] certificateChain, X509Certificate cert, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, Set processedCerts) throws IOException, CertificateException, CRLException { processedCerts.add(cert); byte[] validityAssured = SignUtils.getExtensionValueByOid(cert, X509Extensions.VALIDITY_ASSURED_SHORT_TERM); if (validityAssured != null) { LOGGER.info(MessageFormatUtil.format(SignLogMessageConstant.REVOCATION_DATA_NOT_ADDED_VALIDITY_ASSURED, cert.getSubjectX500Principal())); return; } byte[] ocspEnc = null; boolean revocationDataAdded = false; if (ocsp != null && level != Level.CRL) { ocspEnc = ocsp.getEncoded(cert, getParent(cert, certificateChain), null); if (ocspEnc != null && BOUNCY_CASTLE_FACTORY.createCertificateStatus().getGood().equals( OcspClientBouncyCastle.getCertificateStatus(ocspEnc))) { validationData.ocsps.add(LtvVerification.buildOCSPResponse(ocspEnc)); revocationDataAdded = true; LOGGER.info("OCSP added"); if (certOption == CertificateOption.ALL_CERTIFICATES) { addRevocationDataForOcspCert(ocspEnc, signingCert, ocsp, crl, level, certInclude, certOption, validationData, processedCerts); } } else { ocspEnc = null; } } if (crl != null && (level == Level.CRL || level == Level.OCSP_CRL || (level == Level.OCSP_OPTIONAL_CRL && ocspEnc == null))) { Collection cims = crl.getEncoded(cert, null); if (cims != null) { for (byte[] cim : cims) { boolean dup = false; for (byte[] b : validationData.crls) { if (Arrays.equals(b, cim)) { dup = true; break; } } if (!dup) { validationData.crls.add(cim); revocationDataAdded = true; LOGGER.info("CRL added"); if (certOption == CertificateOption.ALL_CERTIFICATES) { Certificate[] certsList = issuingCertificateRetriever.getCrlIssuerCertificates( SignUtils.parseCrlFromStream(new ByteArrayInputStream(cim))); addRevocationDataForChain(signingCert, certsList, ocsp, crl, level, certInclude, certOption, validationData, processedCerts); } } } } } if (revocationDataNecessity == RevocationDataNecessity.REQUIRED_FOR_SIGNING_CERTIFICATE && signingCert.equals(cert) && !revocationDataAdded) { throw new PdfException(SignExceptionMessageConstant.NO_REVOCATION_DATA_FOR_SIGNING_CERTIFICATE); } } private void addRevocationDataForOcspCert(byte[] ocspEnc, X509Certificate signingCert, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, Set processedCerts) throws CertificateException, IOException, CRLException { IBasicOCSPResp ocspResp = BOUNCY_CASTLE_FACTORY.createBasicOCSPResp( BOUNCY_CASTLE_FACTORY.createBasicOCSPResponse(ocspEnc)); Iterable certs = SignUtils.getCertsFromOcspResponse(ocspResp); List ocspCertsList = iterableToList(certs); X509Certificate ocspSigningCert = null; for (X509Certificate ocspCert : ocspCertsList) { try { if (SignUtils.isSignatureValid(ocspResp, ocspCert, BOUNCY_CASTLE_FACTORY.getProviderName())) { ocspSigningCert = ocspCert; break; } } catch (AbstractOperatorCreationException | AbstractOCSPException ignored) { // Wasn't possible to check if this cert is signing one, skip. } } if (ocspSigningCert != null && SignUtils.getExtensionValueByOid( ocspSigningCert, X509Extensions.ID_PKIX_OCSP_NOCHECK) != null) { // If ocsp_no_check extension is set on OCSP signing cert we shan't collect revocation data for this cert. ocspCertsList.remove(ocspSigningCert); processedCerts.add(ocspSigningCert); } addRevocationDataForChain(signingCert, ocspCertsList.toArray(new X509Certificate[0]), ocsp, crl, level, certInclude, certOption, validationData, processedCerts); } private static List iterableToList(Iterable iterable) { List list = new ArrayList<>(); for (X509Certificate certificate : iterable) { list.add(certificate); } return list; } private static byte[] buildOCSPResponse(byte[] basicOcspResponse) throws IOException { IDEROctetString doctet = BOUNCY_CASTLE_FACTORY.createDEROctetString(basicOcspResponse); IOCSPResponseStatus respStatus = BOUNCY_CASTLE_FACTORY.createOCSPResponseStatus( BOUNCY_CASTLE_FACTORY.createOCSPRespBuilderInstance().getSuccessful()); IResponseBytes responseBytes = BOUNCY_CASTLE_FACTORY.createResponseBytes( BOUNCY_CASTLE_FACTORY.createOCSPObjectIdentifiers().getIdPkixOcspBasic(), doctet); IOCSPResponse ocspResponse = BOUNCY_CASTLE_FACTORY.createOCSPResponse(respStatus, responseBytes); return BOUNCY_CASTLE_FACTORY.createOCSPResp(ocspResponse).getEncoded(); } private PdfName getSignatureHashKey(String signatureName) throws NoSuchAlgorithmException { PdfSignature sig = sgnUtil.getSignature(signatureName); PdfString contents = sig.getContents(); byte[] bc = PdfEncodings.convertToBytes(contents.getValue(), null); byte[] bt = hashBytesSha1(bc); return new PdfName(convertToHex(bt)); } private static byte[] hashBytesSha1(byte[] b) throws NoSuchAlgorithmException { MessageDigest sh = MessageDigest.getInstance("SHA1"); return sh.digest(b); } private void updateDss() { PdfDictionary catalog = document.getCatalog().getPdfObject(); catalog.setModified(); PdfDictionary dss = catalog.getAsDictionary(PdfName.DSS); PdfArray ocsps = dss.getAsArray(PdfName.OCSPs); PdfArray crls = dss.getAsArray(PdfName.CRLs); PdfArray certs = dss.getAsArray(PdfName.Certs); dss.remove(PdfName.OCSPs); dss.remove(PdfName.CRLs); dss.remove(PdfName.Certs); PdfDictionary vrim = dss.getAsDictionary(PdfName.VRI); // delete old validations if (vrim != null) { for (PdfName n : vrim.keySet()) { if (validated.containsKey(n)) { PdfDictionary vri = vrim.getAsDictionary(n); if (vri != null) { deleteOldReferences(ocsps, vri.getAsArray(PdfName.OCSP)); deleteOldReferences(crls, vri.getAsArray(PdfName.CRL)); deleteOldReferences(certs, vri.getAsArray(PdfName.Cert)); } } } } if (ocsps == null) { ocsps = new PdfArray(); } if (crls == null) { crls = new PdfArray(); } if (certs == null) { certs = new PdfArray(); } if (vrim == null) { vrim = new PdfDictionary(); } outputDss(dss, vrim, ocsps, crls, certs); } private static void deleteOldReferences(PdfArray all, PdfArray toDelete) { if (all == null || toDelete == null) { return; } for (PdfObject pi : toDelete) { final PdfIndirectReference pir = pi.getIndirectReference(); for (int i = 0; i < all.size(); i++) { final PdfIndirectReference pod = all.get(i).getIndirectReference(); if (Objects.equals(pir, pod)) { all.remove(i); i--; } } } } private void createDss() { outputDss(new PdfDictionary(), new PdfDictionary(), new PdfArray(), new PdfArray(), new PdfArray()); } private void outputDss(PdfDictionary dss, PdfDictionary vrim, PdfArray ocsps, PdfArray crls, PdfArray certs) { PdfCatalog catalog = document.getCatalog(); if (document.getPdfVersion().compareTo(PdfVersion.PDF_2_0) < 0) { catalog.addDeveloperExtension(PdfDeveloperExtension.ESIC_1_7_EXTENSIONLEVEL5); } for (PdfName vkey : validated.keySet()) { PdfArray ocsp = new PdfArray(); PdfArray crl = new PdfArray(); PdfArray cert = new PdfArray(); PdfDictionary vri = new PdfDictionary(); for (byte[] b : validated.get(vkey).crls) { PdfStream ps = new PdfStream(b); ps.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION); ps.makeIndirect(document); crl.add(ps); crls.add(ps); crls.setModified(); } for (byte[] b : validated.get(vkey).ocsps) { PdfStream ps = new PdfStream(b); ps.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION); ocsp.add(ps); ocsps.add(ps); ocsps.setModified(); } for (byte[] b : validated.get(vkey).certs) { PdfStream ps = new PdfStream(b); ps.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION); ps.makeIndirect(document); cert.add(ps); certs.add(ps); certs.setModified(); } if (ocsp.size() > 0) { ocsp.makeIndirect(document); vri.put(PdfName.OCSP, ocsp); } if (crl.size() > 0) { crl.makeIndirect(document); vri.put(PdfName.CRL, crl); } if (cert.size() > 0) { cert.makeIndirect(document); vri.put(PdfName.Cert, cert); } vri.makeIndirect(document); vrim.put(vkey, vri); } vrim.makeIndirect(document); vrim.setModified(); dss.put(PdfName.VRI, vrim); if (ocsps.size() > 0) { ocsps.makeIndirect(document); dss.put(PdfName.OCSPs, ocsps); } if (crls.size() > 0) { crls.makeIndirect(document); dss.put(PdfName.CRLs, crls); } if (certs.size() > 0) { certs.makeIndirect(document); dss.put(PdfName.Certs, certs); } dss.makeIndirect(document); dss.setModified(); catalog.put(PdfName.DSS, dss); } private static class ValidationData { public List crls = new ArrayList<>(); public List ocsps = new ArrayList<>(); public List certs = new ArrayList<>(); } private Certificate[] retrieveMissingCertificates(Certificate[] certChain) { Map restoredChain = new LinkedHashMap<>(); Certificate[] subChain; for (Certificate certificate : certChain) { subChain = issuingCertificateRetriever.retrieveMissingCertificates(new Certificate[]{certificate}); for (Certificate cert : subChain) { restoredChain.put(((X509Certificate) cert).getSubjectX500Principal().getName(), cert); } } return restoredChain.values().toArray(new Certificate[0]); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy