view src/share/classes/sun/security/provider/certpath/OCSPResponse.java @ 9986:6d1d19e80aa0 icedtea-2.6.23 icedtea-2.6.24pre00

Merge jdk7u271-ga
author Andrew John Hughes <gnu.andrew@redhat.com>
date Thu, 03 Sep 2020 04:06:33 +0100
parents e857ed3f6db8 e1e2c267ee83
children
line wrap: on
line source

/*
 * Copyright (c) 2003, 2020, 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.provider.certpath;

import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CRLReason;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.x500.X500Principal;

import sun.misc.HexDumpEncoder;
import sun.security.action.GetIntegerAction;
import sun.security.x509.*;
import sun.security.util.*;

/**
 * This class is used to process an OCSP response.
 * The OCSP Response is defined
 * in RFC 2560 and the ASN.1 encoding is as follows:
 * <pre>
 *
 *  OCSPResponse ::= SEQUENCE {
 *      responseStatus         OCSPResponseStatus,
 *      responseBytes          [0] EXPLICIT ResponseBytes OPTIONAL }
 *
 *   OCSPResponseStatus ::= ENUMERATED {
 *       successful            (0),  --Response has valid confirmations
 *       malformedRequest      (1),  --Illegal confirmation request
 *       internalError         (2),  --Internal error in issuer
 *       tryLater              (3),  --Try again later
 *                                   --(4) is not used
 *       sigRequired           (5),  --Must sign the request
 *       unauthorized          (6)   --Request unauthorized
 *   }
 *
 *   ResponseBytes ::=       SEQUENCE {
 *       responseType   OBJECT IDENTIFIER,
 *       response       OCTET STRING }
 *
 *   BasicOCSPResponse       ::= SEQUENCE {
 *      tbsResponseData      ResponseData,
 *      signatureAlgorithm   AlgorithmIdentifier,
 *      signature            BIT STRING,
 *      certs                [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
 *
 *   The value for signature SHALL be computed on the hash of the DER
 *   encoding ResponseData.
 *
 *   ResponseData ::= SEQUENCE {
 *      version              [0] EXPLICIT Version DEFAULT v1,
 *      responderID              ResponderID,
 *      producedAt               GeneralizedTime,
 *      responses                SEQUENCE OF SingleResponse,
 *      responseExtensions   [1] EXPLICIT Extensions OPTIONAL }
 *
 *   ResponderID ::= CHOICE {
 *      byName               [1] Name,
 *      byKey                [2] KeyHash }
 *
 *   KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
 *   (excluding the tag and length fields)
 *
 *   SingleResponse ::= SEQUENCE {
 *      certID                       CertID,
 *      certStatus                   CertStatus,
 *      thisUpdate                   GeneralizedTime,
 *      nextUpdate         [0]       EXPLICIT GeneralizedTime OPTIONAL,
 *      singleExtensions   [1]       EXPLICIT Extensions OPTIONAL }
 *
 *   CertStatus ::= CHOICE {
 *       good        [0]     IMPLICIT NULL,
 *       revoked     [1]     IMPLICIT RevokedInfo,
 *       unknown     [2]     IMPLICIT UnknownInfo }
 *
 *   RevokedInfo ::= SEQUENCE {
 *       revocationTime              GeneralizedTime,
 *       revocationReason    [0]     EXPLICIT CRLReason OPTIONAL }
 *
 *   UnknownInfo ::= NULL -- this can be replaced with an enumeration
 *
 * </pre>
 *
 * @author      Ram Marti
 */

public final class OCSPResponse {

    public enum ResponseStatus {
        SUCCESSFUL,            // Response has valid confirmations
        MALFORMED_REQUEST,     // Illegal confirmation request
        INTERNAL_ERROR,        // Internal error in issuer
        TRY_LATER,             // Try again later
        UNUSED,                // is not used
        SIG_REQUIRED,          // Must sign the request
        UNAUTHORIZED           // Request unauthorized
    };
    private static final ResponseStatus[] rsvalues = ResponseStatus.values();

    private static final Debug debug = Debug.getInstance("certpath");
    private static final boolean dump = debug != null && Debug.isOn("ocsp");
    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 int CERT_STATUS_GOOD = 0;
    private static final int CERT_STATUS_REVOKED = 1;
    private static final int CERT_STATUS_UNKNOWN = 2;

    // ResponderID CHOICE tags
    private static final int NAME_TAG = 1;
    private static final int KEY_TAG = 2;

    // Object identifier for the OCSPSigning key purpose
    private static final String KP_OCSP_SIGNING_OID = "1.3.6.1.5.5.7.3.9";

    // Default maximum clock skew in milliseconds (15 minutes)
    // allowed when checking validity of OCSP responses
    private static final int DEFAULT_MAX_CLOCK_SKEW = 900000;

    /**
     * Integer value indicating the maximum allowable clock skew,
     * in milliseconds, to be used for the OCSP check.
     */
    private static final int MAX_CLOCK_SKEW = initializeClockSkew();

    /**
     * Initialize the maximum allowable clock skew by getting the OCSP
     * clock skew system property. If the property has not been set, or if its
     * value is negative, set the skew to the default.
     */
    private static int initializeClockSkew() {
        Integer tmp = java.security.AccessController.doPrivileged(
                new GetIntegerAction("com.sun.security.ocsp.clockSkew"));
        if (tmp == null || tmp < 0) {
            return DEFAULT_MAX_CLOCK_SKEW;
        }
        // Convert to milliseconds, as the system property will be
        // specified in seconds
        return tmp * 1000;
    }

    // an array of all of the CRLReasons (used in SingleResponse)
    private static final CRLReason[] values = CRLReason.values();

    private final ResponseStatus responseStatus;
    private final Map<CertId, SingleResponse> singleResponseMap;
    private final AlgorithmId sigAlgId;
    private final byte[] signature;
    private final byte[] tbsResponseData;
    private final byte[] responseNonce;
    private List<X509CertImpl> certs;
    private X509CertImpl signerCert = null;
    private final ResponderId respId;
    private Date producedAtDate = null;
    private final Map<String, java.security.cert.Extension> responseExtensions;

