/*
 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.testlibrary;

import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.CRLReason;
import java.security.cert.X509Certificate;
import java.security.cert.Extension;
import java.security.cert.CertificateException;
import java.security.cert.CertificateEncodingException;
import java.security.Signature;
import java.util.*;
import java.util.concurrent.*;
import java.text.SimpleDateFormat;
import java.math.BigInteger;

import sun.security.x509.*;
import sun.security.x509.PKIXExtensions;
import sun.security.provider.certpath.ResponderId;
import sun.security.provider.certpath.CertId;
import sun.security.provider.certpath.OCSPResponse;
import sun.security.provider.certpath.OCSPResponse.ResponseStatus;
import sun.security.util.Debug;
import sun.security.util.DerInputStream;
import sun.security.util.DerOutputStream;
import sun.security.util.DerValue;
import sun.security.util.ObjectIdentifier;


/**
 * This is a simple OCSP server designed to listen and respond to incoming
 * requests.
 */
public class SimpleOCSPServer {
    private final Debug debug = Debug.getInstance("oserv");
    private static final ObjectIdentifier OCSP_BASIC_RESPONSE_OID =
            ObjectIdentifier.newInternal(
                    new int[] { 1, 3, 6, 1, 5, 5, 7, 48, 1, 1});
    private static final SimpleDateFormat utcDateFmt =
            new SimpleDateFormat("MMM dd yyyy, HH:mm:ss z");

    static final int FREE_PORT = 0;

    // CertStatus values
    public static enum CertStatus {
        CERT_STATUS_GOOD,
        CERT_STATUS_REVOKED,
        CERT_STATUS_UNKNOWN,
    }

    // Fields used for the networking portion of the responder
    private ServerSocket servSocket;
    private InetAddress listenAddress;
    private int listenPort;

    // Keystore information (certs, keys, etc.)
    private KeyStore keystore;
    private X509Certificate issuerCert;
    private X509Certificate signerCert;
    private PrivateKey signerKey;

    // Fields used for the operational portions of the server
    private boolean logEnabled = false;
    private ExecutorService threadPool;
    private volatile boolean started = false;
    private volatile boolean serverReady = false;
    private volatile boolean receivedShutdown = false;
    private volatile boolean acceptConnections = true;
    private volatile long delayMsec = 0;

    // Fields used in the generation of responses
    private long nextUpdateInterval = -1;
    private Date nextUpdate = null;
    private ResponderId respId;
    private AlgorithmId sigAlgId;
    private Map<CertId, CertStatusInfo> statusDb =
            Collections.synchronizedMap(new HashMap<>());

    /**
     * Construct a SimpleOCSPServer using keystore, password, and alias
     * parameters.
     *
     * @param ks the keystore to be used
     * @param password the password to access key material in the keystore
     * @param issuerAlias the alias of the issuer certificate
     * @param signerAlias the alias of the signer certificate and key.  A
     * value of {@code null} means that the {@code issuerAlias} will be used
     * to look up the signer key.
     *
     * @throws GeneralSecurityException if there are problems accessing the
     * keystore or finding objects within the keystore.
     * @throws IOException if a {@code ResponderId} cannot be generated from
     * the signer certificate.
     */
    public SimpleOCSPServer(KeyStore ks, String password, String issuerAlias,
            String signerAlias) throws GeneralSecurityException, IOException {
        this(null, FREE_PORT, ks, password, issuerAlias, signerAlias);
    }

    /**
     * Construct a SimpleOCSPServer using specific network parameters,
     * keystore, password, and alias.
     *
     * @param addr the address to bind the server to.  A value of {@code null}
     * means the server will bind to all interfaces.
     * @param port the port to listen on.  A value of {@code 0} will mean that
     * the server will randomly pick an open ephemeral port to bind to.
     * @param ks the keystore to be used
     * @param password the password to access key material in the keystore
     * @param issuerAlias the alias of the issuer certificate
     * @param signerAlias the alias of the signer certificate and key.  A
     * value of {@code null} means that the {@code issuerAlias} will be used
     * to look up the signer key.
     *
     * @throws GeneralSecurityException if there are problems accessing the
     * keystore or finding objects within the keystore.
     * @throws IOException if a {@code ResponderId} cannot be generated from
     * the signer certificate.
     */
    public SimpleOCSPServer(InetAddress addr, int port, KeyStore ks,
            String password, String issuerAlias, String signerAlias)
            throws GeneralSecurityException, IOException {
        Objects.requireNonNull(ks, "Null keystore provided");
        Objects.requireNonNull(issuerAlias, "Null issuerName provided");

        utcDateFmt.setTimeZone(TimeZone.getTimeZone("GMT"));

        keystore = ks;
        issuerCert = (X509Certificate)ks.getCertificate(issuerAlias);
        if (issuerCert == null) {
            throw new IllegalArgumentException("Certificate for alias " +
                    issuerAlias + " not found");
        }

        if (signerAlias != null) {
            signerCert = (X509Certificate)ks.getCertificate(signerAlias);
            if (signerCert == null) {
                throw new IllegalArgumentException("Certificate for alias " +
                    signerAlias + " not found");
            }
            signerKey = (PrivateKey)ks.getKey(signerAlias,
                    password.toCharArray());
            if (signerKey == null) {
                throw new IllegalArgumentException("PrivateKey for alias " +
                    signerAlias + " not found");
            }
        } else {
            signerCert = issuerCert;
            signerKey = (PrivateKey)ks.getKey(issuerAlias,
                    password.toCharArray());
            if (signerKey == null) {
                throw new IllegalArgumentException("PrivateKey for alias " +
                    issuerAlias + " not found");
            }
        }

        sigAlgId = AlgorithmId.get("Sha256withRSA");
        respId = new ResponderId(signerCert.getSubjectX500Principal());
        listenAddress = addr;
        listenPort = port;
    }

    /**
     * Start the server.  The server will bind to the specified network
     * address and begin listening for incoming connections.
     *
     * @throws IOException if any number of things go wonky.
     */
    public synchronized void start() throws IOException {
        // You cannot start the server twice.
        if (started) {
            log("Server has already been started");
            return;
        } else {
            started = true;
        }

        // Create and start the thread pool
        threadPool = Executors.newFixedThreadPool(32, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = Executors.defaultThreadFactory().newThread(r);
                t.setDaemon(true);
                return t;
            }
        });

        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                try (ServerSocket sSock = new ServerSocket()) {
                    servSocket = sSock;
                    servSocket.setReuseAddress(true);
                    servSocket.setSoTimeout(500);
                    servSocket.bind(new InetSocketAddress(listenAddress,
                            listenPort), 128);
                    log("Listening on " + servSocket.getLocalSocketAddress());

                    // Singal ready
                    serverReady = true;

                    // Update the listenPort with the new port number.  If
                    // the server is restarted, it will bind to the same
                    // port rather than picking a new one.
                    listenPort = servSocket.getLocalPort();

                    // Main dispatch loop
                    while (!receivedShutdown) {
                        try {
                            Socket newConnection = servSocket.accept();
                            if (!acceptConnections) {
                                try {
                                    log("Reject connection");
                                    newConnection.close();
                                } catch (IOException e) {
                                    // ignore
                                }
                                continue;
                            }
                            threadPool.submit(new OcspHandler(newConnection));
                        } catch (SocketTimeoutException timeout) {
                            // Nothing to do here.  If receivedShutdown
                            // has changed to true then the loop will
                            // exit on its own.
                        } catch (IOException ioe) {
                            // Something bad happened, log and force a shutdown
                            log("Unexpected Exception: " + ioe);
                            stop();
                        }
                    }

                    log("Shutting down...");
                    threadPool.shutdown();
                } catch (IOException ioe) {
                    err(ioe);
                } finally {
                    // Reset state variables so the server can be restarted
                    receivedShutdown = false;
                    started = false;
                    serverReady = false;
                }
            }
        });
    }

    /**
     * Make the OCSP server reject incoming connections.
     */
    public synchronized void rejectConnections() {
        log("Reject OCSP connections");
        acceptConnections = false;
    }

    /**
     * Make the OCSP server accept incoming connections.
     */
    public synchronized void acceptConnections() {
        log("Accept OCSP connections");
        acceptConnections = true;
    }


    /**
     * Stop the OCSP server.
     */
    public synchronized void stop() {
        if (started) {
            receivedShutdown = true;
            log("Received shutdown notification");
        }
    }

    /**
     * Print {@code SimpleOCSPServer} operating parameters.
     *
     * @return the {@code SimpleOCSPServer} operating parameters in
     * {@code String} form.
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("OCSP Server:\n");
        sb.append("----------------------------------------------\n");
        sb.append("issuer: ").append(issuerCert.getSubjectX500Principal()).
                append("\n");
        sb.append("signer: ").append(signerCert.getSubjectX500Principal()).
                append("\n");
        sb.append("ResponderId: ").append(respId).append("\n");
        sb.append("----------------------------------------------");

        return sb.toString();
    }

    /**
     * Helpful debug routine to hex dump byte arrays.
     *
     * @param data the array of bytes to dump to stdout.
     *
     * @return the hexdump of the byte array
     */
    private static String dumpHexBytes(byte[] data) {
        return dumpHexBytes(data, 16, "\n", " ");
    }

    /**
     *
     * @param data the array of bytes to dump to stdout.
     * @param itemsPerLine the number of bytes to display per line
     * if the {@code lineDelim} character is blank then all bytes will be
     * printed on a single line.
     * @param lineDelim the delimiter between lines
     * @param itemDelim the delimiter between bytes
     *
     * @return The hexdump of the byte array
     */
    private static String dumpHexBytes(byte[] data, int itemsPerLine,
            String lineDelim, String itemDelim) {
        StringBuilder sb = new StringBuilder();
        if (data != null) {
            for (int i = 0; i < data.length; i++) {
                if (i % itemsPerLine == 0 && i != 0) {
                    sb.append(lineDelim);
                }
                sb.append(String.format("%02X", data[i])).append(itemDelim);
            }
        }

        return sb.toString();
    }

    /**
     * Enable or disable the logging feature.
     *
     * @param enable {@code true} to enable logging, {@code false} to
     * disable it.  The setting must be activated before the server calls
     * its start method.  Any calls after that have no effect.
     */
    public void enableLog(boolean enable) {
        if (!started) {
            logEnabled = enable;
        }
    }

    /**
     * Sets the nextUpdate interval.  Intervals will be calculated relative
     * to the server startup time.  When first set, the nextUpdate date is
     * calculated based on the current time plus the interval.  After that,
     * calls to getNextUpdate() will return this date if it is still
     * later than current time.  If not, the Date will be updated to the
     * next interval that is later than current time.  This value must be set
     * before the server has had its start method called.  Calls made after
     * the server has been started have no effect.
     *
     * @param interval the recurring time interval in seconds used to
     * calculate nextUpdate times.   A value less than or equal to 0 will
     * disable the nextUpdate feature.
     */
    public synchronized void setNextUpdateInterval(long interval) {
        if (!started) {
            if (interval <= 0) {
                nextUpdateInterval = -1;
                nextUpdate = null;
                log("nexUpdate support has been disabled");
            } else {
                nextUpdateInterval = interval * 1000;
                nextUpdate = new Date(System.currentTimeMillis() +
                        nextUpdateInterval);
                log("nextUpdate set to " + nextUpdate);
            }
        }
    }

    /**
     * Return the nextUpdate {@code Date} object for this server.  If the
     * nextUpdate date has already passed, set a new nextUpdate based on
     * the nextUpdate interval and return that date.
     *
     * @return a {@code Date} object set to the nextUpdate field for OCSP
     * responses.
     */
    private synchronized Date getNextUpdate() {
        if (nextUpdate != null && nextUpdate.before(new Date())) {
            long nuEpochTime = nextUpdate.getTime();
            long currentTime = System.currentTimeMillis();

            // Keep adding nextUpdate intervals until you reach a date
            // that is later than current time.
            while (currentTime >= nuEpochTime) {
                nuEpochTime += nextUpdateInterval;
            }

            // Set the nextUpdate for future threads
            nextUpdate = new Date(nuEpochTime);
            log("nextUpdate updated to new value: " + nextUpdate);
        }
        return nextUpdate;
    }

    /**
     * Add entries into the responder's status database.
     *
     * @param newEntries a map of {@code CertStatusInfo} objects, keyed on
     * their serial number (as a {@code BigInteger}).  All serial numbers
     * are assumed to have come from this responder's issuer certificate.
     *
     * @throws IOException if a CertId cannot be generated.
     */
    public void updateStatusDb(Map<BigInteger, CertStatusInfo> newEntries)
            throws IOException {
         if (newEntries != null) {
            for (BigInteger serial : newEntries.keySet()) {
                CertStatusInfo info = newEntries.get(serial);
                if (info != null) {
                    CertId cid = new CertId(issuerCert,
                            new SerialNumber(serial));
                    statusDb.put(cid, info);
                    log("Added entry for serial " + serial + "(" +
                            info.getType() + ")");
                }
            }
        }
    }

    /**
     * Check the status database for revocation information one one or more
     * certificates.
     *
     * @param reqList the list of {@code LocalSingleRequest} objects taken
     * from the incoming OCSP request.
     *
     * @return a {@code Map} of {@code CertStatusInfo} objects keyed by their
     * {@code CertId} values, for each single request passed in.  Those
     * CertIds not found in the statusDb will have returned List members with
     * a status of UNKNOWN.
     */
    private Map<CertId, CertStatusInfo> checkStatusDb(
            List<LocalOcspRequest.LocalSingleRequest> reqList) {
        // TODO figure out what, if anything to do with request extensions
        Map<CertId, CertStatusInfo> returnMap = new HashMap<>();

        for (LocalOcspRequest.LocalSingleRequest req : reqList) {
            CertId cid = req.getCertId();
            CertStatusInfo info = statusDb.get(cid);
            if (info != null) {
                log("Status for SN " + cid.getSerialNumber() + ": " +
                        info.getType());
                returnMap.put(cid, info);
            } else {
                log("Status for SN " + cid.getSerialNumber() +
                        " not found, using CERT_STATUS_UNKNOWN");
                returnMap.put(cid,
                        new CertStatusInfo(CertStatus.CERT_STATUS_UNKNOWN));
            }
        }

        return Collections.unmodifiableMap(returnMap);
    }

    /**
     * Set the digital signature algorithm used to sign OCSP responses.
     *
     * @param algName The algorithm name
     *
     * @throws NoSuchAlgorithmException if the algorithm name is invalid.
     */
    public void setSignatureAlgorithm(String algName)
            throws NoSuchAlgorithmException {
        if (!started) {
            sigAlgId = AlgorithmId.get(algName);
        }
    }

    /**
     * Get the port the OCSP server is running on.
     *
     * @return the port that the OCSP server is running on, or -1 if the
     * server has not yet been bound to a port.
     */
    public int getPort() {
        if (serverReady) {
            InetSocketAddress inetSock =
                    (InetSocketAddress)servSocket.getLocalSocketAddress();
            return inetSock.getPort();
        } else {
            return -1;
        }
    }

    /**
     * Use to check if OCSP server is ready to accept connection.
     *
     * @return true if server ready, false otherwise
     */
    public boolean isServerReady() {
        return serverReady;
    }

    /**
     * Set a delay between the reception of the request and production of
     * the response.
     *
     * @param delayMillis the number of milliseconds to wait before acting
     * on the incoming request.
     */
    public void setDelay(long delayMillis) {
        delayMsec = delayMillis > 0 ? delayMillis : 0;
        if (delayMsec > 0) {
            log("OCSP latency set to " + delayMsec + " milliseconds.");
        } else {
            log("OCSP latency disabled");
        }
    }

    /**
     * Log a message to stdout.
     *
     * @param message the message to log
     */
    private synchronized void log(String message) {
        if (logEnabled || debug != null) {
            System.out.println("[" + Thread.currentThread().getName() + "]: " +
                    message);
        }
    }

    /**
     * Log an error message on the stderr stream.
     *
     * @param message the message to log
     */
    private static synchronized void err(String message) {
        System.out.println("[" + Thread.currentThread().getName() + "]: " +
                message);
    }

    /**
     * Log exception information on the stderr stream.
     *
     * @param exc the exception to dump information about
     */
    private static synchronized void err(Throwable exc) {
        System.out.print("[" + Thread.currentThread().getName() +
                "]: Exception: ");
        exc.printStackTrace(System.out);
    }

    /**
     * The {@code CertStatusInfo} class defines an object used to return
     * information from the internal status database.  The data in this
     * object may be used to construct OCSP responses.
     */
    public static class CertStatusInfo {
        private CertStatus certStatusType;
        private CRLReason reason;
        private Date revocationTime;

        /**
         * Create a Certificate status object by providing the status only.
         * If the status is {@code REVOKED} then current time is assumed
         * for the revocation time.
         *
         * @param statType the status for this entry.
         */
        public CertStatusInfo(CertStatus statType) {
            this(statType, null, null);
        }

        /**
         * Create a CertStatusInfo providing both type and revocation date
         * (if applicable).
         *
         * @param statType the status for this entry.
         * @param revDate if applicable, the date that revocation took place.
         * A value of {@code null} indicates that current time should be used.
         * If the value of {@code statType} is not {@code CERT_STATUS_REVOKED},
         * then the {@code revDate} parameter is ignored.
         */
        public CertStatusInfo(CertStatus statType, Date revDate) {
            this(statType, revDate, null);
        }

        /**
         * Create a CertStatusInfo providing type, revocation date
         * (if applicable) and revocation reason.
         *
         * @param statType the status for this entry.
         * @param revDate if applicable, the date that revocation took place.
         * A value of {@code null} indicates that current time should be used.
         * If the value of {@code statType} is not {@code CERT_STATUS_REVOKED},
         * then the {@code revDate} parameter is ignored.
         * @param revReason the reason the certificate was revoked.  A value of
         * {@code null} means that no reason was provided.
         */
        public CertStatusInfo(CertStatus statType, Date revDate,
                CRLReason revReason) {
            Objects.requireNonNull(statType, "Cert Status must be non-null");
            certStatusType = statType;
            switch (statType) {
                case CERT_STATUS_GOOD:
                case CERT_STATUS_UNKNOWN:
                    revocationTime = null;
                    break;
                case CERT_STATUS_REVOKED:
                    revocationTime = revDate != null ? (Date)revDate.clone() :
                            new Date();
                    break;
                default:
                    throw new IllegalArgumentException("Unknown status type: " +
                            statType);
            }
        }

        /**
         * Get the cert status type
         *
         * @return the status applied to this object (e.g.
         * {@code CERT_STATUS_GOOD}, {@code CERT_STATUS_UNKNOWN}, etc.)
         */
        public CertStatus getType() {
            return certStatusType;
        }

        /**
         * Get the revocation time (if applicable).
         *
         * @return the revocation time as a {@code Date} object, or
         * {@code null} if not applicable (i.e. if the certificate hasn't been
         * revoked).
         */
        public Date getRevocationTime() {
            return (revocationTime != null ? (Date)revocationTime.clone() :
                    null);
        }

        /**
         * Get the revocation reason.
         *
         * @return the revocation reason, or {@code null} if one was not
         * provided.
         */
        public CRLReason getRevocationReason() {
            return reason;
        }
    }

    /**
     * Runnable task that handles incoming OCSP Requests and returns
     * responses.
     */
    private class OcspHandler implements Runnable {
        private final Socket sock;
        InetSocketAddress peerSockAddr;

        /**
         * Construct an {@code OcspHandler}.
         *
         * @param incomingSocket the socket the server created on accept()
         */
        private OcspHandler(Socket incomingSocket) {
            sock = incomingSocket;
        }

        /**
         * Run the OCSP Request parser and construct a response to be sent
         * back to the client.
         */
        @Override
        public void run() {
            // If we have implemented a delay to simulate network latency
            // wait out the delay here before any other processing.
            try {
                if (delayMsec > 0) {
                    Thread.sleep(delayMsec);
                }
            } catch (InterruptedException ie) {
                // Just log the interrupted sleep
                log("Delay of " + delayMsec + " milliseconds was interrupted");
            }

            try (Socket ocspSocket = sock;
                    InputStream in = ocspSocket.getInputStream();
                    OutputStream out = ocspSocket.getOutputStream()) {
                peerSockAddr =
                        (InetSocketAddress)ocspSocket.getRemoteSocketAddress();
                log("Received incoming connection from " + peerSockAddr);
                String[] headerTokens = readLine(in).split(" ");
                LocalOcspRequest ocspReq = null;
                LocalOcspResponse ocspResp = null;
                ResponseStatus respStat = ResponseStatus.INTERNAL_ERROR;
                try {
                    if (headerTokens[0] != null) {
                        switch (headerTokens[0]) {
                            case "POST":
                                    ocspReq = parseHttpOcspPost(in);
                                break;
                            case "GET":
                                // req = parseHttpOcspGet(in);
                                // TODO implement the GET parsing
                                throw new IOException("GET method unsupported");
                            default:
                                respStat = ResponseStatus.MALFORMED_REQUEST;
                                throw new IOException("Not a GET or POST");
                        }
                    } else {
                        respStat = ResponseStatus.MALFORMED_REQUEST;
                        throw new IOException("Unable to get HTTP method");
                    }

                    if (ocspReq != null) {
                        log(ocspReq.toString());
                        // Get responses for all CertIds in the request
                        Map<CertId, CertStatusInfo> statusMap =
                                checkStatusDb(ocspReq.getRequests());
                        if (statusMap.isEmpty()) {
                            respStat = ResponseStatus.UNAUTHORIZED;
                        } else {
                            ocspResp = new LocalOcspResponse(
                                    ResponseStatus.SUCCESSFUL, statusMap,
                                    ocspReq.getExtensions());
                        }
                    } else {
                        respStat = ResponseStatus.MALFORMED_REQUEST;
                        throw new IOException("Found null request");
                    }
                } catch (IOException | RuntimeException exc) {
                    err(exc);
                }
                if (ocspResp == null) {
                    ocspResp = new LocalOcspResponse(respStat);
                }
                sendResponse(out, ocspResp);
            } catch (IOException | CertificateException exc) {
                err(exc);
            }
        }

        /**
         * Send an OCSP response on an {@code OutputStream}.
         *
         * @param out the {@code OutputStream} on which to send the response.
         * @param resp the OCSP response to send.
         *
         * @throws IOException if an encoding error occurs.
         */
        public void sendResponse(OutputStream out, LocalOcspResponse resp)
                throws IOException {
            StringBuilder sb = new StringBuilder();

            byte[] respBytes;
            try {
                respBytes = resp.getBytes();
            } catch (RuntimeException re) {
                err(re);
                return;
            }

            sb.append("HTTP/1.0 200 OK\r\n");
            sb.append("Content-Type: application/ocsp-response\r\n");
            sb.append("Content-Length: ").append(respBytes.length);
            sb.append("\r\n\r\n");

            out.write(sb.toString().getBytes("UTF-8"));
            out.write(respBytes);
            log(resp.toString());
        }

        /**
         * Parse the incoming HTTP POST of an OCSP Request.
         *
         * @param inStream the input stream from the socket bound to this
         * {@code OcspHandler}.
         *
         * @return the OCSP Request as a {@code LocalOcspRequest}
         *
         * @throws IOException if there are network related issues or problems
         * occur during parsing of the OCSP request.
         * @throws CertificateException if one or more of the certificates in
         * the OCSP request cannot be read/parsed.
         */
        private LocalOcspRequest parseHttpOcspPost(InputStream inStream)
                throws IOException, CertificateException {
            boolean endOfHeader = false;
            boolean properContentType = false;
            int length = -1;

            while (!endOfHeader) {
                String[] lineTokens = readLine(inStream).split(" ");
                if (lineTokens[0].isEmpty()) {
                    endOfHeader = true;
                } else if (lineTokens[0].equalsIgnoreCase("Content-Type:")) {
                    if (lineTokens[1] == null ||
                            !lineTokens[1].equals(
                                    "application/ocsp-request")) {
                        log("Unknown Content-Type: " +
                                (lineTokens[1] != null ?
                                        lineTokens[1] : "<NULL>"));
                        return null;
                    } else {
                        properContentType = true;
                        log("Content-Type = " + lineTokens[1]);
                    }
                } else if (lineTokens[0].equalsIgnoreCase("Content-Length:")) {
                    if (lineTokens[1] != null) {
                        length = Integer.parseInt(lineTokens[1]);
                        log("Content-Length = " + length);
                    }
                }
            }

            // Okay, make sure we got what we needed from the header, then
            // read the remaining OCSP Request bytes
            if (properContentType && length >= 0) {
                byte[] ocspBytes = new byte[length];
                inStream.read(ocspBytes);
                return new LocalOcspRequest(ocspBytes);
            } else {
                return null;
            }
        }

        /**
         * Read a line of text that is CRLF-delimited.
         *
         * @param is the {@code InputStream} tied to the socket
         * for this {@code OcspHandler}
         *
         * @return a {@code String} consisting of the line of text
         * read from the stream with the CRLF stripped.
         *
         * @throws IOException if any I/O error occurs.
         */
        private String readLine(InputStream is) throws IOException {
            PushbackInputStream pbis = new PushbackInputStream(is);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            boolean done = false;
            while (!done) {
                byte b = (byte)pbis.read();
                if (b == '\r') {
                    byte bNext = (byte)pbis.read();
                    if (bNext == '\n' || bNext == -1) {
                        done = true;
                    } else {
                        pbis.unread(bNext);
                        bos.write(b);
                    }
                } else if (b == -1) {
                    done = true;
                } else {
                    bos.write(b);
                }
            }

            return new String(bos.toByteArray(), "UTF-8");
        }
    }


    /**
     * Simple nested class to handle OCSP requests without making
     * changes to sun.security.provider.certpath.OCSPRequest
     */
    public class LocalOcspRequest {

        private byte[] nonce;
        private byte[] signature = null;
        private AlgorithmId algId = null;
        private int version = 0;
        private GeneralName requestorName = null;
        private Map<String, Extension> extensions = Collections.emptyMap();
        private final List<LocalSingleRequest> requestList = new ArrayList<>();
        private final List<X509Certificate> certificates = new ArrayList<>();

        /**
         * Construct a {@code LocalOcspRequest} from its DER encoding.
         *
         * @param requestBytes the DER-encoded bytes
         *
         * @throws IOException if decoding errors occur
         * @throws CertificateException if certificates are found in the
         * OCSP request and they do not parse correctly.
         */
        private LocalOcspRequest(byte[] requestBytes) throws IOException,
                CertificateException {
            Objects.requireNonNull(requestBytes, "Received null input");

            DerInputStream dis = new DerInputStream(requestBytes);

            // Parse the top-level structure, it should have no more than
            // two elements.
            DerValue[] topStructs = dis.getSequence(2);
            for (DerValue dv : topStructs) {
                if (dv.tag == DerValue.tag_Sequence) {
                    parseTbsRequest(dv);
                } else if (dv.isContextSpecific((byte)0)) {
                    parseSignature(dv);
                } else {
                    throw new IOException("Unknown tag at top level: " +
                            dv.tag);
                }
            }
        }

        /**
         * Parse the signature block from an OCSP request
         *
         * @param sigSequence a {@code DerValue} containing the signature
         * block at the outer sequence datum.
         *
         * @throws IOException if any non-certificate-based parsing errors occur
         * @throws CertificateException if certificates are found in the
         * OCSP request and they do not parse correctly.
         */
        private void parseSignature(DerValue sigSequence)
                throws IOException, CertificateException {
            DerValue[] sigItems = sigSequence.data.getSequence(3);
            if (sigItems.length != 3) {
                throw new IOException("Invalid number of signature items: " +
                        "expected 3, got " + sigItems.length);
            }

            algId = AlgorithmId.parse(sigItems[0]);
            signature = sigItems[1].getBitString();

            if (sigItems[2].isContextSpecific((byte)0)) {
                DerValue[] certDerItems = sigItems[2].data.getSequence(4);
                int i = 0;
                for (DerValue dv : certDerItems) {
                    X509Certificate xc = new X509CertImpl(dv);
                    certificates.add(xc);
                }
            } else {
                throw new IOException("Invalid tag in signature block: " +
                    sigItems[2].tag);
            }
        }

        /**
         * Parse the to-be-signed request data
         *
         * @param tbsReqSeq a {@code DerValue} object containing the to-be-
         * signed OCSP request at the outermost SEQUENCE tag.
         * @throws IOException if any parsing errors occur
         */
        private void parseTbsRequest(DerValue tbsReqSeq) throws IOException {
            while (tbsReqSeq.data.available() > 0) {
                DerValue dv = tbsReqSeq.data.getDerValue();
                if (dv.isContextSpecific((byte)0)) {
                    // The version was explicitly called out
                    version = dv.data.getInteger();
                } else if (dv.isContextSpecific((byte)1)) {
                    // A GeneralName was provided
                    requestorName = new GeneralName(dv.data.getDerValue());
                } else if (dv.isContextSpecific((byte)2)) {
                    // Parse the extensions
                    DerValue[] extItems = dv.data.getSequence(2);
                    extensions = parseExtensions(extItems);
                } else if (dv.tag == DerValue.tag_Sequence) {
                    while (dv.data.available() > 0) {
                        requestList.add(new LocalSingleRequest(dv.data));
                    }
                }
            }
        }

        /**
         * Parse a SEQUENCE of extensions.  This routine is used both
         * at the overall request level and down at the singleRequest layer.
         *
         * @param extDerItems an array of {@code DerValue} items, each one
         * consisting of a DER-encoded extension.
         *
         * @return a {@code Map} of zero or more extensions,
         * keyed by its object identifier in {@code String} form.
         *
         * @throws IOException if any parsing errors occur.
         */
        private Map<String, Extension> parseExtensions(DerValue[] extDerItems)
                throws IOException {
            Map<String, Extension> extMap = new HashMap<>();

            if (extDerItems != null && extDerItems.length != 0) {
                for (DerValue extDerVal : extDerItems) {
                    sun.security.x509.Extension ext =
                            new sun.security.x509.Extension(extDerVal);
                    extMap.put(ext.getId(), ext);
                }
            }

            return extMap;
        }

        /**
         * Return the list of single request objects in this OCSP request.
         *
         * @return an unmodifiable {@code List} of zero or more requests.
         */
        private List<LocalSingleRequest> getRequests() {
            return Collections.unmodifiableList(requestList);
        }

        /**
         * Return the list of X.509 Certificates in this OCSP request.
         *
         * @return an unmodifiable {@code List} of zero or more
         * {@cpde X509Certificate} objects.
         */
        private List<X509Certificate> getCertificates() {
            return Collections.unmodifiableList(certificates);
        }

        /**
         * Return the map of OCSP request extensions.
         *
         * @return an unmodifiable {@code Map} of zero or more
         * {@code Extension} objects, keyed by their object identifiers
         * in {@code String} form.
         */
        private Map<String, Extension> getExtensions() {
            return Collections.unmodifiableMap(extensions);
        }

        /**
         * Display the {@code LocalOcspRequest} in human readable form.
         *
         * @return a {@code String} representation of the
         * {@code LocalOcspRequest}
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();

            sb.append(String.format("OCSP Request: Version %d (0x%X)",
                    version + 1, version)).append("\n");
            if (requestorName != null) {
                sb.append("Requestor Name: ").append(requestorName).
                        append("\n");
            }

            int requestCtr = 0;
            for (LocalSingleRequest lsr : requestList) {
                sb.append("Request [").append(requestCtr++).append("]\n");
                sb.append(lsr).append("\n");
            }
            if (!extensions.isEmpty()) {
                sb.append("Extensions (").append(extensions.size()).
                        append(")\n");
                for (Extension ext : extensions.values()) {
                    sb.append("\t").append(ext).append("\n");
                }
            }
            if (signature != null) {
                sb.append("Signature: ").append(algId).append("\n");
                sb.append(dumpHexBytes(signature)).append("\n");
                int certCtr = 0;
                for (X509Certificate cert : certificates) {
                    sb.append("Certificate [").append(certCtr++).append("]").
                            append("\n");
                    sb.append("\tSubject: ");
                    sb.append(cert.getSubjectX500Principal()).append("\n");
                    sb.append("\tIssuer: ");
                    sb.append(cert.getIssuerX500Principal()).append("\n");
                    sb.append("\tSerial: ").append(cert.getSerialNumber());
                }
            }

            return sb.toString();
        }

        /**
         * Inner class designed to handle the decoding/representation of
         * single requests within a {@code LocalOcspRequest} object.
         */
        public class LocalSingleRequest {
            private final CertId cid;
            private Map<String, Extension> extensions = Collections.emptyMap();

            private LocalSingleRequest(DerInputStream dis)
                    throws IOException {
                DerValue[] srItems = dis.getSequence(2);

                // There should be 1, possibly 2 DerValue items
                if (srItems.length == 1 || srItems.length == 2) {
                    // The first parsable item should be the mandatory CertId
                    cid = new CertId(srItems[0].data);
                    if (srItems.length == 2) {
                        if (srItems[1].isContextSpecific((byte)0)) {
                            DerValue[] extDerItems = srItems[1].data.getSequence(2);
                            extensions = parseExtensions(extDerItems);
                        } else {
                            throw new IOException("Illegal tag in Request " +
                                    "extensions: " + srItems[1].tag);
                        }
                    }
                } else {
                    throw new IOException("Invalid number of items in " +
                            "Request (" + srItems.length + ")");
                }
            }

            /**
             * Get the {@code CertId} for this single request.
             *
             * @return the {@code CertId} for this single request.
             */
            private CertId getCertId() {
                return cid;
            }

            /**
             * Return the map of single request extensions.
             *
             * @return an unmodifiable {@code Map} of zero or more
             * {@code Extension} objects, keyed by their object identifiers
             * in {@code String} form.
             */
            private Map<String, Extension> getExtensions() {
                return Collections.unmodifiableMap(extensions);
            }

            /**
             * Display the {@code LocalSingleRequest} in human readable form.
             *
             * @return a {@code String} representation of the
             * {@code LocalSingleRequest}
             */
            @Override
            public String toString() {
                StringBuilder sb = new StringBuilder();
                sb.append("CertId, Algorithm = ");
                sb.append(cid.getHashAlgorithm()).append("\n");
                sb.append("\tIssuer Name Hash: ");
                sb.append(dumpHexBytes(cid.getIssuerNameHash(), 256, "", ""));
                sb.append("\n");
                sb.append("\tIssuer Key Hash: ");
                sb.append(dumpHexBytes(cid.getIssuerKeyHash(), 256, "", ""));
                sb.append("\n");
                sb.append("\tSerial Number: ").append(cid.getSerialNumber());
                if (!extensions.isEmpty()) {
                    sb.append("Extensions (").append(extensions.size()).
                            append(")\n");
                    for (Extension ext : extensions.values()) {
                        sb.append("\t").append(ext).append("\n");
                    }
                }

                return sb.toString();
            }
        }
    }

    /**
     * Simple nested class to handle OCSP requests without making
     * changes to sun.security.provider.certpath.OCSPResponse
     */
    public class LocalOcspResponse {
        private final int version = 0;
        private final OCSPResponse.ResponseStatus responseStatus;
        private final Map<CertId, CertStatusInfo> respItemMap;
        private final Date producedAtDate;
        private final List<LocalSingleResponse> singleResponseList =
                new ArrayList<>();
        private final Map<String, Extension> responseExtensions;
        private byte[] signature;
        private final List<X509Certificate> certificates;
        private final byte[] encodedResponse;

        /**
         * Constructor for the generation of non-successful responses
         *
         * @param respStat the OCSP response status.
         *
         * @throws IOException if an error happens during encoding
         * @throws NullPointerException if {@code respStat} is {@code null}
         * or {@code respStat} is successful.
         */
        public LocalOcspResponse(OCSPResponse.ResponseStatus respStat)
                throws IOException {
            this(respStat, null, null);
        }

        /**
         * Construct a response from a list of certificate
         * status objects and extensions.
         *
         * @param respStat the status of the entire response
         * @param itemMap a {@code Map} of {@code CertId} objects and their
         * respective revocation statuses from the server's response DB.
         * @param reqExtensions a {@code Map} of request extensions
         *
         * @throws IOException if an error happens during encoding
         * @throws NullPointerException if {@code respStat} is {@code null}
         * or {@code respStat} is successful, and a {@code null} {@code itemMap}
         * has been provided.
         */
        public LocalOcspResponse(OCSPResponse.ResponseStatus respStat,
                Map<CertId, CertStatusInfo> itemMap,
                Map<String, Extension> reqExtensions) throws IOException {
            responseStatus = Objects.requireNonNull(respStat,
                    "Illegal null response status");
            if (responseStatus == ResponseStatus.SUCCESSFUL) {
                respItemMap = Objects.requireNonNull(itemMap,
                        "SUCCESSFUL responses must have a response map");
                producedAtDate = new Date();

                // Turn the answerd from the response DB query into a list
                // of single responses.
                for (CertId id : itemMap.keySet()) {
                    singleResponseList.add(
                            new LocalSingleResponse(id, itemMap.get(id)));
                }

                responseExtensions = setResponseExtensions(reqExtensions);
                certificates = new ArrayList<>();
                if (signerCert != issuerCert) {
                    certificates.add(signerCert);
                }
                certificates.add(issuerCert);
            } else {
                respItemMap = null;
                producedAtDate = null;
                responseExtensions = null;
                certificates = null;
            }
            encodedResponse = this.getBytes();
        }

        /**
         * Set the response extensions based on the request extensions
         * that were received.  Right now, this is limited to the
         * OCSP nonce extension.
         *
         * @param reqExts a {@code Map} of zero or more request extensions
         *
         * @return a {@code Map} of zero or more response extensions, keyed
         * by the extension object identifier in {@code String} form.
         */
        private Map<String, Extension> setResponseExtensions(
                Map<String, Extension> reqExts) {
            Map<String, Extension> respExts = new HashMap<>();
            String ocspNonceStr = PKIXExtensions.OCSPNonce_Id.toString();

            if (reqExts != null) {
                for (String id : reqExts.keySet()) {
                    if (id.equals(ocspNonceStr)) {
                        // We found a nonce, add it into the response extensions
                        Extension ext = reqExts.get(id);
                        if (ext != null) {
                            respExts.put(id, ext);
                            log("Added OCSP Nonce to response");
                        } else {
                            log("Error: Found nonce entry, but found null " +
                                    "value.  Skipping");
                        }
                    }
                }
            }

            return respExts;
        }

        /**
         * Get the DER-encoded response bytes for this response
         *
         * @return a byte array containing the DER-encoded bytes for
         * the response
         *
         * @throws IOException if any encoding errors occur
         */
        private byte[] getBytes() throws IOException {
            DerOutputStream outerSeq = new DerOutputStream();
            DerOutputStream responseStream = new DerOutputStream();
            responseStream.putEnumerated(responseStatus.ordinal());
            if (responseStatus == ResponseStatus.SUCCESSFUL &&
                    respItemMap != null) {
                encodeResponseBytes(responseStream);
            }

            // Commit the outermost sequence bytes
            outerSeq.write(DerValue.tag_Sequence, responseStream);
            return outerSeq.toByteArray();
        }

        private void encodeResponseBytes(DerOutputStream responseStream)
                throws IOException {
            DerOutputStream explicitZero = new DerOutputStream();
            DerOutputStream respItemStream = new DerOutputStream();

            respItemStream.putOID(OCSP_BASIC_RESPONSE_OID);

            byte[] basicOcspBytes = encodeBasicOcspResponse();
            respItemStream.putOctetString(basicOcspBytes);
            explicitZero.write(DerValue.tag_Sequence, respItemStream);
            responseStream.write(DerValue.createTag(DerValue.TAG_CONTEXT,
                    true, (byte)0), explicitZero);
        }

        private byte[] encodeBasicOcspResponse() throws IOException {
            DerOutputStream outerSeq = new DerOutputStream();
            DerOutputStream basicORItemStream = new DerOutputStream();

            // Encode the tbsResponse
            byte[] tbsResponseBytes = encodeTbsResponse();
            basicORItemStream.write(tbsResponseBytes);

            try {
                sigAlgId.derEncode(basicORItemStream);

                // Create the signature
                Signature sig = Signature.getInstance(sigAlgId.getName());
                sig.initSign(signerKey);
                sig.update(tbsResponseBytes);
                signature = sig.sign();
                basicORItemStream.putBitString(signature);
            } catch (GeneralSecurityException exc) {
                err(exc);
                throw new IOException(exc);
            }

            // Add certificates
            try {
                DerOutputStream certStream = new DerOutputStream();
                ArrayList<DerValue> certList = new ArrayList<>();
                if (signerCert != issuerCert) {
                    certList.add(new DerValue(signerCert.getEncoded()));
                }
                certList.add(new DerValue(issuerCert.getEncoded()));
                DerValue[] dvals = new DerValue[certList.size()];
                certStream.putSequence(certList.toArray(dvals));
                basicORItemStream.write(DerValue.createTag(DerValue.TAG_CONTEXT,
                        true, (byte)0), certStream);
            } catch (CertificateEncodingException cex) {
                err(cex);
                throw new IOException(cex);
            }

            // Commit the outermost sequence bytes
            outerSeq.write(DerValue.tag_Sequence, basicORItemStream);
            return outerSeq.toByteArray();
        }

        private byte[] encodeTbsResponse() throws IOException {
            DerOutputStream outerSeq = new DerOutputStream();
            DerOutputStream tbsStream = new DerOutputStream();

            // Note: We're not going explicitly assert the version
            tbsStream.write(respId.getEncoded());
            tbsStream.putGeneralizedTime(producedAtDate);

            // Sequence of responses
            encodeSingleResponses(tbsStream);

            // TODO: add response extension support
            encodeExtensions(tbsStream);

            outerSeq.write(DerValue.tag_Sequence, tbsStream);
            return outerSeq.toByteArray();
        }

        private void encodeSingleResponses(DerOutputStream tbsStream)
                throws IOException {
            DerValue[] srDerVals = new DerValue[singleResponseList.size()];
            int srDvCtr = 0;

            for (LocalSingleResponse lsr : singleResponseList) {
                srDerVals[srDvCtr++] = new DerValue(lsr.getBytes());
            }

            tbsStream.putSequence(srDerVals);
        }

        private void encodeExtensions(DerOutputStream tbsStream)
                throws IOException {
            DerOutputStream extSequence = new DerOutputStream();
            DerOutputStream extItems = new DerOutputStream();

            for (Extension ext : responseExtensions.values()) {
                ext.encode(extItems);
            }
            extSequence.write(DerValue.tag_Sequence, extItems);
            tbsStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, true,
                    (byte)1), extSequence);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();

            sb.append("OCSP Response: ").append(responseStatus).append("\n");
            if (responseStatus == ResponseStatus.SUCCESSFUL) {
                sb.append("Response Type: ").
                        append(OCSP_BASIC_RESPONSE_OID.toString()).append("\n");
                sb.append(String.format("Version: %d (0x%X)", version + 1,
                        version)).append("\n");
                sb.append("Responder Id: ").append(respId.toString()).
                        append("\n");
                sb.append("Produced At: ").
                        append(utcDateFmt.format(producedAtDate)).append("\n");

                int srCtr = 0;
                for (LocalSingleResponse lsr : singleResponseList) {
                    sb.append("SingleResponse [").append(srCtr++).append("]\n");
                    sb.append(lsr);
                }

                if (!responseExtensions.isEmpty()) {
                    sb.append("Extensions (").append(responseExtensions.size()).
                            append(")\n");
                    for (Extension ext : responseExtensions.values()) {
                        sb.append("\t").append(ext).append("\n");
                    }
                } else {
                    sb.append("\n");
                }

                if (signature != null) {
                    sb.append("Signature: ").append(sigAlgId).append("\n");
                    sb.append(dumpHexBytes(signature)).append("\n");
                    int certCtr = 0;
                    for (X509Certificate cert : certificates) {
                        sb.append("Certificate [").append(certCtr++).append("]").
                                append("\n");
                        sb.append("\tSubject: ");
                        sb.append(cert.getSubjectX500Principal()).append("\n");
                        sb.append("\tIssuer: ");
                        sb.append(cert.getIssuerX500Principal()).append("\n");
                        sb.append("\tSerial: ").append(cert.getSerialNumber());
                        sb.append("\n");
                    }
                }
            }

            return sb.toString();
        }

        private class LocalSingleResponse {
            private final CertId certId;
            private final CertStatusInfo csInfo;
            private final Date thisUpdate;
            private final Date lsrNextUpdate;
            private final Map<String, Extension> singleExtensions;

            public LocalSingleResponse(CertId cid, CertStatusInfo info) {
                certId = Objects.requireNonNull(cid, "CertId must be non-null");
                csInfo = Objects.requireNonNull(info,
                        "CertStatusInfo must be non-null");

                // For now, we'll keep things simple and make the thisUpdate
                // field the same as the producedAt date.
                thisUpdate = producedAtDate;
                lsrNextUpdate = getNextUpdate();

                // TODO Add extensions support
                singleExtensions = Collections.emptyMap();
            }

            @Override
            public String toString() {
                StringBuilder sb = new StringBuilder();
                sb.append("Certificate Status: ").append(csInfo.getType());
                sb.append("\n");
                if (csInfo.getType() == CertStatus.CERT_STATUS_REVOKED) {
                    sb.append("Revocation Time: ");
                    sb.append(utcDateFmt.format(csInfo.getRevocationTime()));
                    sb.append("\n");
                    if (csInfo.getRevocationReason() != null) {
                        sb.append("Revocation Reason: ");
                        sb.append(csInfo.getRevocationReason()).append("\n");
                    }
                }

                sb.append("CertId, Algorithm = ");
                sb.append(certId.getHashAlgorithm()).append("\n");
                sb.append("\tIssuer Name Hash: ");
                sb.append(dumpHexBytes(certId.getIssuerNameHash(), 256, "", ""));
                sb.append("\n");
                sb.append("\tIssuer Key Hash: ");
                sb.append(dumpHexBytes(certId.getIssuerKeyHash(), 256, "", ""));
                sb.append("\n");
                sb.append("\tSerial Number: ").append(certId.getSerialNumber());
                sb.append("\n");
                sb.append("This Update: ");
                sb.append(utcDateFmt.format(thisUpdate)).append("\n");
                if (lsrNextUpdate != null) {
                    sb.append("Next Update: ");
                    sb.append(utcDateFmt.format(lsrNextUpdate)).append("\n");
                }

                if (!singleExtensions.isEmpty()) {
                    sb.append("Extensions (").append(singleExtensions.size()).
                            append(")\n");
                    for (Extension ext : singleExtensions.values()) {
                        sb.append("\t").append(ext).append("\n");
                    }
                }

                return sb.toString();
            }

            public byte[] getBytes() throws IOException {
                byte[] nullData = { };
                DerOutputStream responseSeq = new DerOutputStream();
                DerOutputStream srStream = new DerOutputStream();

                // Encode the CertId
                certId.encode(srStream);

                // Next, encode the CertStatus field
                CertStatus csiType = csInfo.getType();
                switch (csiType) {
                    case CERT_STATUS_GOOD:
                        srStream.write(DerValue.createTag(DerValue.TAG_CONTEXT,
                                false, (byte)0), nullData);
                        break;
                    case CERT_STATUS_REVOKED:
                        DerOutputStream revInfo = new DerOutputStream();
                        revInfo.putGeneralizedTime(csInfo.getRevocationTime());
                        CRLReason revReason = csInfo.getRevocationReason();
                        if (revReason != null) {
                            byte[] revDer = new byte[3];
                            revDer[0] = DerValue.tag_Enumerated;
                            revDer[1] = 1;
                            revDer[2] = (byte)revReason.ordinal();
                            revInfo.write(DerValue.createTag(
                                    DerValue.TAG_CONTEXT, true, (byte)0),
                                    revDer);
                        }
                        srStream.write(DerValue.createTag(
                                DerValue.TAG_CONTEXT, true, (byte)1),
                                revInfo);
                        break;
                    case CERT_STATUS_UNKNOWN:
                        srStream.write(DerValue.createTag(DerValue.TAG_CONTEXT,
                                false, (byte)2), nullData);
                        break;
                    default:
                        throw new IOException("Unknown CertStatus: " + csiType);
                }

                // Add the necessary dates
                srStream.putGeneralizedTime(thisUpdate);
                if (lsrNextUpdate != null) {
                    DerOutputStream nuStream = new DerOutputStream();
                    nuStream.putGeneralizedTime(lsrNextUpdate);
                    srStream.write(DerValue.createTag(DerValue.TAG_CONTEXT,
                            true, (byte)0), nuStream);
                }

                // TODO add singleResponse Extension support

                // Add the single response to the response output stream
                responseSeq.write(DerValue.tag_Sequence, srStream);
                return responseSeq.toByteArray();
            }
        }
    }
}
