at.gv.egiz.pdfas.lib.pki.impl.DefaultCertificateVerificationDataProvider.java Source code

Java tutorial

Introduction

Here is the source code for at.gv.egiz.pdfas.lib.pki.impl.DefaultCertificateVerificationDataProvider.java

Source

/*******************************************************************************
 * <copyright> Copyright 2017 by PrimeSign GmbH, Graz, Austria </copyright>
 *
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 * http://www.osor.eu/eupl/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and
 * limitations under the Licence.
 *
 * This product combines work with different licenses. See the "NOTICE" text
 * file for details on the various modules and licenses.
 * The "NOTICE" text file is part of the distribution. Any derivative works
 * that you distribute must include a readable copy of the "NOTICE" text file.
 ******************************************************************************/
package at.gv.egiz.pdfas.lib.pki.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.CRLException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import at.gv.egiz.pdfas.common.settings.ISettings;
import at.gv.egiz.pdfas.lib.pki.spi.CertificateVerificationData;
import at.gv.egiz.pdfas.lib.pki.spi.CertificateVerificationDataProviderSpi;
import iaik.asn1.structures.DistributionPoint;
import iaik.utils.Util;
import iaik.x509.X509CRL;
import iaik.x509.X509Certificate;
import iaik.x509.X509ExtensionInitException;
import iaik.x509.extensions.AuthorityKeyIdentifier;
import iaik.x509.extensions.CRLDistributionPoints;
import iaik.x509.ocsp.BasicOCSPResponse;
import iaik.x509.ocsp.OCSPResponse;

/**
 * Provides basic support for gathering certificate verification information.
 * 
 * @author Thomas Knall, PrimeSign GmbH
 * @implNote This class is immutable and therefore thread-safe. Note that this implementation might not support
 *           resolving chains with certificates that have been re-certified.
 */
public class DefaultCertificateVerificationDataProvider implements CertificateVerificationDataProviderSpi {

    private static final Logger log = LoggerFactory.getLogger(DefaultCertificateVerificationDataProvider.class);

    // @formatter:off
    // Timeouts for OCSP and CRL connections
    private final int DEFAULT_CONNECTION_TIMEOUT_MS = 10000;
    private final int DEFAULT_READ_TIMEOUT_MS = 10000;

    /**
     * Name of configuration folder containing PKCS#7 certificate chains.
     */
    private final String DEFAULT_CHAINSTORE_FOLDER_NAME = "/certchains";
    // @formatter:on   

    @Override
    public boolean canHandle(java.security.cert.X509Certificate eeCertificate, ISettings settings) {
        return findChainFile(toIAIKX509Certificate(Objects.requireNonNull(eeCertificate)),
                Objects.requireNonNull(settings)) != null;
    }

    @Override
    public CertificateVerificationData getCertificateVerificationData(
            java.security.cert.X509Certificate eeCertificate, ISettings settings)
            throws CertificateException, IOException {

        X509Certificate iaikEeCertificate = toIAIKX509Certificate(Objects.requireNonNull(eeCertificate));

        // @formatter:off
        final Set<java.security.cert.X509Certificate> certs = new LinkedHashSet<>(); // not thread-safe
        final List<byte[]> ocsps = new ArrayList<>(); // not thread-safe
        final Set<java.security.cert.X509CRL> crls = new LinkedHashSet<>(); // not thread-safe
        // @formatter:on

        StopWatch sw = new StopWatch();
        sw.start();

        if (log.isDebugEnabled()) {
            log.debug("Retrieving certificate validation info info for {}", iaikEeCertificate.getSubjectDN());
        } else if (log.isInfoEnabled()) {
            log.info("Retrieving certificate validation data for certificate (SHA-1 fingerprint): {}",
                    Hex.encodeHexString(iaikEeCertificate.getFingerprintSHA()));
        }

        // retrieve certificate chain for eeCertificate
        X509Certificate[] caChainCertificates = retrieveChain(iaikEeCertificate, Objects.requireNonNull(settings));
        // build up full (sorted) chain including eeCertificate
        X509Certificate[] fullChainCertificates = Util.createCertificateChain(iaikEeCertificate,
                caChainCertificates);
        // add chain to certs list
        certs.addAll(Arrays.asList(fullChainCertificates));

        // determine revocation info, preferring OCSP
        // assume last certificate in chain is trust anchor
        OCSPClient ocspClient = OCSPClient.builder().setConnectTimeOutMillis(DEFAULT_CONNECTION_TIMEOUT_MS)
                .setSocketTimeOutMillis(DEFAULT_READ_TIMEOUT_MS).build();
        for (int i = 0; i < fullChainCertificates.length - 1; i++) {
            final X509Certificate subjectCertificate = fullChainCertificates[i];
            final X509Certificate issuerCertificate = fullChainCertificates[i + 1];
            OCSPResponse ocspResponse = null;
            if (OCSPClient.Util.hasOcspResponder(subjectCertificate)) {
                try {
                    ocspResponse = ocspClient.getOcspResponse(issuerCertificate, subjectCertificate);
                } catch (Exception e) {
                    log.info("Unable to retrieve OCSP response: {}", String.valueOf(e));
                }
            }

            if (ocspResponse != null) {

                ocsps.add(ocspResponse.getEncoded());

                // add ocsp signer certificate to certs
                // The currently used OCSP client support BasicOCSPResponse only, otherwise an exception would have been
                // thrown earlier. Therefore we can safely cast to BasicOCSPResponse here.
                X509Certificate ocspSignerCertificate = ((BasicOCSPResponse) ocspResponse.getResponse())
                        .getSignerCertificate();
                certs.add(ocspSignerCertificate);

            } else {

                // fall back to CRL

                CRLDistributionPoints cRLDistributionPoints;
                try {
                    cRLDistributionPoints = (CRLDistributionPoints) subjectCertificate
                            .getExtension(CRLDistributionPoints.oid);
                } catch (X509ExtensionInitException e) {
                    throw new IllegalStateException("Unable to initialize extension CRLDistributionPoints.", e);
                }
                X509CRL x509Crl = null;
                if (cRLDistributionPoints != null) {
                    if (log.isDebugEnabled()) {
                        log.debug("Retrieving CRL revocation info for: {}", subjectCertificate.getSubjectDN());
                    } else if (log.isInfoEnabled()) {
                        log.info("Retrieving CRL revocation info for certificate (SHA-1 fingerprint): {}",
                                Hex.encodeHexString(subjectCertificate.getFingerprintSHA()));
                    }

                    Exception lastException = null;
                    @SuppressWarnings("unchecked")
                    Enumeration<DistributionPoint> e = cRLDistributionPoints.getDistributionPoints();
                    while (e.hasMoreElements() && x509Crl == null) {
                        DistributionPoint distributionPoint = e.nextElement();

                        // inspect distribution point
                        if (distributionPoint.containsUriDpName()) {

                            String[] distributionPointNameURIs = distributionPoint.getDistributionPointNameURIs();
                            for (String distributionPointNameURI : distributionPointNameURIs) {
                                URL url;
                                try {
                                    log.debug("Trying to download crl from distribution point: {}",
                                            distributionPointNameURI);
                                    if (distributionPointNameURI.toLowerCase().startsWith("ldap://")) {
                                        url = new URL(null, distributionPointNameURI,
                                                new iaik.x509.net.ldap.Handler());
                                    } else {
                                        url = new URL(distributionPointNameURI);
                                    }
                                    URLConnection urlConnection = url.openConnection();
                                    urlConnection.setConnectTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
                                    urlConnection.setReadTimeout(DEFAULT_READ_TIMEOUT_MS);
                                    try (InputStream in = urlConnection.getInputStream()) {
                                        x509Crl = new X509CRL(in);
                                        // we got crl, exit loop
                                        break;
                                    } catch (CRLException e1) {
                                        lastException = e1;
                                        log.debug("Unable to parse CRL read from distribution point: {} ({})",
                                                distributionPointNameURI, e1.getMessage());
                                    }
                                } catch (MalformedURLException e1) {
                                    log.debug("Unsupported CRL distribution point uri: {} ({})",
                                            distributionPointNameURI, e1.getMessage());
                                    lastException = e1;
                                } catch (IOException e1) {
                                    log.debug("Error reading from CRL distribution point uri: {} ({})",
                                            distributionPointNameURI, e1.getMessage());
                                    lastException = e1;
                                } catch (Exception e1) {
                                    log.debug("Unknown error reading from CRL distribution point uri: {} ({})",
                                            distributionPointNameURI, e1.getMessage());
                                    lastException = e1;
                                }
                            }

                        }

                    }
                    if (x509Crl != null) {
                        crls.add(x509Crl);
                    } else if (lastException != null) {
                        log.info("Unable to load CRL: {}", String.valueOf(lastException));
                    }
                }

            }

        }
        sw.stop();
        log.debug("Querying certificate validation info took: {}ms", sw.getTime());

        return new CertificateVerificationData() {

            @Override
            public List<byte[]> getEncodedOCSPResponses() {
                return ocsps;
            }

            @Override
            public Set<java.security.cert.X509Certificate> getChainCerts() {
                return certs;
            }

            @Override
            public Set<java.security.cert.X509CRL> getCRLs() {
                return crls;
            }
        };
    }

    /**
     * Returns the signing certificate's authority key identifier entry (if any).
     * 
     * @param signingCertificate
     *            The signing certificate (required; must not be {@code null}).
     * @return A hex string (lowercase) representing the authority key identifier (or {@code null} in case the
     *         certificate does not have this extension or the extension could not be initialized).
     */
    private String getAuthorityKeyIdentifierHexString(X509Certificate signingCertificate) {

        AuthorityKeyIdentifier aki;
        try {
            aki = (AuthorityKeyIdentifier) signingCertificate.getExtension(AuthorityKeyIdentifier.oid);
        } catch (X509ExtensionInitException e) {
            // go a defensive way, do not throw exception
            log.warn("Unable to initialize X.509 authority key identifier extension.", e);
            return null;
        }
        if (aki != null) {
            return Hex.encodeHexString(aki.getKeyIdentifier());
        }
        return null;
    }

    /**
     * Looks for an certificate chain from configuration folder chain suitable for the provided certificate.
     * 
     * @param certificate
     *            The (end entity) certificate (the signing certificate actually) (required; must not be {@code null}).
     * @param settings
     *            The configuration of the PDF-AS environment (required; must not be {@code null}).
     * @return A readable URL reflecting the a PKCS7 resource or {@code null}.
     */
    private File findChainFile(X509Certificate certificate, ISettings settings) {
        final String issuerSkiHex = getAuthorityKeyIdentifierHexString(certificate);
        final String subjectFingerprintHex = Hex.encodeHexString(certificate.getFingerprintSHA());

        if (issuerSkiHex != null) {
            log.debug(
                    "Looking for certificate chain file for certificate with SHA-1 fingerprint {} (issuer subject key identifier {}).",
                    subjectFingerprintHex, issuerSkiHex);
            File certChainDirectory = new File(settings.getWorkingDirectory(), DEFAULT_CHAINSTORE_FOLDER_NAME);
            if (certChainDirectory.exists() && certChainDirectory.isDirectory()) {
                File certChainFile = new File(certChainDirectory, "certchain-ski" + issuerSkiHex + ".p7b");
                if (certChainFile.exists()) {
                    if (certChainFile.canRead()) {
                        log.debug("Found chain file for certificate (SHA-1 fingerprint {}): {}",
                                subjectFingerprintHex, certChainFile.getAbsolutePath());
                        return certChainFile;
                    } else {
                        log.warn(
                                "Found chain file for certificate (SHA-1 fingerprint {}), but file is not readable: {}",
                                subjectFingerprintHex, certChainFile.getAbsolutePath());
                    }
                } else {
                    log.debug("No chain file for certificate (SHA-1 fingerprint {}) provided by configuration: {}",
                            subjectFingerprintHex, certChainFile.getAbsolutePath());
                }
            } else {
                log.trace("Certificate chain folder does not exist: {}", certChainDirectory.getAbsolutePath());
            }
        } else {
            log.warn("Unable to determine authority key identifier of certificate with SHA-1 fingerprint {}.",
                    subjectFingerprintHex);
        }
        return null;
    }

    /**
     * Retrieves the chain for a provided end entity certificate.
     * 
     * @param eeCertificate
     *            The end entity certificate.
     * @param settings
     *            The configuration of the PDF-AS environment (required; must not be {@code null}).
     * @return The CA chain (never {@code null}).
     * @throws IOException
     *             Thrown in case the chain could not be read.
     * @throws CertificateException
     *             Thrown in case of an error parsing the chain.
     * @throws IllegalStateException
     *             In case the {@code eeCertificate}'s chain is not supported. Use
     *             {@link #isSupportedCA(X509Certificate)} in order to assure the CA is supported before calling this
     *             method).
     */
    private X509Certificate[] retrieveChain(X509Certificate eeCertificate, ISettings settings)
            throws IOException, CertificateException {

        File certChainFile = findChainFile(eeCertificate, settings);
        if (certChainFile == null) {
            throw new IllegalStateException("Unsupported CA.");
        }

        // load certificate chain
        try (InputStream certChainIn = new FileInputStream(certChainFile)) {
            Collection<? extends Certificate> certificates;
            try {
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); // not guaranteed to be thread-safe
                certificates = certificateFactory.generateCertificates(certChainIn);
            } catch (CertificateException e) {
                // should never occur (therefore not mentioned in javadoc)
                throw new IllegalStateException("X.509 certificates not supported.");
            }
            return Util.convertCertificateChain(certificates.toArray(new Certificate[certificates.size()]));
        }
    }

    /**
     * Converts a "Java" X.509 certificate to "IAIK" X.509 certificate. May return the very same {@code eeCertificate}
     * in case it is already a IAIK certificate.
     * 
     * @param certificate
     *            The certificate (required; must not be {@code null}).
     * @return The IAIK certificate (never {@code null}).
     */
    private X509Certificate toIAIKX509Certificate(java.security.cert.X509Certificate certificate) {
        if (Objects.requireNonNull(certificate) instanceof X509Certificate) {
            return (X509Certificate) certificate;
        } else {
            try {
                return new X509Certificate(certificate.getEncoded());
            } catch (CertificateException e) {
                throw new IllegalStateException("Unable to encode/decode certificate.", e);
            }
        }
    }

}