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

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

The 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.cert.ocsp.IBasicOCSPResp;
import com.itextpdf.signatures.logs.SignLogMessageConstant;
import com.itextpdf.signatures.validation.TrustedCertificatesStore;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.cert.CRL;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * {@link IIssuingCertificateRetriever} default implementation.
 */
public class IssuingCertificateRetriever implements IIssuingCertificateRetriever {

    private static final IBouncyCastleFactory FACTORY = BouncyCastleFactoryCreator.getFactory();
    private static final Logger LOGGER = LoggerFactory.getLogger(IssuingCertificateRetriever.class);

    private final TrustedCertificatesStore trustedCertificatesStore = new TrustedCertificatesStore();
    private final Map> knownCertificates = new HashMap<>();

    /**
     * Creates {@link IssuingCertificateRetriever} instance.
     */
    public IssuingCertificateRetriever() {
        // Empty constructor.
    }

    /**
     * {@inheritDoc}
     *
     * @param chain {@inheritDoc}
     *
     * @return {@inheritDoc}
     */
    @Override
    public Certificate[] retrieveMissingCertificates(Certificate[] chain) {
        List fullChain = new ArrayList<>();
        X509Certificate signingCertificate = (X509Certificate) chain[0];
        fullChain.add(signingCertificate);

        int i = 1;
        X509Certificate lastAddedCert = signingCertificate;
        while (!CertificateUtil.isSelfSigned(lastAddedCert)) {
            // Check if there are any missing certificates with isSignedByNext
            if (i < chain.length &&
                    CertificateUtil.isIssuerCertificate(lastAddedCert, (X509Certificate) chain[i])) {
                fullChain.add(chain[i]);
                i++;
            } else {
                // Get missing certificates using AIA Extensions
                String url = CertificateUtil.getIssuerCertURL(lastAddedCert);
                Collection certificatesFromAIA = processCertificatesFromAIA(url);
                if (certificatesFromAIA == null || certificatesFromAIA.isEmpty()) {
                    // Retrieve Issuer from the certificate store
                    Certificate issuer = getIssuerFromCertificateSet(lastAddedCert, trustedCertificatesStore
                            .getKnownCertificates(lastAddedCert.getIssuerX500Principal().getName()));
                    if (issuer == null || !isSignedBy(lastAddedCert, issuer)) {
                        issuer = getIssuerFromCertificateSet(lastAddedCert, knownCertificates.get(
                                lastAddedCert.getIssuerX500Principal().getName()));
                        if (issuer == null) {
                            // Unable to retrieve missing certificates
                            while (i < chain.length) {
                                fullChain.add(chain[i]);
                                i++;
                            }
                            return fullChain.toArray(new Certificate[0]);
                        }
                    }
                    fullChain.add(issuer);
                } else {
                    fullChain.addAll(certificatesFromAIA);
                }
            }
            lastAddedCert = (X509Certificate) fullChain.get(fullChain.size() - 1);
        }

        return fullChain.toArray(new Certificate[0]);
    }

    /**
     * This method tries to rebuild certificate issuer chain. The result contains all possible chains
     * starting with the given certificate based on issuer names and public keys.
     *
     * @param certificate {@link X509Certificate} for which issuer chains shall be built
     *
     * @return all possible issuer chains
     */
    public List buildCertificateChains(X509Certificate certificate) {
        return buildCertificateChains(new X509Certificate[] {certificate});
    }

    /**
     * This method tries to rebuild certificate issuer chain. The result contains all possible chains
     * starting with the given certificate array based on issuer names and public keys.
     *
     * @param certificate {@link X509Certificate} array for which issuer chains shall be built
     *
     * @return all possible issuer chains
     */
    public List buildCertificateChains(X509Certificate[] certificate) {
        List> allCertificateChains = buildCertificateChainsList(certificate);
        List result = new ArrayList<>(allCertificateChains.size() * 5);
        for (List chain : allCertificateChains) {
            Collections.reverse(chain);
            result.add(chain.toArray(new X509Certificate[0]));
        }
        return result;
    }

    private List> buildCertificateChainsList(X509Certificate[] certificates) {
        List> allChains =
                new ArrayList<>(buildCertificateChainsList(certificates[certificates.length - 1]));
        for (List issuerChain : allChains) {
            for (int i = certificates.length - 2; i >= 0; --i) {
                issuerChain.add(certificates[i]);
            }
        }
        return allChains;
    }