    /*
     * Create an OCSP response from its ASN.1 DER encoding.
     *
     * @param bytes The DER-encoded bytes for an OCSP response
     */
    public OCSPResponse(byte[] bytes) throws IOException {
        if (dump) {
            HexDumpEncoder hexEnc = new HexDumpEncoder();
            debug.println("OCSPResponse bytes...\n\n" +
                hexEnc.encode(bytes) + "\n");
        }
        DerValue der = new DerValue(bytes);
        if (der.tag != DerValue.tag_Sequence) {
            throw new IOException("Bad encoding in OCSP response: " +
                "expected ASN.1 SEQUENCE tag.");
        }
        DerInputStream derIn = der.getData();

        // responseStatus
        int status = derIn.getEnumerated();
        if (status >= 0 && status < rsvalues.length) {
            responseStatus = rsvalues[status];
        } else {
            // unspecified responseStatus
            throw new IOException("Unknown OCSPResponse status: " + status);
        }
        if (debug != null) {
            debug.println("OCSP response status: " + responseStatus);
        }
        if (responseStatus != ResponseStatus.SUCCESSFUL) {
            // no need to continue, responseBytes are not set.
            singleResponseMap = Collections.emptyMap();
            certs = new ArrayList<X509CertImpl>();
            sigAlgId = null;
            signature = null;
            tbsResponseData = null;
            responseNonce = null;
            responseExtensions = Collections.emptyMap();
            respId = null;
            return;
        }

        // responseBytes
        der = derIn.getDerValue();
        if (!der.isContextSpecific((byte)0)) {
            throw new IOException("Bad encoding in responseBytes element " +
                "of OCSP response: expected ASN.1 context specific tag 0.");
        }
        DerValue tmp = der.data.getDerValue();
        if (tmp.tag != DerValue.tag_Sequence) {
            throw new IOException("Bad encoding in responseBytes element " +
                "of OCSP response: expected ASN.1 SEQUENCE tag.");
        }

        // responseType
        derIn = tmp.data;
        ObjectIdentifier responseType = derIn.getOID();
        if (responseType.equals((Object)OCSP_BASIC_RESPONSE_OID)) {
            if (debug != null) {
                debug.println("OCSP response type: basic");
            }
        } else {
            if (debug != null) {
                debug.println("OCSP response type: " + responseType);
            }
            throw new IOException("Unsupported OCSP response type: " +
                                  responseType);
        }

        // BasicOCSPResponse
        DerInputStream basicOCSPResponse =
            new DerInputStream(derIn.getOctetString());

        DerValue[] seqTmp = basicOCSPResponse.getSequence(3);
        if (seqTmp.length < 3) {
            throw new IOException("Unexpected BasicOCSPResponse value");
        }

        DerValue responseData = seqTmp[0];

        // Need the DER encoded ResponseData to verify the signature later
        tbsResponseData = seqTmp[0].toByteArray();

        // tbsResponseData
        if (responseData.tag != DerValue.tag_Sequence) {
            throw new IOException("Bad encoding in tbsResponseData " +
                "element of OCSP response: expected ASN.1 SEQUENCE tag.");
        }
        DerInputStream seqDerIn = responseData.data;
        DerValue seq = seqDerIn.getDerValue();

        // version
        if (seq.isContextSpecific((byte)0)) {
            // seq[0] is version
            if (seq.isConstructed() && seq.isContextSpecific()) {
                //System.out.println ("version is available");
                seq = seq.data.getDerValue();
                int version = seq.getInteger();
                if (seq.data.available() != 0) {
                    throw new IOException("Bad encoding in version " +
                        " element of OCSP response: bad format");
                }
                seq = seqDerIn.getDerValue();
            }
        }

        // responderID
        respId = new ResponderId(seq.toByteArray());
        if (debug != null) {
            debug.println("Responder ID: " + respId);
        }

        // producedAt
        seq = seqDerIn.getDerValue();
        producedAtDate = seq.getGeneralizedTime();
        if (debug != null) {
            debug.println("OCSP response produced at: " + producedAtDate);
        }

        // responses
        DerValue[] singleResponseDer = seqDerIn.getSequence(1);
        singleResponseMap = new HashMap<>(singleResponseDer.length);
        if (debug != null) {
            debug.println("OCSP number of SingleResponses: "
                          + singleResponseDer.length);
        }
        for (DerValue srDer : singleResponseDer) {
            SingleResponse singleResponse = new SingleResponse(srDer);
            singleResponseMap.put(singleResponse.getCertId(), singleResponse);
        }

        // responseExtensions
        Map<String, java.security.cert.Extension> tmpExtMap = new HashMap<>();
        if (seqDerIn.available() > 0) {
            seq = seqDerIn.getDerValue();
            if (seq.isContextSpecific((byte)1)) {
                tmpExtMap = parseExtensions(seq);
            }
        }
        responseExtensions = tmpExtMap;

        // Attach the nonce value if found in the extension map
        Extension nonceExt = (Extension)tmpExtMap.get(
                PKIXExtensions.OCSPNonce_Id.toString());
        responseNonce = (nonceExt != null) ?
                nonceExt.getExtensionValue() : null;
        if (debug != null && responseNonce != null) {
            debug.println("Response nonce: " + Arrays.toString(responseNonce));
        }

        // signatureAlgorithmId
        sigAlgId = AlgorithmId.parse(seqTmp[1]);

        // signature
        signature = seqTmp[2].getBitString();

        // if seq[3] is available , then it is a sequence of certificates
        if (seqTmp.length > 3) {
            // certs are available
            DerValue seqCert = seqTmp[3];
            if (!seqCert.isContextSpecific((byte)0)) {
                throw new IOException("Bad encoding in certs element of " +
                    "OCSP response: expected ASN.1 context specific tag 0.");
            }
            DerValue[] derCerts = seqCert.getData().getSequence(3);
            certs = new ArrayList<X509CertImpl>(derCerts.length);
            try {
                for (int i = 0; i < derCerts.length; i++) {
                    X509CertImpl cert =
                        new X509CertImpl(derCerts[i].toByteArray());
                    certs.add(cert);

                    if (debug != null) {
                        debug.println("OCSP response cert #" + (i + 1) + ": " +
                            cert.getSubjectX500Principal());
                    }
                }
            } catch (CertificateException ce) {
                throw new IOException("Bad encoding in X509 Certificate", ce);
            }
        } else {
            certs = Collections.<X509CertImpl>emptyList();
        }
    }

