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

org.cesecore.util.PKIXCertRevocationStatusChecker Maven / Gradle / Ivy

/*************************************************************************
 *                                                                       *
 *  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: PKIXCertRevocationStatusChecker.java 24567 2016-10-25 05:36:17Z anatom $
 *
 */
public class PKIXCertRevocationStatusChecker extends PKIXCertPathChecker {

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

    
    private String ocspUrl;
    private String crlUrl;
    private X509Certificate issuerCert;
    private Collection 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  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  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 getSupportedExtensions() {
        ArrayList exts = new ArrayList();
        exts.add(Extension.cRLDistributionPoints.getId());
        exts.add(Extension.authorityInfoAccess.getId());
        return new HashSet(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 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 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 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 getOcspUrls(Certificate cert) {
        ArrayList urls = new ArrayList();
        if(StringUtils.isNotEmpty(this.ocspUrl)) {
            urls.add(this.ocspUrl);
        }
        
        urls.addAll(CertTools.getAuthorityInformationAccessOcspUrls((X509Certificate)cert));
        
        return urls;
    }
    
    private ArrayList getCrlUrl(final Certificate cert) {
        
        ArrayList urls = new ArrayList();
        
        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 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;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy