org.cesecore.util.PKIXCertRevocationStatusChecker.java Source code

Java tutorial

Introduction

Here is the source code for org.cesecore.util.PKIXCertRevocationStatusChecker.java

Source

/*************************************************************************
 *                                                                       *
 *  CESeCore: CE Security Core                                           *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/
package org.cesecore.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CRL;
import java.security.cert.CRLException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.cesecore.certificates.ocsp.SHA1DigestCalculator;

/**
 * A class to check whether a certificate is revoked or not using either OCSP or CRL. 
 * The revocation status will first be obtained using OCSP. If it turned out that that was not possible for 
 * some reason, a CRL will be used instead. If it was not possible to check the CRL for some reason, an 
 * exception will be thrown.
 * 
 * @version $Id$
 *
 */
public class PKIXCertRevocationStatusChecker extends PKIXCertPathChecker {

    private final Logger log = Logger.getLogger(PKIXCertRevocationStatusChecker.class);

    private String ocspUrl;
    private String crlUrl;
    private X509Certificate issuerCert;
    private Collection<X509Certificate> caCerts;

    private SingleResp ocspResponse = null;
    private CRL crl = null;

    /**
     * With this constructor, the certificate revocation status will be checked using a CRL fetched using a URL 
     * extracted from the certificate's CRL Distribution Points extension.
     * 
     * @param issuerCert The certificate of the issuer of the certificate whose revocation status is to be checked. 
     * if 'null', the issuer certificate will be looked for among the certificates specified in 'cacerts'
     * @param cacerts A collection of certificates where one of them is the certificate of the issuer of the certificate 
     * whose status is to be checked. This parameter will be used only if 'issuerCert' is null
        
     */
    public PKIXCertRevocationStatusChecker(final X509Certificate issuerCert,
            final Collection<X509Certificate> cacerts) {
        this.ocspUrl = null;
        this.crlUrl = null;
        this.issuerCert = issuerCert;
        this.caCerts = cacerts;
    }

    /**
     * @param ocspurl The URL to use when sending an OCSP request. If 'null', the OCSP URL will be extracted from 
     * the certificate's AuthorityInformationAccess extension if it exists.
     * @param crlurl The URL to fetch the CRL from. If 'null', the CRL URL will be extracted from the certificate's 
     * CRLDistributionPoints extension if exists.
     * @param issuerCert The certificate of the issuer of the certificate whose revocation status is to be checked. 
     * if 'null', the issuer certificate will be looked for among the certificates specified in 'cacerts'
     * @param cacerts A collection of certificates where one of them is the certificate of the issuer of the certificate 
     * whose status is to be checked. This parameter will be used only if 'issuerCert' is null
     */
    public PKIXCertRevocationStatusChecker(final String ocspurl, final String crlurl,
            final X509Certificate issuerCert, final Collection<X509Certificate> cacerts) {
        this.ocspUrl = ocspurl;
        this.crlUrl = crlurl;
        this.issuerCert = issuerCert;
        this.caCerts = cacerts;
    }

    @Override
    public void init(boolean forward) throws CertPathValidatorException {
    }

    @Override
    public boolean isForwardCheckingSupported() {
        // Not used
        return true;
    }

    @Override
    public Set<String> getSupportedExtensions() {
        ArrayList<String> exts = new ArrayList<String>();
        exts.add(Extension.cRLDistributionPoints.getId());
        exts.add(Extension.authorityInfoAccess.getId());
        return new HashSet<String>(exts);
    }

    /**
     * @return The OCSP response containing the certificate status of the saught out certificate. Or 'null' if an OCSP response 
     * could not be obtained for any reason.
     */
    public SingleResp getOCSPResponse() {
        return this.ocspResponse;
    }

    /**
     * @return The CRLs that were checked. Or an empty Collection if no CRLs were checked 
     */
    public CRL getcrl() {
        return this.crl;
    }

    /**
     * Resets the OCSP response, the checked CRLs and whether the certificate was revoked in case the same instance of this class is 
     * used to check the revocation status of more that one certificate.
     */
    private void clearResult() {
        this.ocspResponse = null;
        this.crl = null;
    }