    private List> buildCertificateChainsList(X509Certificate certificate) {
        if (CertificateUtil.isSelfSigned(certificate)) {
            List> singleChain = new ArrayList<>();
            List chain = new ArrayList<>();
            chain.add(certificate);
            singleChain.add(chain);
            return singleChain;
        }

        List> allChains = new ArrayList<>();
        // Get missing certificates using AIA Extensions
        String url = CertificateUtil.getIssuerCertURL(certificate);
        Collection certificatesFromAIA = processCertificatesFromAIA(url);
        if (certificatesFromAIA != null && !certificatesFromAIA.isEmpty()) {
            List> issuerChains = buildCertificateChainsList(certificatesFromAIA
                    .toArray(new X509Certificate[0]));
            for (List issuerChain : issuerChains) {
                issuerChain.add(certificate);
                allChains.add(issuerChain);
            }
        } else {
            Set possibleIssuers = trustedCertificatesStore
                    .getKnownCertificates(certificate.getIssuerX500Principal().getName());
            if (knownCertificates.get(
                    certificate.getIssuerX500Principal().getName()) != null) {
                possibleIssuers.addAll(knownCertificates.get(certificate.getIssuerX500Principal().getName()));
            }
            if (possibleIssuers.isEmpty()) {
                List> singleChain = new ArrayList<>();
                List chain = new ArrayList<>();
                chain.add(certificate);
                singleChain.add(chain);
                return singleChain;
            }
            for (Certificate possibleIssuer : possibleIssuers) {
                List> issuerChains = buildCertificateChainsList((X509Certificate) possibleIssuer);
                for (List issuerChain : issuerChains) {
                    issuerChain.add(certificate);
                    allChains.add(issuerChain);
                }
            }
        }
        return allChains;
    }

    /**
     * Retrieve issuer certificate for the provided certificate.
     *
     * @param certificate {@link Certificate} for which issuer certificate shall be retrieved
     *
     * @return issuer certificate. {@code null} if there is no issuer certificate, or it cannot be retrieved.
     */
    public List retrieveIssuerCertificate(Certificate certificate) {
        List result = new ArrayList<>();
        for (X509Certificate[] certificateChain : buildCertificateChains((X509Certificate) certificate)) {
            if (certificateChain.length > 1) {
                result.add(certificateChain[1]);
            }
        }
        return result;
    }

    /**
     * Retrieves OCSP responder certificate candidates either from the response certs or
     * trusted store in case responder certificate isn't found in /Certs.
     *
     * @param ocspResp basic OCSP response to get responder certificate for
     *
     * @return retrieved OCSP responder candidates or an empty set in case none were found.
     */
    public Set retrieveOCSPResponderByNameCertificate(IBasicOCSPResp ocspResp) {

        String name = null;
        name = FACTORY.createX500Name( FACTORY.createASN1Sequence(
                ocspResp.getResponderId().toASN1Primitive().getName().toASN1Primitive())).getName();

        // Look for the existence of an Authorized OCSP responder inside the cert chain in the ocsp response.
        Iterable certs = SignUtils.getCertsFromOcspResponse(ocspResp);
        for (X509Certificate cert : certs) {
            try {
                if (name.equals(cert.getSubjectX500Principal().getName())) {
                    return Collections.singleton(cert);
                }
            } catch (Exception ignored) {
                // Ignore.
            }
        }
        // Certificate chain is not present in the response, or is does not contain the responder.
        return trustedCertificatesStore.getKnownCertificates(name);
    }


    /**
     * {@inheritDoc}
     *
     * @param crl {@inheritDoc}
     *
     * @return {@inheritDoc}
     */
    @Override
    public Certificate[] getCrlIssuerCertificates(CRL crl) {
        Certificate[][] result = getCrlIssuerCertificatesGeneric(crl, true);
        if (result.length == 0) {
            return new Certificate[0];
        }
        return result[0];
    }

    /**
     * {@inheritDoc}
     *
     * @param crl {@inheritDoc}
     *
     * @return {@inheritDoc}
     */

    @Override
    public Certificate[][] getCrlIssuerCertificatesByName(CRL crl) {
        return getCrlIssuerCertificatesGeneric(crl, false);
    }

    private Certificate[][] getCrlIssuerCertificatesGeneric(CRL crl, boolean verify) {
        // Usually CRLs are signed using CA certificate, so we don’t need to do anything extra and the revocation data
        // is already collected. However, it is possible to sign it with any other certificate.

        // IssuingDistributionPoint extension: https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5
        // Nothing special for the indirect CRLs.

        // AIA Extension
        ArrayList matches = new ArrayList();
        String url = CertificateUtil.getIssuerCertURL(crl);
        List certificatesFromAIA = (List) processCertificatesFromAIA(url);
        if (certificatesFromAIA == null) {
            // Retrieve Issuer from the certificate store
            Set issuers = trustedCertificatesStore
                    .getKnownCertificates(((X509CRL) crl).getIssuerX500Principal().getName());
            if (issuers == null) {
                issuers = new HashSet<>();
            }
            List localIssuers = getCrlIssuersFromKnownCertificates((X509CRL) crl);
            if (localIssuers != null) {
                issuers.addAll(localIssuers);
            }
            if (issuers.isEmpty()) {
                // Unable to retrieve CRL issuer
                return new Certificate[0][];
            }
            for (Certificate i: issuers) {
                if (!verify || isSignedBy((X509CRL) crl, i)) {
                    matches.addAll(buildCertificateChains((X509Certificate) i));
                }
            }
            return matches.toArray(new Certificate[][]{});
        }
        return buildCertificateChains(certificatesFromAIA.toArray(new X509Certificate[0]))
                .toArray(new Certificate[][]{});
    }