    void verify(List<CertId> certIds, IssuerInfo issuerInfo,
            X509Certificate responderCert, Date date, byte[] nonce,
            String variant)
        throws CertPathValidatorException
    {
        if (responseStatus != ResponseStatus.SUCCESSFUL) {
            throw new CertPathValidatorException
                ("OCSP response error: " + responseStatus);
        }

        // Check that the response includes a response for all of the
        // certs that were supplied in the request
        for (CertId certId : certIds) {
            SingleResponse sr = getSingleResponse(certId);
            if (sr == null) {
                if (debug != null) {
                    debug.println("No response found for CertId: " + certId);
                }
                throw new CertPathValidatorException(
                    "OCSP response does not include a response for a " +
                    "certificate supplied in the OCSP request");
            }
            if (debug != null) {
                debug.println("Status of certificate (with serial number " +
                    certId.getSerialNumber() + ") is: " + sr.getCertStatus());
            }
        }

        // Locate the signer cert
        if (signerCert == null) {
            // Add the Issuing CA cert and/or Trusted Responder cert to the list
            // of certs from the OCSP response
            try {
                if (issuerInfo.getCertificate() != null) {
                    certs.add(X509CertImpl.toImpl(issuerInfo.getCertificate()));
                }
                if (responderCert != null) {
                    certs.add(X509CertImpl.toImpl(responderCert));
                }
            } catch (CertificateException ce) {
                throw new CertPathValidatorException(
                    "Invalid issuer or trusted responder certificate", ce);
            }

            if (respId.getType() == ResponderId.Type.BY_NAME) {
                X500Principal rName = respId.getResponderName();
                for (X509CertImpl cert : certs) {
                    if (cert.getSubjectX500Principal().equals(rName)) {
                        signerCert = cert;
                        break;
                    }
                }
            } else if (respId.getType() == ResponderId.Type.BY_KEY) {
                KeyIdentifier ridKeyId = respId.getKeyIdentifier();
                for (X509CertImpl cert : certs) {
                    // Match responder's key identifier against the cert's SKID
                    // This will match if the SKID is encoded using the 160-bit
                    // SHA-1 hash method as defined in RFC 5280.
                    KeyIdentifier certKeyId = cert.getSubjectKeyId();
                    if (certKeyId != null && ridKeyId.equals(certKeyId)) {
                        signerCert = cert;
                        break;
                    } else {
                        // The certificate does not have a SKID or may have
                        // been using a different algorithm (ex: see RFC 7093).
                        // Check if the responder's key identifier matches
                        // against a newly generated key identifier of the
                        // cert's public key using the 160-bit SHA-1 method.
                        try {
                            certKeyId = new KeyIdentifier(cert.getPublicKey());
                        } catch (IOException e) {
                            // ignore
                        }
                        if (ridKeyId.equals(certKeyId)) {
                            signerCert = cert;
                            break;
                        }
                    }
                }
            }
        }

        // Check whether the signer cert returned by the responder is trusted
        if (signerCert != null) {
            // Check if the response is signed by the issuing CA
            if (signerCert.getSubjectX500Principal().equals(
                    issuerInfo.getName()) &&
                    signerCert.getPublicKey().equals(
                            issuerInfo.getPublicKey())) {
                if (debug != null) {
                    debug.println("OCSP response is signed by the target's " +
                        "Issuing CA");
                }
                // cert is trusted, now verify the signed response

            // Check if the response is signed by a trusted responder
            } else if (signerCert.equals(responderCert)) {
                if (debug != null) {
                    debug.println("OCSP response is signed by a Trusted " +
                        "Responder");
                }
                // cert is trusted, now verify the signed response

            // Check if the response is signed by an authorized responder
            } else if (signerCert.getIssuerX500Principal().equals(
                    issuerInfo.getName())) {

                // Check for the OCSPSigning key purpose
                try {
                    List<String> keyPurposes = signerCert.getExtendedKeyUsage();
                    if (keyPurposes == null ||
                        !keyPurposes.contains(KP_OCSP_SIGNING_OID)) {
                        throw new CertPathValidatorException(
                            "Responder's certificate not valid for signing " +
                            "OCSP responses");
                    }
                } catch (CertificateParsingException cpe) {
                    // assume cert is not valid for signing
                    throw new CertPathValidatorException(
                        "Responder's certificate not valid for signing " +
                        "OCSP responses", cpe);
                }

                // Check algorithm constraints specified in security property
                // "jdk.certpath.disabledAlgorithms".
                AlgorithmChecker algChecker =
                        new AlgorithmChecker(issuerInfo.getAnchor(), date,
                                variant);
                algChecker.init(false);
                algChecker.check(signerCert, Collections.<String>emptySet());

                // check the validity
                try {
                    if (date == null) {
                        signerCert.checkValidity();
                    } else {
                        signerCert.checkValidity(date);
                    }
                } catch (CertificateException e) {
                    throw new CertPathValidatorException(
                        "Responder's certificate not within the " +
                        "validity period", e);
                }

                // check for revocation
                //
                // A CA may specify that an OCSP client can trust a
                // responder for the lifetime of the responder's
                // certificate. The CA does so by including the
                // extension id-pkix-ocsp-nocheck.
                //
                Extension noCheck =
                    signerCert.getExtension(PKIXExtensions.OCSPNoCheck_Id);
                if (noCheck != null) {
                    if (debug != null) {
                        debug.println("Responder's certificate includes " +
                            "the extension id-pkix-ocsp-nocheck.");
                    }
                } else {
                    // we should do the revocation checking of the
                    // authorized responder in a future update.
                }

                // verify the signature
                try {
                    signerCert.verify(issuerInfo.getPublicKey());
                    if (debug != null) {
                        debug.println("OCSP response is signed by an " +
                            "Authorized Responder");
                    }
                    // cert is trusted, now verify the signed response

                } catch (GeneralSecurityException e) {
                    signerCert = null;
                }
            } else {
                throw new CertPathValidatorException(
                    "Responder's certificate is not authorized to sign " +
                    "OCSP responses");
            }
        }

        // Confirm that the signed response was generated using the public
        // key from the trusted responder cert
        if (signerCert != null) {
            // Check algorithm constraints specified in security property
            // "jdk.certpath.disabledAlgorithms".
            AlgorithmChecker.check(signerCert.getPublicKey(), sigAlgId, variant);

            if (!verifySignature(signerCert)) {
                throw new CertPathValidatorException(
                    "Error verifying OCSP Response's signature");
            }
        } else {
            // Need responder's cert in order to verify the signature
            throw new CertPathValidatorException(
                "Unable to verify OCSP Response's signature");
        }

        if (nonce != null) {
            if (responseNonce != null && !Arrays.equals(nonce, responseNonce)) {
                throw new CertPathValidatorException("Nonces don't match");
            }
        }

        // Check freshness of OCSPResponse
        long now = (date == null) ? System.currentTimeMillis() : date.getTime();
        Date nowPlusSkew = new Date(now + MAX_CLOCK_SKEW);
        Date nowMinusSkew = new Date(now - MAX_CLOCK_SKEW);
        for (SingleResponse sr : singleResponseMap.values()) {
            if (debug != null) {
                String until = "";
                if (sr.nextUpdate != null) {
                    until = " until " + sr.nextUpdate;
                }
                debug.println("OCSP response validity interval is from " +
                        sr.thisUpdate + until);
                debug.println("Checking validity of OCSP response on: " +
                        new Date(now));
            }

            // Check that the test date is within the validity interval:
            //   [ thisUpdate - MAX_CLOCK_SKEW,
            //     MAX(thisUpdate, nextUpdate) + MAX_CLOCK_SKEW ]
            if (nowPlusSkew.before(sr.thisUpdate) ||
                    nowMinusSkew.after(
                    sr.nextUpdate != null ? sr.nextUpdate : sr.thisUpdate))
            {
                throw new CertPathValidatorException(
                                      "Response is unreliable: its validity " +
                                      "interval is out-of-date");
            }
        }
    }

    /**
     * Returns the OCSP ResponseStatus.
     *
     * @return the {@code ResponseStatus} for this OCSP response
     */
    public ResponseStatus getResponseStatus() {
        return responseStatus;
    }

    /*
     * Verify the signature of the OCSP response.
     */
    private boolean verifySignature(X509Certificate cert)
        throws CertPathValidatorException {

        try {
            Signature respSignature = Signature.getInstance(sigAlgId.getName());
            respSignature.initVerify(cert.getPublicKey());
            respSignature.update(tbsResponseData);

            if (respSignature.verify(signature)) {
                if (debug != null) {
                    debug.println("Verified signature of OCSP Response");
                }
                return true;

            } else {
                if (debug != null) {
                    debug.println(
                        "Error verifying signature of OCSP Response");
                }
                return false;
            }
        } catch (InvalidKeyException | NoSuchAlgorithmException |
                 SignatureException e)
        {
            throw new CertPathValidatorException(e);
        }
    }

    /**
     * Returns the SingleResponse of the specified CertId, or null if
     * there is no response for that CertId.
     *
     * @param certId the {@code CertId} for a {@code SingleResponse} to be
     * searched for in the OCSP response.
     *
     * @return the {@code SingleResponse} for the provided {@code CertId},
     * or {@code null} if it is not found.
     */
    public SingleResponse getSingleResponse(CertId certId) {
        return singleResponseMap.get(certId);
    }

    /**
     * Return a set of all CertIds in this {@code OCSPResponse}
     *
     * @return an unmodifiable set containing every {@code CertId} in this
     *      response.
     */
    public Set<CertId> getCertIds() {
        return Collections.unmodifiableSet(singleResponseMap.keySet());
    }

    /*
     * Returns the certificate for the authority that signed the OCSP response.
     */
    X509Certificate getSignerCertificate() {
        return signerCert; // set in verify()
    }

    /**
     * Get the {@code ResponderId} from this {@code OCSPResponse}
     *
     * @return the {@code ResponderId} from this response or {@code null}
     *      if no responder ID is in the body of the response (e.g. a
     *      response with a status other than SUCCESS.
     */
    public ResponderId getResponderId() {
        return respId;
    }

    /**
     * Provide a String representation of an OCSPResponse
     *
     * @return a human-readable representation of the OCSPResponse
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("OCSP Response:\n");
        sb.append("Response Status: ").append(responseStatus).append("\n");
        sb.append("Responder ID: ").append(respId).append("\n");
        sb.append("Produced at: ").append(producedAtDate).append("\n");
        int count = singleResponseMap.size();
        sb.append(count).append(count == 1 ?
                " response:\n" : " responses:\n");
        for (SingleResponse sr : singleResponseMap.values()) {
            sb.append(sr).append("\n");
        }
        if (responseExtensions != null && responseExtensions.size() > 0) {
            count = responseExtensions.size();
            sb.append(count).append(count == 1 ?
                    " extension:\n" : " extensions:\n");
            for (String extId : responseExtensions.keySet()) {
                sb.append(responseExtensions.get(extId)).append("\n");
            }
        }

        return sb.toString();
    }

    /**
     * Build a String-Extension map from DER encoded data.
     * @param derVal A {@code DerValue} object built from a SEQUENCE of
     *      extensions
     *
     * @return a {@code Map} using the OID in string form as the keys.  If no
     *      extensions are found or an empty SEQUENCE is passed in, then
     *      an empty {@code Map} will be returned.
     *
     * @throws IOException if any decoding errors occur.
     */
    private static Map<String, java.security.cert.Extension>
        parseExtensions(DerValue derVal) throws IOException {
        DerValue[] extDer = derVal.data.getSequence(3);
        Map<String, java.security.cert.Extension> extMap =
                new HashMap<>(extDer.length);

        for (DerValue extDerVal : extDer) {
            Extension ext = new Extension(extDerVal);
            if (debug != null) {
                debug.println("Extension: " + ext);
            }
            // We don't support any extensions yet. Therefore, if it
            // is critical we must throw an exception because we
            // don't know how to process it.
            if (ext.isCritical()) {
                throw new IOException("Unsupported OCSP critical extension: " +
                        ext.getExtensionId());
            }
            extMap.put(ext.getId(), ext);
        }

        return extMap;
    }

    /*
     * A class representing a single OCSP response.
     */
    public static final class SingleResponse implements OCSP.RevocationStatus {
        private final CertId certId;
        private final CertStatus certStatus;
        private final Date thisUpdate;
        private final Date nextUpdate;
        private final Date revocationTime;
        private final CRLReason revocationReason;
        private final Map<String, java.security.cert.Extension> singleExtensions;

        private SingleResponse(DerValue der) throws IOException {
            this(der, null);
        }

        private SingleResponse(DerValue der, Date dateCheckedAgainst)
            throws IOException {
            if (der.tag != DerValue.tag_Sequence) {
                throw new IOException("Bad ASN.1 encoding in SingleResponse");
            }
            DerInputStream tmp = der.data;

            certId = new CertId(tmp.getDerValue().data);
            DerValue derVal = tmp.getDerValue();
            short tag = (byte)(derVal.tag & 0x1f);
            if (tag ==  CERT_STATUS_REVOKED) {
                certStatus = CertStatus.REVOKED;
                revocationTime = derVal.data.getGeneralizedTime();
                if (derVal.data.available() != 0) {
                    DerValue dv = derVal.data.getDerValue();
                    tag = (byte)(dv.tag & 0x1f);
                    if (tag == 0) {
                        int reason = dv.data.getEnumerated();
                        // if reason out-of-range just leave as UNSPECIFIED
                        if (reason >= 0 && reason < values.length) {
                            revocationReason = values[reason];
                        } else {
                            revocationReason = CRLReason.UNSPECIFIED;
                        }
                    } else {
                        revocationReason = CRLReason.UNSPECIFIED;
                    }
                } else {
                    revocationReason = CRLReason.UNSPECIFIED;
                }
                // RevokedInfo
                if (debug != null) {
                    debug.println("Revocation time: " + revocationTime);
                    debug.println("Revocation reason: " + revocationReason);
                }
            } else {
                revocationTime = null;
                revocationReason = null;
                if (tag == CERT_STATUS_GOOD) {
                    certStatus = CertStatus.GOOD;
                } else if (tag == CERT_STATUS_UNKNOWN) {
                    certStatus = CertStatus.UNKNOWN;
                } else {
                    throw new IOException("Invalid certificate status");
                }
            }

            thisUpdate = tmp.getGeneralizedTime();
            if (debug != null) {
                debug.println("thisUpdate: " + thisUpdate);
            }

            // Parse optional fields like nextUpdate and singleExtensions
            Date tmpNextUpdate = null;
            Map<String, java.security.cert.Extension> tmpMap = null;

            // Check for the first optional item, it could be nextUpdate
            // [CONTEXT 0] or singleExtensions [CONTEXT 1]
            if (tmp.available() > 0) {
                derVal = tmp.getDerValue();

                // nextUpdate processing
                if (derVal.isContextSpecific((byte)0)) {
                    tmpNextUpdate = derVal.data.getGeneralizedTime();
                    if (debug != null) {
                        debug.println("nextUpdate: " + tmpNextUpdate);
                    }

                    // If more data exists in the singleResponse, it
                    // can only be singleExtensions.  Get this DER value
                    // for processing in the next block
                    derVal = tmp.available() > 0 ? tmp.getDerValue() : null;
                }

                // singleExtensions processing
                if (derVal != null) {
                    if (derVal.isContextSpecific((byte)1)) {
                        tmpMap = parseExtensions(derVal);

                        // There should not be any other items in the
                        // singleResponse at this point.
                        if (tmp.available() > 0) {
                            throw new IOException(tmp.available() +
                                " bytes of additional data in singleResponse");
                        }
                    } else {
                        // Unknown item in the singleResponse
                        throw new IOException("Unsupported singleResponse " +
                            "item, tag = " + String.format("%02X", derVal.tag));
                    }
                }
            }

            nextUpdate = tmpNextUpdate;
            singleExtensions = (tmpMap != null) ? tmpMap :
                    Collections.<String, java.security.cert.Extension>emptyMap();
            if (debug != null) {
                for (java.security.cert.Extension ext :
                        singleExtensions.values()) {
                   debug.println("singleExtension: " + ext);
                }
            }
        }

        /*
         * Return the certificate's revocation status code
         */
        @Override
        public CertStatus getCertStatus() {
            return certStatus;
        }

        /**
         * Get the Cert ID that this SingleResponse is for.
         *
         * @return the {@code CertId} for this {@code SingleResponse}
         */
        public CertId getCertId() {
            return certId;
        }

        /**
         * Get the {@code thisUpdate} field from this {@code SingleResponse}.
         *
         * @return a {@link Date} object containing the thisUpdate date
         */
        public Date getThisUpdate() {
            return (thisUpdate != null ? (Date) thisUpdate.clone() : null);
        }

        /**
         * Get the {@code nextUpdate} field from this {@code SingleResponse}.
         *
         * @return a {@link Date} object containing the nexUpdate date or
         * {@code null} if a nextUpdate field is not present in the response.
         */
        public Date getNextUpdate() {
            return (nextUpdate != null ? (Date) nextUpdate.clone() : null);
        }