    /**
     * Checks the revocation status of 'cert'; first by sending on OCSP request. If that fails for any reason, then through a CRL
     */
    @Override
    public void check(Certificate cert, Collection<String> unresolvedCritExts) throws CertPathValidatorException {

        clearResult();
        Certificate cacert = getCaCert(cert);
        if (cacert == null) {
            final String msg = "No issuer CA certificate was found. An issuer CA certificate is needed to create an OCSP request and to get the right CRL";
            log.info(msg);
            throw new CertPathValidatorException(msg);
        }

        ArrayList<String> ocspurls = getOcspUrls(cert);
        if (!ocspurls.isEmpty()) {
            BigInteger certSerialnumber = CertTools.getSerialNumber(cert);
            byte[] nonce = new byte[16];
            final Random randomSource = new Random();
            randomSource.nextBytes(nonce);
            OCSPReq req = null;
            try {
                req = getOcspRequest(cacert, certSerialnumber, nonce);
            } catch (CertificateEncodingException | OCSPException e) {
                if (log.isDebugEnabled()) {
                    log.debug("Failed to create OCSP request. " + e.getLocalizedMessage());
                }
                fallBackToCrl(cert, CertTools.getSubjectDN(cacert));
                return;

            }

            SingleResp ocspResp = null;
            for (String url : ocspurls) {
                ocspResp = getOCSPResponse(url, req, cert, nonce, OCSPRespBuilder.SUCCESSFUL, 200);
                if (ocspResp != null) {
                    log.info("Obtained OCSP response from " + url);
                    break;
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("Failed to obtain an OCSP reponse from " + url);
                    }
                }
            }

            if (ocspResp == null) {
                log.info(
                        "Failed to check certificate revocation status using OCSP. Falling back to check using CRL");
                fallBackToCrl(cert, CertTools.getSubjectDN(cacert));
            } else {
                CertificateStatus status = ocspResp.getCertStatus();
                this.ocspResponse = ocspResp;
                if (log.isDebugEnabled()) {
                    log.debug("The certificate status is: " + (status == null ? "Good" : status.toString()));
                }
                if (status != null) { // status==null -> certificate OK
                    throw new CertPathValidatorException("Certificate with serialnumber "
                            + CertTools.getSerialNumberAsString(cert) + " was revoked");
                }

                if (unresolvedCritExts != null) {
                    unresolvedCritExts.remove(Extension.authorityInfoAccess.getId());
                }
            }

        } else {
            fallBackToCrl(cert, CertTools.getSubjectDN(cacert));

            if (unresolvedCritExts != null) {
                unresolvedCritExts.remove(Extension.cRLDistributionPoints.getId());
            }
        }

    }

    /**
     * Check the revocation status of 'cert' using a CRL
     * @param cert the certificate whose revocation status is to be checked
     * @throws CertPathValidatorException
     */
    private void fallBackToCrl(final Certificate cert, final String issuerDN) throws CertPathValidatorException {
        final ArrayList<URL> crlUrls = getCrlUrl(cert);
        if (crlUrls.isEmpty()) {
            final String errmsg = "Failed to verify certificate status using the fallback CRL method. Could not find a CRL URL";
            log.info(errmsg);
            throw new CertPathValidatorException(errmsg);
        }
        if (log.isDebugEnabled()) {
            log.debug("Found " + crlUrls.size() + " CRL URLs");
        }

        CRL crl = null;
        for (URL url : crlUrls) {
            crl = getCRL(url);
            if (crl != null) {
                if (isCorrectCRL(crl, issuerDN)) {
                    final boolean isRevoked = crl.isRevoked(cert);
                    this.crl = crl;
                    if (isRevoked) {
                        throw new CertPathValidatorException("Certificate with serialnumber "
                                + CertTools.getSerialNumberAsString(cert) + " was revoked");
                    }
                    break;
                }
            }
        }
        if (this.crl == null) {
            throw new CertPathValidatorException(
                    "Failed to verify certificate status using CRL. Could not find a CRL issued by " + issuerDN
                            + " reasonably lately");
        }
    }

    private boolean isCorrectCRL(final CRL crl, final String issuerDN) {
        if (!(crl instanceof X509CRL)) {
            return false;
        }

        X509CRL x509crl = (X509CRL) crl;
        if (!StringUtils.equals(issuerDN, CertTools.getIssuerDN(x509crl))) {
            return false;
        }

        final Date now = new Date(System.currentTimeMillis());
        final Date nextUpdate = x509crl.getNextUpdate();
        if (nextUpdate != null) {
            if (nextUpdate.after(now)) {
                return true;
            }

            if (log.isDebugEnabled()) {
                log.debug("CRL issued by " + issuerDN + " is out of date");
            }
            return false;
        }

        final Date thisUpdate = x509crl.getThisUpdate();
        if (thisUpdate != null) {
            final GregorianCalendar gc = new GregorianCalendar();
            gc.setTime(now);
            gc.add(Calendar.HOUR, 1);
            final Date expire = gc.getTime();

            if (expire.before(now)) {
                if (log.isDebugEnabled()) {
                    log.debug("Could not find when CRL issued by " + issuerDN
                            + " should be updated and this CRL is over one hour old. Not using it");
                }
                return false;
            }

            log.warn("Could not find when CRL issued by " + issuerDN
                    + " should be updated, but this CRL was issued less than an hour ago, so we are using it");
            return true;
        }

        if (log.isDebugEnabled()) {
            log.debug("Could not check issuance time for CRL issued by " + issuerDN);
        }
        return false;
    }

    private CRL getCRL(final URL url) {
        CRL crl = null;
        try {
            final URLConnection con = url.openConnection();
            final InputStream is = con.getInputStream();
            final CertificateFactory cf = CertificateFactory.getInstance("X.509");
            crl = cf.generateCRL(is);
            is.close();
            log.info("Downloaded CRL from " + url);
        } catch (IOException | CertificateException | CRLException e) {
            if (log.isDebugEnabled()) {
                log.debug("Fetching CRL from " + url.toString() + " failed. " + e.getLocalizedMessage());
            }
        }
        return crl;
    }

    /**
     * Construct an OCSP request
     * @param cacert The certificate of the issuer of the certificate to be checked
     * @param certSerialnumber the serialnumber of the certificate to be checked
     * @param nonce random nonce to be included in the OCSP request (OCSP POST)
     * @return OCSPReq
     * @throws CertificateEncodingException
     * @throws OCSPException
     */
    private OCSPReq getOcspRequest(Certificate cacert, BigInteger certSerialnumber, final byte[] nonce)
            throws CertificateEncodingException, OCSPException {
        OCSPReqBuilder gen = new OCSPReqBuilder();
        gen.addRequest(new JcaCertificateID(SHA1DigestCalculator.buildSha1Instance(), (X509Certificate) cacert,
                certSerialnumber));

        Extension[] extensions = new Extension[1];
        extensions[0] = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, new DEROctetString(nonce));
        gen.setRequestExtensions(new Extensions(extensions));

        return gen.build();
    }

    /**
     * Sends an OCSP request, gets a response and verifies the response as much as possible before returning it to the caller.
     * 
     * @return The OCSP response, or null of no correct response could be obtained.
     */
    private SingleResp getOCSPResponse(final String ocspurl, final OCSPReq ocspRequest, final Certificate cert,
            final byte[] nonce, int expectedOcspRespCode, int expectedHttpRespCode) {
        if (log.isDebugEnabled()) {
            log.debug("Sending OCSP request to " + ocspurl + " regarding certificate with SubjectDN: "
                    + CertTools.getSubjectDN(cert) + " - IssuerDN: " + CertTools.getIssuerDN(cert));
        }

        //----------------------- Open connection and send the request --------------//
        OCSPResp response = null;
        HttpURLConnection con = null;
        try {
            final URL url = new URL(ocspurl);
            con = (HttpURLConnection) url.openConnection();
            // we are going to do a POST
            con.setDoOutput(true);
            con.setRequestMethod("POST");

            // POST it
            con.setRequestProperty("Content-Type", "application/ocsp-request");
            OutputStream os = con.getOutputStream();
            os.write(ocspRequest.getEncoded());
            os.close();

            final int httpRespCode = ((HttpURLConnection) con).getResponseCode();
            if (httpRespCode != expectedHttpRespCode) {
                log.info("HTTP response from OCSP request was " + httpRespCode + ". Expected "
                        + expectedHttpRespCode);
                handleContentOfErrorStream(con.getErrorStream());
                return null; // if it is an http error code we don't need to test any more
            }

            InputStream is = con.getInputStream();
            response = new OCSPResp(IOUtils.toByteArray(is));
            is.close();

        } catch (IOException e) {
            log.info("Unable to get an OCSP response. " + e.getLocalizedMessage());
            if (con != null) {
                handleContentOfErrorStream(con.getErrorStream());
            }
            return null;
        }

        // ------------ Verify the response signature --------------//
        BasicOCSPResp brep = null;
        try {
            brep = (BasicOCSPResp) response.getResponseObject();

            if ((expectedOcspRespCode != OCSPRespBuilder.SUCCESSFUL) && (brep != null)) {
                log.warn("According to RFC 2560, responseBytes are not set on error, but we got some.");
                return null; // it messes up testing of invalid signatures... but is needed for the unsuccessful responses
            }

            if (brep == null) {
                log.warn("Cannot extract OCSP response object. OCSP response status: " + response.getStatus());
                return null;
            }

            X509CertificateHolder[] chain = brep.getCerts();
            boolean verify = brep.isSignatureValid(new JcaContentVerifierProviderBuilder()
                    .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(chain[0]));
            if (!verify) {
                log.warn("OCSP response signature was not valid");
                return null;
            }
        } catch (OCSPException | OperatorCreationException | CertificateException e) {
            if (log.isDebugEnabled()) {
                log.debug("Failed to obtain or verify OCSP response. " + e.getLocalizedMessage());
            }
            return null;
        }

        // ------------- Verify the nonce ---------------//
        byte[] noncerep;
        try {
            noncerep = brep.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce).getExtnValue().getEncoded();
        } catch (IOException e) {
            if (log.isDebugEnabled()) {
                log.debug("Failed to read extension from OCSP response. " + e.getLocalizedMessage());
            }
            return null;
        }
        if (noncerep == null) {
            log.warn("Sent an OCSP request containing a nonce, but the OCSP response does not contain a nonce");
            return null;
        }

        try {
            ASN1InputStream ain = new ASN1InputStream(noncerep);
            ASN1OctetString oct = ASN1OctetString.getInstance(ain.readObject());
            ain.close();
            if (!Arrays.equals(nonce, oct.getOctets())) {
                log.warn("The nonce in the OCSP request and the OCSP response do not match");
                return null;
            }
        } catch (IOException e) {
            if (log.isDebugEnabled()) {
                log.debug("Failed to read extension from OCSP response. " + e.getLocalizedMessage());
            }
            return null;
        }

        // ------------ Extract the single response and verify that it concerns a cert with the right serialnumber ----//
        SingleResp[] singleResps = brep.getResponses();
        if ((singleResps == null) || (singleResps.length == 0)) {
            if (log.isDebugEnabled()) {
                log.debug("The OCSP response object contained no responses.");
            }
            return null;
        }

        SingleResp singleResponse = singleResps[0];
        CertificateID certId = singleResponse.getCertID();
        if (!certId.getSerialNumber().equals(CertTools.getSerialNumber(cert))) {
            if (log.isDebugEnabled()) {
                log.debug(
                        "Certificate serialnumber in response does not match certificate serialnumber in request.");
            }
            return null;
        }

        // ------------ Return the single response ---------------//
        return singleResponse;
    }

    /**
     * Reads the content of 'httpErrorStream' and ignores it. 
     */
    private void handleContentOfErrorStream(final InputStream httpErrorStream) {
        if (httpErrorStream != null) {
            try {
                OutputStream os = new NullOutputStream();
                IOUtils.copy(httpErrorStream, os);
                httpErrorStream.close();
                os.close();
            } catch (IOException ex) {
            }
        }
    }

    private ArrayList<String> getOcspUrls(Certificate cert) {
        ArrayList<String> urls = new ArrayList<String>();
        if (StringUtils.isNotEmpty(this.ocspUrl)) {
            urls.add(this.ocspUrl);
        }

        urls.addAll(CertTools.getAuthorityInformationAccessOcspUrls((X509Certificate) cert));

        return urls;
    }

    private ArrayList<URL> getCrlUrl(final Certificate cert) {

        ArrayList<URL> urls = new ArrayList<URL>();

        if (StringUtils.isNotEmpty(this.crlUrl)) {
            try {
                urls.add(new URL(this.crlUrl));
            } catch (MalformedURLException e) {
                if (log.isDebugEnabled()) {
                    log.debug("Failed to parse '" + this.crlUrl + "' as a URL. " + e.getLocalizedMessage());
                }
            }
        }

        Collection<URL> crlUrlFromExtension = CertTools.getCrlDistributionPoints((X509Certificate) cert);
        urls.addAll(crlUrlFromExtension);

        return urls;
    }

    private X509Certificate getCaCert(final Certificate targetCert) {
        if (this.issuerCert != null) {
            return issuerCert;
        }

        if (this.caCerts == null) { // no CA specified
            return null;
        }

        for (final X509Certificate cacert : this.caCerts) {
            if (isIssuerCA(targetCert, cacert)) {
                return cacert;
            }
        }
        return null;
    }

    private boolean isIssuerCA(final Certificate cert, final Certificate cacert) {
        if (!StringUtils.equals(CertTools.getIssuerDN(cert), CertTools.getSubjectDN(cacert))) {
            return false;
        }
        try {
            cert.verify(cacert.getPublicKey());
            return true;
        } catch (InvalidKeyException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException
                | SignatureException e) {
            return false;
        }
    }

}