org.ejbca.core.protocol.ocsp.OCSPUnidClient.java Source code

Java tutorial

Introduction

Here is the source code for org.ejbca.core.protocol.ocsp.OCSPUnidClient.java

Source

/*************************************************************************
 *                                                                       *
 *  EJBCA Community: The OpenSource Certificate Authority                *
 *                                                                       *
 *  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.ejbca.core.protocol.ocsp;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERTaggedObject;
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.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
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.RespID;
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
import org.bouncycastle.cert.ocsp.jcajce.JcaRespID;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.BufferingContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.cesecore.certificates.ocsp.SHA1DigestCalculator;
import org.cesecore.keys.util.KeyTools;
import org.cesecore.util.Base64;
import org.cesecore.util.CertTools;
import org.cesecore.util.CryptoProviderTools;
import org.ejbca.core.protocol.ocsp.extension.unid.FnrFromUnidExtension;
import org.ejbca.core.protocol.ocsp.extension.unid.OCSPUnidResponse;

/** A simple OCSP lookup client used to query the OCSPUnidExtension. Attributes needed to call the client is a keystore
 * issued from the same CA as has issued the TLS server certificate of the OCSP/Lookup server.
 * The keystore must be a PKCS#12 file.
 * If a keystore is not used, regular OCSP requests can still be made, using normal http.
 * 
 * If requesting an Fnr and the fnr rturned is null, even though the OCSP code is good there can be several reasons:
 * 1.The client was not authorized to request an Fnr
 * 2.There was no Unid Fnr mapping available
 * 3.There was no Unid in the certificate (serialNumber DN component)
 *
 * @version $Id: OCSPUnidClient.java 20516 2015-01-08 17:26:01Z mikekushner $
 *
 */
public class OCSPUnidClient {

    final public static String requestDirectory = "ocspRequests";
    final private String httpReqPath;
    final private KeyStore ks;
    final private String passphrase;
    final private PrivateKey signKey;
    final private X509Certificate[] certChain;
    final private Extensions extensions;
    final private byte nonce[];
    private static final Logger m_log = Logger.getLogger(OCSPUnidClient.class);

    /**
     * @param keystore KeyStore client keystore used to authenticate TLS client authentication, or null if TLS is not used
     * @param pwd String password for the key store, or null if no keystore is used
     * @param ocspurl String url to the OCSP server, or null if we should try to use the AIA extension from the cert; e.g. http://127.0.0.1:8080/ejbca/publicweb/status/ocsp (or https for TLS)
     * @param certs certificate chain to signing key
     * @param _signKey signing key
     * @param getfnr true if FNR should be fetched
     * @throws NoSuchAlgorithmException
     * @throws IOException if ASN1 parsing error occurs
     */
    private OCSPUnidClient(KeyStore keystore, String pwd, String ocspurl, Certificate[] certs, PrivateKey _signKey,
            boolean getfnr) throws NoSuchAlgorithmException, IOException {
        this.httpReqPath = ocspurl;
        this.passphrase = pwd;
        this.ks = keystore;
        this.signKey = _signKey;
        this.certChain = certs != null ? Arrays.asList(certs).toArray(new X509Certificate[0]) : null;
        this.nonce = new byte[16];
        {
            List<Extension> extensionList = new ArrayList<Extension>();
            final Random randomSource = new Random();
            randomSource.nextBytes(nonce);
            extensionList
                    .add(new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, new DEROctetString(nonce)));
            // Don't bother adding Unid extension if we are not using client authentication
            if (getfnr) {
                extensionList.add(new Extension(FnrFromUnidExtension.FnrFromUnidOid, false,
                        new DEROctetString(new FnrFromUnidExtension("1"))));
            }
            extensions = new Extensions(extensionList.toArray(new Extension[extensionList.size()]));
        }
        CryptoProviderTools.installBCProviderIfNotAvailable();
    }

    /**
      * @param ksfilename String Filename of PKCS#12 keystore used to authenticate TLS client authentication, or null if TLS is not used
      * @param pwd String password for the key store,or null if no keystore is used 
      * @param ocspurl String url to the OCSP server, or null if we should try to use the AIA extension from the cert; e.g. http://127.0.0.1:8080/ejbca/publicweb/status/ocsp (or https for TLS)
     * @return the client to use
      * @throws Exception
     */
    public static OCSPUnidClient getOCSPUnidClient(String ksfilename, String pwd, String ocspurl,
            boolean doSignRequst, boolean getfnr) throws Exception {
        if (doSignRequst && ksfilename == null) {
            throw new Exception("You got to give the path name for a keystore to use when using signing.");
        }
        final KeyStore ks;
        if (ksfilename != null) {
            ks = KeyStore.getInstance("PKCS12", "BC");
            ks.load(new FileInputStream(ksfilename), pwd.toCharArray());
            Enumeration<String> en = ks.aliases();
            String alias = null;
            // If this alias is a trusted certificate entry, we don't want to fetch that, we want the key entry
            while ((alias == null || ks.isCertificateEntry(alias)) && en.hasMoreElements()) {
                alias = en.nextElement();
            }
            final Certificate[] certs = KeyTools.getCertChain(ks, alias);
            if (certs == null) {
                throw new IOException("Can not find a certificate entry in PKCS12 keystore for alias " + alias);
            }
            final PrivateKey privateKey = doSignRequst ? (PrivateKey) ks.getKey(alias, null) : null;
            return new OCSPUnidClient(ks, pwd, ocspurl, certs, privateKey, getfnr);
        } else {
            return new OCSPUnidClient(null, null, ocspurl, null, null, getfnr);
        }
    }

    /**
     * @param cert X509Certificate to query, the DN should contain serialNumber which is Unid to be looked up
     * @param cacert CA certificate that issued the certificate to be queried
      * @param useGet if true GET will be used instead of POST as HTTP method
     * @return OCSPUnidResponse conatining the response and the fnr, can contain and an error code and the fnr can be null, never returns null.
     * @throws OCSPException
     * @throws IOException
     * @throws GeneralSecurityException
     * @throws OperatorCreationException 
     * @throws IllegalArgumentException 
     */
    public OCSPUnidResponse lookup(Certificate cert, X509Certificate cacert, boolean useGet) throws OCSPException,
            IOException, GeneralSecurityException, IllegalArgumentException, OperatorCreationException {
        return lookup(CertTools.getSerialNumber(cert), cacert, useGet);
    }

    /**
     * @param serialNr serial number of the certificate to verify
     * @param cacert issuer of the certificate to verify
     * @param useGet if true GET will be used instead of POST as HTTP method
     * @return response can contain and an error code but the fnr is allways null, never returns null.
     * @throws OCSPException 
     * @throws IOException
     * @throws OperatorCreationException if Signer couldn't be created
     * @throws KeyStoreException 
     * @throws NoSuchAlgorithmException 
     * @throws CertificateException 
     * @throws KeyManagementException 
     * @throws UnrecoverableKeyException 
     */
    public OCSPUnidResponse lookup(BigInteger serialNr, X509Certificate cacert, boolean useGet)
            throws OCSPException, IOException, OperatorCreationException, UnrecoverableKeyException,
            KeyManagementException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
        if (this.httpReqPath == null) {
            // If we didn't pass a url to the constructor and the cert does not have the URL, we will fail...
            OCSPUnidResponse ret = new OCSPUnidResponse();
            ret.setErrorCode(OCSPUnidResponse.ERROR_NO_OCSP_URI);
            return ret;
        }
        final OCSPReqBuilder gen = new OCSPReqBuilder();
        final CertificateID certId = new JcaCertificateID(SHA1DigestCalculator.buildSha1Instance(),
                (X509Certificate) cacert, serialNr);
        gen.addRequest(certId);
        if (!useGet) {
            // Add a nonce to the request
            gen.setRequestExtensions(this.extensions);
        }
        final OCSPReq req;
        if (this.signKey != null) {
            final X509Certificate localCertChain[] = this.certChain != null ? this.certChain
                    : new X509Certificate[] { (X509Certificate) cacert };
            final JcaX509CertificateHolder[] certificateHolderChain = CertTools
                    .convertToX509CertificateHolder(localCertChain);
            gen.setRequestorName(certificateHolderChain[0].getSubject());
            req = gen.build(
                    new BufferingContentSigner(new JcaContentSignerBuilder("SHA1withRSA")
                            .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(this.signKey), 20480),
                    certificateHolderChain);
        } else {
            req = gen.build();
        }
        // write request if directory exists.
        File ocspReqDir = new File(requestDirectory);
        if (ocspReqDir.isDirectory()) {
            OutputStream os = new FileOutputStream(new File(ocspReqDir, serialNr.toString()));
            os.write(req.getEncoded());
            os.close();
        }
        // Send the request and receive a BasicResponse
        return sendOCSPRequest(req.getEncoded(), cacert, useGet);
    }

    //
    // Private helper methods
    //

    private OCSPUnidResponse sendOCSPRequest(byte[] ocspPackage, X509Certificate knownTrustAnchor, boolean useGet)
            throws IOException, OCSPException, OperatorCreationException, CertificateException,
            UnrecoverableKeyException, KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
        final HttpURLConnection con;
        if (useGet) {
            String b64 = new String(Base64.encode(ocspPackage, false));
            URL url = new URL(httpReqPath + '/' + b64);
            con = (HttpURLConnection) url.openConnection();
        } else {
            // POST the OCSP request
            URL url = new URL(httpReqPath);
            con = (HttpURLConnection) getUrlConnection(url);
            // we are going to do a POST
            con.setDoOutput(true);
            con.setRequestMethod("POST");
            // POST it
            con.setRequestProperty("Content-Type", "application/ocsp-request");
            OutputStream os = null;
            try {
                os = con.getOutputStream();
                os.write(ocspPackage);
            } finally {
                if (os != null) {
                    os.close();
                }
            }
        }
        final OCSPUnidResponse ret = new OCSPUnidResponse();
        ret.setHttpReturnCode(con.getResponseCode());
        if (ret.getHttpReturnCode() != 200) {
            if (ret.getHttpReturnCode() == 401) {
                ret.setErrorCode(OCSPUnidResponse.ERROR_UNAUTHORIZED);
            } else {
                ret.setErrorCode(OCSPUnidResponse.ERROR_UNKNOWN);
            }
            return ret;
        }
        final OCSPResp response;
        {
            final InputStream in = con.getInputStream();
            if (in != null) {
                try {
                    response = new OCSPResp(IOUtils.toByteArray(in));
                } finally {
                    in.close();
                }
            } else {
                response = null;
            }
        }
        if (response == null) {
            ret.setErrorCode(OCSPUnidResponse.ERROR_NO_RESPONSE);
            return ret;
        }
        ret.setResp(response);
        final BasicOCSPResp brep = (BasicOCSPResp) response.getResponseObject();
        if (brep == null) {
            ret.setErrorCode(OCSPUnidResponse.ERROR_NO_RESPONSE);
            return ret;
        }
        // Compare nonces to see if the server sent the same nonce as we sent
        final byte[] noncerep = brep.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce).getExtnValue()
                .getEncoded();
        if (noncerep != null) {
            ASN1InputStream ain = new ASN1InputStream(noncerep);
            ASN1OctetString oct = ASN1OctetString.getInstance(ain.readObject());
            ain.close();
            boolean eq = ArrayUtils.isEquals(this.nonce, oct.getOctets());
            if (!eq) {
                ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_NONCE);
                return ret;
            }
        }

        final RespID id = brep.getResponderId();
        final DERTaggedObject to = (DERTaggedObject) id.toASN1Object().toASN1Primitive();
        final RespID respId;
        final X509CertificateHolder[] chain = brep.getCerts();
        JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
        X509Certificate signerCertificate = converter.getCertificate(chain[0]);
        final PublicKey signerPub = signerCertificate.getPublicKey();
        if (to.getTagNo() == 1) {
            // This is Name
            respId = new JcaRespID(signerCertificate.getSubjectX500Principal());
        } else {
            // This is KeyHash
            respId = new JcaRespID(signerPub, SHA1DigestCalculator.buildSha1Instance());
        }
        if (!id.equals(respId)) {
            // Response responderId does not match signer certificate responderId!
            ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_SIGNERID);
        }
        if (!brep.isSignatureValid(new JcaContentVerifierProviderBuilder().build(signerPub))) {
            ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_SIGNATURE);
            return ret;
        }

        /* 
         * Okay, at this point we have three different variables and six different possible valid use cases. These
         * variables are:
         *          1. If the OCSP reply is from a CA (integrated) or an OCSP responder (standalone) 
         *          2. If it was from a CA, then if that CA is self signed or a subCA
         *          3. If the server (in the integrated case) or keybinding (standalone case) was set to include the certificate chain
         */

        //If we have a chain, verify it
        if (chain.length > 1) {
            // end at one shortof chain.length, because the root certificate is (usually) not included in the OCSP response
            // TODO: improve this when we can pass in the root cert from parameter to properly validate the whole chain
            for (int i = 0; i + 1 < chain.length; i++) {
                final X509Certificate cert1 = converter.getCertificate(chain[i]);
                final X509Certificate cert2 = converter.getCertificate(chain[Math.min(i + 1, chain.length - 1)]);
                try {
                    cert1.verify(cert2.getPublicKey());
                } catch (GeneralSecurityException e) {
                    m_log.info("Verifying problem with", e);
                    m_log.info("Certificate to be verified: " + cert1);
                    m_log.info("Verifying certificate: " + cert2);
                    ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_SIGNERCERT);
                    return ret;
                }
            }
        }

        if (CertTools.isCA(signerCertificate)) {
            //Verify that the signer certificate was the same as the trust anchor
            if (!signerCertificate.getSerialNumber().equals(knownTrustAnchor.getSerialNumber())) {
                m_log.info("Signing certificate for integrated OCSP was not the provided trust anchor.");
                ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_SIGNERCERT);
                return ret;
            }
        } else if (CertTools.isOCSPCert(signerCertificate)) {
            //If an OCSP certificate was used to sign
            try {
                signerCertificate.verify(knownTrustAnchor.getPublicKey());
            } catch (GeneralSecurityException e) {
                m_log.info("Signing certificate was not signed by known trust anchor.");
                ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_SIGNERCERT);
                return ret;
            }
        } else {
            m_log.info("Signing certificate was not an OCSP certificate.");
            ret.setErrorCode(OCSPUnidResponse.ERROR_INVALID_SIGNERCERT);
            return ret;
        }

        String fnr = getFnr(brep);
        if (fnr != null) {
            ret.setFnr(fnr);
        }
        return ret;
    }

    private String getFnr(BasicOCSPResp brep) throws IOException {
        Extension fnrrep = brep.getExtension(FnrFromUnidExtension.FnrFromUnidOid);
        if (fnrrep == null) {
            return null;
        }
        ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(fnrrep.getExtnValue().getEncoded()));
        ASN1OctetString octs = (ASN1OctetString) aIn.readObject();
        aIn = new ASN1InputStream(new ByteArrayInputStream(octs.getOctets()));
        FnrFromUnidExtension fnrobj = FnrFromUnidExtension.getInstance(aIn.readObject());
        return fnrobj.getFnr();
    }

    private SSLSocketFactory getSSLFactory() throws IOException, NoSuchAlgorithmException,
            UnrecoverableKeyException, KeyStoreException, CertificateException, KeyManagementException {

        final KeyManager km[];
        final TrustManager tm[];

        // Put the key and certs in the user keystore (if available)
        if (this.ks != null) {
            final KeyManagerFactory kmf;
            kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(this.ks, this.passphrase.toCharArray());
            km = kmf.getKeyManagers();
        } else {
            km = null;
        }
        // Now make a truststore to verify the server
        if (this.certChain != null && this.certChain.length > 0) {
            final KeyStore trustks = KeyStore.getInstance("jks");
            trustks.load(null, "foo123".toCharArray());
            // add trusted CA cert
            trustks.setCertificateEntry("trusted", this.certChain[this.certChain.length - 1]);
            final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(trustks);
            tm = tmf.getTrustManagers();
        } else {
            tm = null;
        }
        if (km == null && tm == null) {
            return (SSLSocketFactory) SSLSocketFactory.getDefault();
        }
        final SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(km, tm, null);

        return ctx.getSocketFactory();
    }

    /**
     * 
     * @param url
     * @return URLConnection
     * @throws IOException
     * @throws CertificateException 
     * @throws KeyStoreException 
     * @throws NoSuchAlgorithmException 
     * @throws UnrecoverableKeyException 
     * @throws KeyManagementException 
     */
    private URLConnection getUrlConnection(URL url) throws IOException, UnrecoverableKeyException,
            NoSuchAlgorithmException, KeyStoreException, CertificateException, KeyManagementException {
        final URLConnection orgcon = url.openConnection();
        if (orgcon instanceof HttpsURLConnection) {
            HttpsURLConnection con = (HttpsURLConnection) orgcon;
            con.setHostnameVerifier(new SimpleVerifier());
            con.setSSLSocketFactory(getSSLFactory());
        }
        return orgcon;
    }

    class SimpleVerifier implements HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

}