        /**
         * Get the {@code revocationTime} field from this
         * {@code SingleResponse}.
         *
         * @return a {@link Date} object containing the revocationTime date or
         * {@code null} if the {@code SingleResponse} does not have a status
         * of {@code REVOKED}.
         */
        @Override
        public Date getRevocationTime() {
            return (revocationTime != null ? (Date) revocationTime.clone() :
                    null);
        }

        /**
         * Get the {@code revocationReason} field for the
         * {@code SingleResponse}.
         *
         * @return a {@link CRLReason} containing the revocation reason, or
         * {@code null} if a revocation reason was not provided or the
         * response status is not {@code REVOKED}.
         */
        @Override
        public CRLReason getRevocationReason() {
            return revocationReason;
        }

        /**
         * Get the {@code singleExtensions} for this {@code SingleResponse}.
         *
         * @return a {@link Map} of {@link Extension} objects, keyed by
         * their OID value in string form.
         */
        @Override
        public Map<String, java.security.cert.Extension> getSingleExtensions() {
            return Collections.unmodifiableMap(singleExtensions);
        }

        /**
         * Construct a string representation of a single OCSP response.
         */
        @Override public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("SingleResponse:\n");
            sb.append(certId);
            sb.append("\nCertStatus: ").append(certStatus).append("\n");
            if (certStatus == CertStatus.REVOKED) {
                sb.append("revocationTime is ");
                sb.append(revocationTime).append("\n");
                sb.append("revocationReason is ");
                sb.append(revocationReason).append("\n");
            }
            sb.append("thisUpdate is ").append(thisUpdate).append("\n");
            if (nextUpdate != null) {
                sb.append("nextUpdate is ").append(nextUpdate).append("\n");
            }
            for (java.security.cert.Extension ext : singleExtensions.values()) {
                sb.append("singleExtension: ");
                sb.append(ext.toString()).append("\n");
            }
            return sb.toString();
        }
    }

    /**
     * Helper class that allows consumers to pass in issuer information.  This
     * will always consist of the issuer's name and public key, but may also
     * contain a certificate if the originating data is in that form.  The
     * trust anchor for the certificate chain will be included for certpath
     * disabled algorithm checking.
     */
    static final class IssuerInfo {
        private final TrustAnchor anchor;
        private final X509Certificate certificate;
        private final X500Principal name;
        private final PublicKey pubKey;

        IssuerInfo(TrustAnchor anchor) {
            this(anchor, (anchor != null) ? anchor.getTrustedCert() : null);
        }

        IssuerInfo(X509Certificate issuerCert) {
            this(null, issuerCert);
        }

        IssuerInfo(TrustAnchor anchor, X509Certificate issuerCert) {
            if (anchor == null && issuerCert == null) {
                throw new NullPointerException("TrustAnchor and issuerCert " +
                        "cannot be null");
            }
            this.anchor = anchor;
            if (issuerCert != null) {
                name = issuerCert.getSubjectX500Principal();
                pubKey = issuerCert.getPublicKey();
                certificate = issuerCert;
            } else {
                name = anchor.getCA();
                pubKey = anchor.getCAPublicKey();
                certificate = anchor.getTrustedCert();
            }
        }

        /**
         * Get the certificate in this IssuerInfo if present.
         *
         * @return the {@code X509Certificate} used to create this IssuerInfo
         * object, or {@code null} if a certificate was not used in its
         * creation.
         */
        X509Certificate getCertificate() {
            return certificate;
        }

        /**
         * Get the name of this issuer.
         *
         * @return an {@code X500Principal} corresponding to this issuer's
         * name.  If derived from an issuer's {@code X509Certificate} this
         * would be equivalent to the certificate subject name.
         */
        X500Principal getName() {
            return name;
        }

        /**
         * Get the public key for this issuer.
         *
         * @return a {@code PublicKey} for this issuer.
         */
        PublicKey getPublicKey() {
            return pubKey;
        }

        /**
         * Get the TrustAnchor for the certificate chain.
         *
         * @return a {@code TrustAnchor}.
         */
        TrustAnchor getAnchor() {
            return anchor;
        }

        /**
         * Create a string representation of this IssuerInfo.
         *
         * @return a {@code String} form of this IssuerInfo object.
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("Issuer Info:\n");
            sb.append("Name: ").append(name.toString()).append("\n");
            sb.append("Public Key:\n").append(pubKey.toString()).append("\n");
            return sb.toString();
        }
    }
}