    /**
     * Sets trusted certificate list to be used as certificates trusted for any possible usage.
     * In case more specific trusted is desired to be configured
     * {@link IssuingCertificateRetriever#getTrustedCertificatesStore()} method is expected to be used.
     *
     * @param certificates certificate list to be used as certificates trusted for any possible usage.
     */
    @Override
    public void setTrustedCertificates(Collection certificates) {
        addTrustedCertificates(certificates);
    }

    /**
     * Add trusted certificates collection to trusted certificates storage.
     *
     * @param certificates certificates {@link Collection} to be added
     */
    public void addTrustedCertificates(Collection certificates) {
        trustedCertificatesStore.addGenerallyTrustedCertificates(certificates);
    }

    /**
     * Add certificates collection to known certificates storage, which is used for issuer certificates retrieval.
     *
     * @param certificates certificates {@link Collection} to be added
     */
    public void addKnownCertificates(Collection certificates) {
        for (Certificate certificate : certificates) {
            String name = ((X509Certificate) certificate).getSubjectX500Principal().getName();
            List certs = knownCertificates.computeIfAbsent(name,k -> new ArrayList<>());
            certs.add(certificate);
        }
    }

    /**
     * Gets {@link TrustedCertificatesStore} to be used to provide more complex trusted certificates configuration.
     *
     * @return {@link TrustedCertificatesStore} storage
     */
    public TrustedCertificatesStore getTrustedCertificatesStore() {
        return trustedCertificatesStore;
    }

    /**
     * Check if provided certificate is present in trusted certificates storage.
     *
     * @param certificate {@link Certificate} to be checked
     *
     * @return {@code true} if certificate is present in trusted certificates storage, {@code false} otherwise
     */
    public boolean isCertificateTrusted(Certificate certificate) {
        return trustedCertificatesStore.isCertificateGenerallyTrusted(certificate);
    }

    /**
     * Get CA issuers certificates represented as {@link InputStream}.
     *
     * @param uri {@link URL} URI, which is expected to be used to get issuer certificates from. Usually
     *            CA Issuers value from Authority Information Access (AIA) certificate extension.
     *
     * @return CA issuer certificate (or chain) bytes, represented as {@link InputStream}.
     *
     * @throws IOException if an I/O error occurs.
     */
    protected InputStream getIssuerCertByURI(String uri) throws IOException {
        return SignUtils.getHttpResponse(new URL(uri));
    }

    /**
     * Parses certificates represented as byte array.
     *
     * @param certsData stream which contains one or more X509 certificates.
     *
     * @return a (possibly empty) collection of the certificates read from the given byte array.
     *
     * @throws CertificateException if parsing error occurs.
     */
    protected Collection parseCertificates(InputStream certsData) throws CertificateException {
        return SignUtils.readAllCerts(certsData, null);
    }

    private Collection processCertificatesFromAIA(String url) {
        if (url == null) {
            // We don't have any URIs to the issuer certificates in AuthorityInfoAccess extension
            return null;
        }
        try (InputStream missingCertsData = getIssuerCertByURI(url)) {
            return parseCertificates(missingCertsData);
        } catch (Exception e) {
            LOGGER.warn(SignLogMessageConstant.UNABLE_TO_PARSE_AIA_CERT);
            return null;
        }
    }

    private static boolean isSignedBy(X509Certificate certificate, Certificate issuer) {
        try {
            certificate.verify(issuer.getPublicKey());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private static boolean isSignedBy(X509CRL crl, Certificate issuer) {
        try {
            crl.verify(issuer.getPublicKey());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private static Certificate getIssuerFromCertificateSet(X509Certificate lastAddedCert,
            Collection certs) {
        if (certs != null) {
            for (Certificate cert : certs) {
                if (isSignedBy(lastAddedCert, cert)) {
                    return cert;
                }
            }
        }
        return null;
    }

    private List getCrlIssuersFromKnownCertificates(X509CRL crl) {
        return knownCertificates.get(crl.getIssuerX500Principal().getName());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy