Пример #1
0
 @Override
 protected void readStatus(ByteBuffer buffer) {
   ResponseStatus responseStatus = ResponseStatus.parseShort(buffer.getShort());
   if (responseStatus == ResponseStatus.UNKNOWN_COMMAND) {
     setException(new UnknownCommandException());
   }
 }
  private List<Loggable> createCommonResponseLoggables(
      Integer responseCode, ResponseStatus responseStatus) {
    List<Loggable> loggables = createCallAwareLoggables();
    loggables.add(createLoggable(WebKeys.CODE, responseCode));
    loggables.add(createLoggable(WebKeys.DURATION, getDuration()));
    loggables.add(createLoggable(WebKeys.STATUS, responseStatus.name()));

    return loggables;
  }
  protected boolean checkUnauthorized(String json) {

    try {
      ResponseStatus responseStatus =
          (ResponseStatus)
              mapper.readValue(
                  mapper.readValue(json, JsonNode.class).findPath(RESPONSE_STATUS).toString(),
                  ResponseStatus.class);
      if ((responseStatus.getErrorCode() != null
              && responseStatus.getErrorCode().equalsIgnoreCase(UNAUTHORIZED))
          || (responseStatus.getMessage() != null
              && responseStatus.getMessage().equalsIgnoreCase(UNAUTHORIZED))) {
        throw new UnauthorizedException();
      }
    } catch (BaseSpaceException ex) {
      throw ex;
    } catch (Throwable t) {

    }
    return false;
  }
 public JSONObject toJSON() throws JSONException {
   final JSONObject ret = new JSONObject();
   ret.put("status", status.toString());
   if (status == ResponseStatus.success) {
     ret.put("message", message);
   } else {
     ret.put("errorMessage", message);
   }
   if (metadata != null) {
     for (final String key : metadata.keySet()) {
       ret.put(key, metadata.get(key));
     }
   }
   return ret;
 }
Пример #5
0
 /**
  * Logs the exception at the appropriate level.
  *
  * @param exception the {@link Exception} that has to be logged.
  */
 private void log(Exception exception) {
   String uri = "unknown";
   RestMethod restMethod = RestMethod.UNKNOWN;
   if (request != null) {
     uri = request.getUri();
     restMethod = request.getRestMethod();
   }
   if (exception instanceof RestServiceException) {
     RestServiceErrorCode errorCode = ((RestServiceException) exception).getErrorCode();
     ResponseStatus responseStatus = ResponseStatus.getResponseStatus(errorCode);
     if (responseStatus == ResponseStatus.InternalServerError) {
       logger.error(
           "Internal error handling request {} with method {}.", uri, restMethod, exception);
     } else {
       logger.trace("Error handling request {} with method {}.", uri, restMethod, exception);
     }
   } else {
     logger.error(
         "Unexpected error handling request {} with method {}.", uri, restMethod, exception);
   }
 }
Пример #6
0
 /**
  * Provided a cause, returns an error response with the right status and error message.
  *
  * @param cause the cause of the error.
  * @return a {@link FullHttpResponse} with the error message that can be sent to the client.
  */
 private FullHttpResponse getErrorResponse(Throwable cause) {
   HttpResponseStatus status;
   StringBuilder errReason = new StringBuilder();
   if (cause instanceof RestServiceException) {
     RestServiceErrorCode restServiceErrorCode = ((RestServiceException) cause).getErrorCode();
     status = getHttpResponseStatus(ResponseStatus.getResponseStatus(restServiceErrorCode));
     if (status == HttpResponseStatus.BAD_REQUEST) {
       errReason.append(" [").append(Utils.getRootCause(cause).getMessage()).append("]");
     }
   } else {
     nettyMetrics.internalServerErrorCount.inc();
     status = HttpResponseStatus.INTERNAL_SERVER_ERROR;
   }
   String fullMsg = "Failure: " + status + errReason;
   logger.trace("Constructed error response for the client - [{}]", fullMsg);
   FullHttpResponse response;
   if (request != null && !request.getRestMethod().equals(RestMethod.HEAD)) {
     response =
         new DefaultFullHttpResponse(
             HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(fullMsg.getBytes()));
   } else {
     // for HEAD, we cannot send the actual body but we need to return what the length would have
     // been if this was GET.
     // https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html (Section 9.4)
     response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
   }
   HttpHeaders.setDate(response, new GregorianCalendar().getTime());
   HttpHeaders.setContentLength(response, fullMsg.length());
   HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
   boolean keepAlive =
       request != null
           && !request.getRestMethod().equals(RestMethod.POST)
           && !CLOSE_CONNECTION_ERROR_STATUSES.contains(status);
   HttpHeaders.setKeepAlive(response, keepAlive);
   return response;
 }
Пример #7
0
/**
 * 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 request
    INTERNAL_ERROR, // Internal error in responder
    TRY_LATER, // Try again later
    UNUSED, // is not used
    SIG_REQUIRED, // Must sign the request
    UNAUTHORIZED // Request unauthorized
  };

  private static ResponseStatus[] rsvalues = ResponseStatus.values();

  private static final Debug debug = Debug.getInstance("certpath");
  private static final boolean dump = false;
  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 seconds, 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 CRLReason[] values = CRLReason.values();

  private final ResponseStatus responseStatus;
  private final Map<CertId, SingleResponse> singleResponseMap;
  private final List<X509CertImpl> certs;
  private final AlgorithmId sigAlgId;
  private final byte[] signature;
  private final byte[] tbsResponseData;
  private final byte[] responseNonce;

  /*
   * Create an OCSP response from its ASN.1 DER encoding.
   */
  OCSPResponse(byte[] bytes) throws IOException {
    if (dump) {
      HexDumpEncoder hexEnc = new HexDumpEncoder();
      System.out.println("OCSPResponse bytes are...");
      System.out.println(hexEnc.encode(bytes));
    }
    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 = Collections.<X509CertImpl>emptyList();
      sigAlgId = null;
      signature = null;
      tbsResponseData = null;
      responseNonce = 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(2);
    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
    short tag = (byte) (seq.tag & 0x1f);
    if (tag == NAME_TAG) {
      if (debug != null) {
        X500Principal responderName = new X500Principal(seq.getData().toByteArray());
        debug.println("OCSP Responder name: " + responderName);
      }
    } else if (tag == KEY_TAG) {
      if (debug != null) {
        byte[] responderKey = seq.getData().getOctetString();
        debug.println("OCSP Responder key: " + Debug.toString(responderKey));
      }
    } else {
      throw new IOException(
          "Bad encoding in responderID element of "
              + "OCSP response: expected ASN.1 context specific tag 0 or 1");
    }

    // producedAt
    seq = seqDerIn.getDerValue();
    if (debug != null) {
      Date producedAtDate = seq.getGeneralizedTime();
      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 (int i = 0; i < singleResponseDer.length; i++) {
      SingleResponse singleResponse = new SingleResponse(singleResponseDer[i]);
      singleResponseMap.put(singleResponse.getCertId(), singleResponse);
    }

    // responseExtensions
    byte[] nonce = null;
    if (seqDerIn.available() > 0) {
      seq = seqDerIn.getDerValue();
      if (seq.isContextSpecific((byte) 1)) {
        DerValue[] responseExtDer = seq.data.getSequence(3);
        for (int i = 0; i < responseExtDer.length; i++) {
          Extension ext = new Extension(responseExtDer[i]);
          if (debug != null) {
            debug.println("OCSP extension: " + ext);
          }
          // Only the NONCE extension is recognized
          if (ext.getExtensionId().equals((Object) OCSP.NONCE_EXTENSION_OID)) {
            nonce = ext.getExtensionValue();
          } else if (ext.isCritical()) {
            throw new IOException("Unsupported OCSP critical extension: " + ext.getExtensionId());
          }
        }
      }
    }
    responseNonce = nonce;

    // 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++) {
          certs.add(new X509CertImpl(derCerts[i].toByteArray()));
        }
      } catch (CertificateException ce) {
        throw new IOException("Bad encoding in X509 Certificate", ce);
      }
    } else {
      certs = Collections.<X509CertImpl>emptyList();
    }
  }

  void verify(List<CertId> certIds, X509Certificate responderCert, Date date, byte[] nonce)
      throws CertPathValidatorException {
    switch (responseStatus) {
      case SUCCESSFUL:
        break;
      case TRY_LATER:
      case INTERNAL_ERROR:
        throw new CertPathValidatorException(
            "OCSP response error: " + responseStatus,
            null,
            null,
            -1,
            BasicReason.UNDETERMINED_REVOCATION_STATUS);
      case UNAUTHORIZED:
      default:
        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());
      }
    }

    // Check whether the cert returned by the responder is trusted
    if (!certs.isEmpty()) {
      X509CertImpl cert = certs.get(0);
      // First check if the cert matches the expected responder cert
      if (cert.equals(responderCert)) {
        // cert is trusted, now verify the signed response

        // Next check if the cert was issued by the responder cert
        // which was set locally.
      } else if (cert.getIssuerX500Principal().equals(responderCert.getSubjectX500Principal())) {

        // Check for the OCSPSigning key purpose
        try {
          List<String> keyPurposes = cert.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(new TrustAnchor(responderCert, null));
        algChecker.init(false);
        algChecker.check(cert, Collections.<String>emptySet());

        // check the validity
        try {
          if (date == null) {
            cert.checkValidity();
          } else {
            cert.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 = cert.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 {
          cert.verify(responderCert.getPublicKey());
          responderCert = cert;
          // cert is trusted, now verify the signed response

        } catch (GeneralSecurityException e) {
          responderCert = 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 (responderCert != null) {
      // Check algorithm constraints specified in security property
      // "jdk.certpath.disabledAlgorithms".
      AlgorithmChecker.check(responderCert.getPublicKey(), sigAlgId);

      if (!verifySignature(responderCert)) {
        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");
    }

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

    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("Response's validity interval is from " + sr.thisUpdate + until);
      }

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

  /** Returns the OCSP ResponseStatus. */
  ResponseStatus getResponseStatus() {
    return responseStatus;
  }

  /*
   * Verify the signature of the OCSP response.
   * The responder's cert is implicitly trusted.
   */
  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.
   */
  SingleResponse getSingleResponse(CertId certId) {
    return singleResponseMap.get(certId);
  }

  /*
   * A class representing a single OCSP response.
   */
  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 {
      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 = CRLReason.UNSPECIFIED;
        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 (tmp.available() == 0) {
        // we are done
        nextUpdate = null;
      } else {
        derVal = tmp.getDerValue();
        tag = (byte) (derVal.tag & 0x1f);
        if (tag == 0) {
          // next update
          nextUpdate = derVal.data.getGeneralizedTime();

          if (tmp.available() == 0) {
            // we are done
          } else {
            derVal = tmp.getDerValue();
            tag = (byte) (derVal.tag & 0x1f);
          }
        } else {
          nextUpdate = null;
        }
      }
      // singleExtensions
      if (tmp.available() > 0) {
        derVal = tmp.getDerValue();
        if (derVal.isContextSpecific((byte) 1)) {
          DerValue[] singleExtDer = derVal.data.getSequence(3);
          singleExtensions = new HashMap<String, java.security.cert.Extension>(singleExtDer.length);
          for (int i = 0; i < singleExtDer.length; i++) {
            Extension ext = new Extension(singleExtDer[i]);
            if (debug != null) {
              debug.println("OCSP single 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());
            }
            singleExtensions.put(ext.getId(), ext);
          }
        } else {
          singleExtensions = Collections.emptyMap();
        }
      } else {
        singleExtensions = Collections.emptyMap();
      }
    }

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

    private CertId getCertId() {
      return certId;
    }

    @Override
    public Date getRevocationTime() {
      return (Date) revocationTime.clone();
    }

    @Override
    public CRLReason getRevocationReason() {
      return revocationReason;
    }

    @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: " + certStatus + "\n");
      if (certStatus == CertStatus.REVOKED) {
        sb.append("revocationTime is " + revocationTime + "\n");
        sb.append("revocationReason is " + revocationReason + "\n");
      }
      sb.append("thisUpdate is " + thisUpdate + "\n");
      if (nextUpdate != null) {
        sb.append("nextUpdate is " + nextUpdate + "\n");
      }
      return sb.toString();
    }
  }
}