Bug 974715 - Create more flexible OCSP response generation code. r=briansmith, r=cviecco
authorDavid Keeler <dkeeler@mozilla.com>
Mon, 10 Mar 2014 14:04:31 -0700
changeset 183948 ec47c16ac24d5c973c827358d9832dea0f68ee5f
parent 183947 64d45543afe4458e9f12749155515901cd697f26
child 183949 404edd4f2130a34d00ffbbe5491f94e34b5537fd
push idunknown
push userunknown
push dateunknown
reviewersbriansmith, cviecco
bugs974715
milestone30.0a1
Bug 974715 - Create more flexible OCSP response generation code. r=briansmith, r=cviecco
security/certverifier/moz.build
security/insanity/test/lib/moz.build
security/insanity/test/lib/pkixtestutil.cpp
security/insanity/test/lib/pkixtestutil.h
security/manager/ssl/tests/unit/head_psm.js
security/manager/ssl/tests/unit/test_ocsp_required.js
security/manager/ssl/tests/unit/test_ocsp_stapling.js
security/manager/ssl/tests/unit/tlsserver/cmd/GenerateOCSPResponse.cpp
security/manager/ssl/tests/unit/tlsserver/cmd/Makefile.in
security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
security/manager/ssl/tests/unit/tlsserver/lib/moz.build
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -18,11 +18,17 @@ if not CONFIG['NSS_NO_EV_CERTS']:
     UNIFIED_SOURCES += [
         'ExtendedValidation.cpp',
     ]
 
 LOCAL_INCLUDES += [
     '../insanity/include',
 ]
 
+DIRS += [
+    '../insanity/test/lib',
+]
+
 FAIL_ON_WARNINGS = True
 
+LIBRARY_NAME = 'certverifier'
+
 FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/security/insanity/test/lib/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# Copyright 2013 Mozilla Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+UNIFIED_SOURCES += [
+    'pkixtestutil.cpp',
+]
+
+LIBRARY_NAME = 'pkixtestutil'
+
+LOCAL_INCLUDES += [
+    '../../include',
+    '../../lib',
+]
+
+FAIL_ON_WARNINGS = True
new file mode 100644
--- /dev/null
+++ b/security/insanity/test/lib/pkixtestutil.cpp
@@ -0,0 +1,610 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Copyright 2013 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pkixcheck.h"
+#include "pkixder.h"
+#include "pkixtestutil.h"
+
+#include "cryptohi.h"
+#include "hasht.h"
+#include "pk11pub.h"
+#include "prinit.h"
+#include "secder.h"
+
+namespace insanity { namespace test {
+
+class Output
+{
+public:
+  Output()
+    : numItems(0)
+    , length(0)
+  {
+  }
+
+  // Makes a shallow copy of the input item. All input items must have a
+  // lifetime that extends at least to where Squash is called.
+  der::Result Add(const SECItem* item)
+  {
+    PR_ASSERT(item);
+    PR_ASSERT(item->data);
+
+    if (numItems >= MaxSequenceItems) {
+      return der::Fail(SEC_ERROR_INVALID_ARGS);
+    }
+    if (length + item->len > 65535) {
+      return der::Fail(SEC_ERROR_INVALID_ARGS);
+    }
+
+    contents[numItems] = item;
+    numItems++;
+    length += item->len;
+    return der::Success;
+  }
+
+  SECItem* Squash(PLArenaPool* arena, uint8_t tag)
+  {
+    PR_ASSERT(arena);
+
+    size_t lengthLength = length < 128 ? 1
+                        : length < 256 ? 2
+                                       : 3;
+    size_t totalLength = 1 + lengthLength + length;
+    SECItem* output = SECITEM_AllocItem(arena, nullptr, totalLength);
+    if (!output) {
+      return nullptr;
+    }
+    uint8_t* d = output->data;
+    *d++ = tag;
+    EncodeLength(d, length, lengthLength);
+    d += lengthLength;
+    for (size_t i = 0; i < numItems; i++) {
+      memcpy(d, contents[i]->data, contents[i]->len);
+      d += contents[i]->len;
+    }
+    return output;
+  }
+
+private:
+  void
+  EncodeLength(uint8_t* data, size_t length, size_t lengthLength)
+  {
+    switch (lengthLength) {
+      case 1:
+        data[0] = length;
+        break;
+      case 2:
+        data[0] = 0x81;
+        data[1] = length;
+        break;
+      case 3:
+        data[0] = 0x82;
+        data[1] = length / 256;
+        data[2] = length % 256;
+        break;
+      default:
+        PR_NOT_REACHED("EncodeLength: bad lengthLength");
+        PR_Abort();
+    }
+  }
+
+  static const size_t MaxSequenceItems = 5;
+  const SECItem* contents[MaxSequenceItems];
+  size_t numItems;
+  size_t length;
+
+  Output(const Output&) /* = delete */;
+  void operator=(const Output&) /* = delete */;
+};
+
+static SECItem* ResponseBytes(OCSPResponseContext& context);
+static SECItem* BasicOCSPResponse(OCSPResponseContext& context);
+static SECItem* ResponseData(OCSPResponseContext& context);
+static SECItem* ResponderID(OCSPResponseContext& context);
+static SECItem* KeyHash(OCSPResponseContext& context);
+static SECItem* SingleResponse(OCSPResponseContext& context);
+static SECItem* CertID(OCSPResponseContext& context);
+static SECItem* CertStatus(OCSPResponseContext& context);
+
+static SECItem*
+EncodeNested(PLArenaPool* arena, uint8_t tag, SECItem* inner)
+{
+  Output output;
+  if (output.Add(inner) != der::Success) {
+    return nullptr;
+  }
+  return output.Squash(arena, tag);
+}
+
+// A return value of 0 is an error, but this should never happen in practice
+// because this function aborts in that case.
+static size_t
+HashAlgorithmToLength(SECOidTag hashAlg)
+{
+  switch (hashAlg) {
+    case SEC_OID_SHA1:
+      return SHA1_LENGTH;
+    case SEC_OID_SHA256:
+      return SHA256_LENGTH;
+    case SEC_OID_SHA384:
+      return SHA384_LENGTH;
+    case SEC_OID_SHA512:
+      return SHA512_LENGTH;
+    default:
+      PR_NOT_REACHED("HashAlgorithmToLength: bad hashAlg");
+      PR_Abort();
+  }
+  return 0;
+}
+
+static SECItem*
+HashedOctetString(PLArenaPool* arena, const SECItem* bytes, SECOidTag hashAlg)
+{
+  size_t hashLen = HashAlgorithmToLength(hashAlg);
+  if (hashLen == 0) {
+    return nullptr;
+  }
+  SECItem* hashBuf = SECITEM_AllocItem(arena, nullptr, hashLen);
+  if (!hashBuf) {
+    return nullptr;
+  }
+  if (PK11_HashBuf(hashAlg, hashBuf->data, bytes->data, bytes->len)
+        != SECSuccess) {
+    return nullptr;
+  }
+
+  return EncodeNested(arena, der::OCTET_STRING, hashBuf);
+}
+
+static SECItem*
+KeyHashHelper(PLArenaPool* arena, const CERTCertificate* cert)
+{
+  // We only need a shallow copy here.
+  SECItem spk = cert->subjectPublicKeyInfo.subjectPublicKey;
+  DER_ConvertBitString(&spk); // bits to bytes
+  return HashedOctetString(arena, &spk, SEC_OID_SHA1);
+}
+
+static SECItem*
+AlgorithmIdentifier(PLArenaPool* arena, SECOidTag algTag)
+{
+  SECAlgorithmIDStr aid;
+  aid.algorithm.data = nullptr;
+  aid.algorithm.len = 0;
+  aid.parameters.data = nullptr;
+  aid.parameters.len = 0;
+  if (SECOID_SetAlgorithmID(arena, &aid, algTag, nullptr) != SECSuccess) {
+    return nullptr;
+  }
+  static const SEC_ASN1Template algorithmIDTemplate[] = {
+    { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(SECAlgorithmID) },
+    { SEC_ASN1_OBJECT_ID, offsetof(SECAlgorithmID, algorithm) },
+    { SEC_ASN1_OPTIONAL | SEC_ASN1_ANY, offsetof(SECAlgorithmID, parameters) },
+    { 0 }
+  };
+  SECItem* algorithmID = SEC_ASN1EncodeItem(arena, nullptr, &aid,
+                                            algorithmIDTemplate);
+  return algorithmID;
+}
+
+static SECItem*
+PRTimeToEncodedTime(PLArenaPool* arena, PRTime time)
+{
+  SECItem derTime;
+  if (DER_TimeToGeneralizedTimeArena(arena, &derTime, time) != SECSuccess) {
+    return nullptr;
+  }
+  return EncodeNested(arena, der::GENERALIZED_TIME, &derTime);
+}
+
+SECItem*
+CreateEncodedOCSPResponse(OCSPResponseContext& context)
+{
+  if (!context.arena || !context.cert || !context.issuerCert ||
+      !context.signerCert) {
+    PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
+    return nullptr;
+  }
+
+  // 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
+  // }
+  SECItem* responseStatus = SECITEM_AllocItem(context.arena, nullptr, 3);
+  if (!responseStatus) {
+    return nullptr;
+  }
+  responseStatus->data[0] = der::ENUMERATED;
+  responseStatus->data[1] = 1;
+  responseStatus->data[2] = context.responseStatus;
+
+  SECItem* responseBytes = ResponseBytes(context);
+  if (!responseBytes) {
+    return nullptr;
+  }
+
+  SECItem* responseBytesNested = EncodeNested(context.arena,
+                                              der::CONSTRUCTED |
+                                              der::CONTEXT_SPECIFIC |
+                                              0,
+                                              responseBytes);
+  if (!responseBytesNested) {
+    return nullptr;
+  }
+
+  Output output;
+  if (output.Add(responseStatus) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(responseBytesNested) != der::Success) {
+    return nullptr;
+  }
+  return output.Squash(context.arena, der::SEQUENCE);
+}
+
+// ResponseBytes ::= SEQUENCE {
+//    responseType            OBJECT IDENTIFIER,
+//    response                OCTET STRING }
+SECItem*
+ResponseBytes(OCSPResponseContext& context)
+{
+  // Includes tag and length
+  static const uint8_t id_pkix_ocsp_basic_encoded[] = {
+    0x06, 0x09, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01
+  };
+  SECItem id_pkix_ocsp_basic = {
+    siBuffer,
+    const_cast<uint8_t*>(id_pkix_ocsp_basic_encoded),
+    PR_ARRAY_SIZE(id_pkix_ocsp_basic_encoded)
+  };
+  SECItem* response = BasicOCSPResponse(context);
+  if (!response) {
+    return nullptr;
+  }
+  SECItem* responseNested = EncodeNested(context.arena, der::OCTET_STRING,
+                                         response);
+  if (!responseNested) {
+    return nullptr;
+  }
+
+  Output output;
+  if (output.Add(&id_pkix_ocsp_basic) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(responseNested) != der::Success) {
+    return nullptr;
+  }
+  return output.Squash(context.arena, der::SEQUENCE);
+}
+
+// BasicOCSPResponse ::= SEQUENCE {
+//   tbsResponseData          ResponseData,
+//   signatureAlgorithm       AlgorithmIdentifier,
+//   signature                BIT STRING,
+//   certs                [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
+SECItem*
+BasicOCSPResponse(OCSPResponseContext& context)
+{
+  SECItem* tbsResponseData = ResponseData(context);
+  if (!tbsResponseData) {
+    return nullptr;
+  }
+
+
+  pkix::ScopedPtr<SECKEYPrivateKey, SECKEY_DestroyPrivateKey> privKey(
+    PK11_FindKeyByAnyCert(context.signerCert.get(), nullptr));
+  if (!privKey) {
+    return nullptr;
+  }
+  SECOidTag signatureAlgTag = SEC_GetSignatureAlgorithmOidTag(privKey->keyType,
+                                                              SEC_OID_SHA1);
+  if (signatureAlgTag == SEC_OID_UNKNOWN) {
+    return nullptr;
+  }
+  SECItem* signatureAlgorithm = AlgorithmIdentifier(context.arena,
+                                                    signatureAlgTag);
+  if (!signatureAlgorithm) {
+    return nullptr;
+  }
+
+  // SEC_SignData doesn't take an arena parameter, so we have to manage
+  // the memory allocated in signature.
+  SECItem signature;
+  if (SEC_SignData(&signature, tbsResponseData->data, tbsResponseData->len,
+                   privKey.get(), signatureAlgTag) != SECSuccess)
+  {
+    return nullptr;
+  }
+  // We have to add a byte at the beginning indicating no unused bits.
+  // TODO: add ability to have signatures of bit length not divisible by 8,
+  // resulting in unused bits in the bitstring encoding
+  SECItem* prefixedSignature = SECITEM_AllocItem(context.arena, nullptr,
+                                                 signature.len + 1);
+  if (!prefixedSignature) {
+    SECITEM_FreeItem(&signature, false);
+    return nullptr;
+  }
+  prefixedSignature->data[0] = 0;
+  memcpy(prefixedSignature->data + 1, signature.data, signature.len);
+  SECITEM_FreeItem(&signature, false);
+  if (context.badSignature) {
+    PR_ASSERT(prefixedSignature->len > 8);
+    prefixedSignature->data[8]++;
+  }
+  SECItem* signatureNested = EncodeNested(context.arena, der::BIT_STRING,
+                                          prefixedSignature);
+  if (!signatureNested) {
+    return nullptr;
+  }
+
+  // TODO(bug 980538): certificates
+  Output output;
+  if (output.Add(tbsResponseData) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(signatureAlgorithm) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(signatureNested) != der::Success) {
+    return nullptr;
+  }
+  return output.Squash(context.arena, der::SEQUENCE);
+}
+
+// ResponseData ::= SEQUENCE {
+//    version             [0] EXPLICIT Version DEFAULT v1,
+//    responderID             ResponderID,
+//    producedAt              GeneralizedTime,
+//    responses               SEQUENCE OF SingleResponse,
+//    responseExtensions  [1] EXPLICIT Extensions OPTIONAL }
+SECItem*
+ResponseData(OCSPResponseContext& context)
+{
+  SECItem* responderID = ResponderID(context);
+  if (!responderID) {
+    return nullptr;
+  }
+  SECItem* producedAtEncoded = PRTimeToEncodedTime(context.arena,
+                                                   context.producedAt);
+  if (!producedAtEncoded) {
+    return nullptr;
+  }
+  SECItem* responses = SingleResponse(context);
+  if (!responses) {
+    return nullptr;
+  }
+  SECItem* responsesNested = EncodeNested(context.arena, der::SEQUENCE,
+                                          responses);
+  if (!responsesNested) {
+    return nullptr;
+  }
+
+  Output output;
+  if (output.Add(responderID) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(producedAtEncoded) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(responsesNested) != der::Success) {
+    return nullptr;
+  }
+  return output.Squash(context.arena, der::SEQUENCE);
+}
+
+// ResponderID ::= CHOICE {
+//    byName              [1] Name,
+//    byKey               [2] KeyHash }
+// }
+SECItem*
+ResponderID(OCSPResponseContext& context)
+{
+  SECItem* contents = nullptr;
+  if (context.responderIDType == OCSPResponseContext::ByName) {
+    contents = &context.signerCert->derSubject;
+  } else if (context.responderIDType == OCSPResponseContext::ByKeyHash) {
+    contents = KeyHash(context);
+    if (!contents) {
+      return nullptr;
+    }
+  } else {
+    return nullptr;
+  }
+
+  return EncodeNested(context.arena,
+                      der::CONSTRUCTED |
+                      der::CONTEXT_SPECIFIC |
+                      context.responderIDType,
+                      contents);
+}
+
+// KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
+//                          -- (i.e., the SHA-1 hash of the value of the
+//                          -- BIT STRING subjectPublicKey [excluding
+//                          -- the tag, length, and number of unused
+//                          -- bits] in the responder's certificate)
+SECItem*
+KeyHash(OCSPResponseContext& context)
+{
+  return KeyHashHelper(context.arena, context.signerCert.get());
+}
+
+// SingleResponse ::= SEQUENCE {
+//    certID                  CertID,
+//    certStatus              CertStatus,
+//    thisUpdate              GeneralizedTime,
+//    nextUpdate          [0] EXPLICIT GeneralizedTime OPTIONAL,
+//    singleExtensions    [1] EXPLICIT Extensions OPTIONAL }
+SECItem*
+SingleResponse(OCSPResponseContext& context)
+{
+  SECItem* certID = CertID(context);
+  if (!certID) {
+    return nullptr;
+  }
+  SECItem* certStatus = CertStatus(context);
+  if (!certStatus) {
+    return nullptr;
+  }
+  SECItem* thisUpdateEncoded = PRTimeToEncodedTime(context.arena,
+                                                   context.thisUpdate);
+  if (!thisUpdateEncoded) {
+    return nullptr;
+  }
+  SECItem* nextUpdateEncodedNested = nullptr;
+  if (context.includeNextUpdate) {
+    SECItem* nextUpdateEncoded = PRTimeToEncodedTime(context.arena,
+                                                     context.nextUpdate);
+    if (!nextUpdateEncoded) {
+      return nullptr;
+    }
+    nextUpdateEncodedNested = EncodeNested(context.arena,
+                                           der::CONSTRUCTED |
+                                           der::CONTEXT_SPECIFIC |
+                                           0,
+                                           nextUpdateEncoded);
+    if (!nextUpdateEncodedNested) {
+      return nullptr;
+    }
+  }
+
+  Output output;
+  if (output.Add(certID) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(certStatus) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(thisUpdateEncoded) != der::Success) {
+    return nullptr;
+  }
+  if (nextUpdateEncodedNested) {
+    if (output.Add(nextUpdateEncodedNested) != der::Success) {
+      return nullptr;
+    }
+  }
+  return output.Squash(context.arena, der::SEQUENCE);
+}
+
+// CertID          ::=     SEQUENCE {
+//        hashAlgorithm       AlgorithmIdentifier,
+//        issuerNameHash      OCTET STRING, -- Hash of issuer's DN
+//        issuerKeyHash       OCTET STRING, -- Hash of issuer's public key
+//        serialNumber        CertificateSerialNumber }
+SECItem*
+CertID(OCSPResponseContext& context)
+{
+  SECItem* hashAlgorithm = AlgorithmIdentifier(context.arena,
+                                               context.certIDHashAlg);
+  if (!hashAlgorithm) {
+    return nullptr;
+  }
+  SECItem* issuerNameHash = HashedOctetString(context.arena,
+                                              &context.issuerCert->derSubject,
+                                              context.certIDHashAlg);
+  if (!issuerNameHash) {
+    return nullptr;
+  }
+  SECItem* issuerKeyHash = KeyHashHelper(context.arena,
+                                         context.issuerCert.get());
+  if (!issuerKeyHash) {
+    return nullptr;
+  }
+  static const SEC_ASN1Template serialTemplate[] = {
+    { SEC_ASN1_INTEGER, offsetof(CERTCertificate, serialNumber) },
+    { 0 }
+  };
+  SECItem* serialNumber = SEC_ASN1EncodeItem(context.arena, nullptr,
+                                             context.cert.get(),
+                                             serialTemplate);
+  if (!serialNumber) {
+    return nullptr;
+  }
+
+  Output output;
+  if (output.Add(hashAlgorithm) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(issuerNameHash) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(issuerKeyHash) != der::Success) {
+    return nullptr;
+  }
+  if (output.Add(serialNumber) != der::Success) {
+    return nullptr;
+  }
+  return output.Squash(context.arena, der::SEQUENCE);
+}
+
+// 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
+//
+SECItem*
+CertStatus(OCSPResponseContext& context)
+{
+  switch (context.certStatus) {
+    // Both good and unknown are ultimately represented as NULL - the only
+    // difference is in the tag that identifies them.
+    case 0:
+    case 2:
+    {
+      SECItem* status = SECITEM_AllocItem(context.arena, nullptr, 2);
+      if (!status) {
+        return nullptr;
+      }
+      status->data[0] = der::CONTEXT_SPECIFIC | context.certStatus;
+      status->data[1] = 0;
+      return status;
+    }
+    case 1:
+    {
+      SECItem* revocationTime = PRTimeToEncodedTime(context.arena,
+                                                    context.revocationTime);
+      if (!revocationTime) {
+        return nullptr;
+      }
+      // TODO(bug 980536): add support for revocationReason
+      return EncodeNested(context.arena,
+                          der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 1,
+                          revocationTime);
+    }
+    default:
+      PR_NOT_REACHED("CertStatus: bad context.certStatus");
+      PR_Abort();
+  }
+  return nullptr;
+}
+
+} } // namespace insanity::test
new file mode 100644
--- /dev/null
+++ b/security/insanity/test/lib/pkixtestutil.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Copyright 2013 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef insanity_test__pkixtestutils_h
+#define insanity_test__pkixtestutils_h
+
+#include "insanity/ScopedPtr.h"
+#include "insanity/pkixtypes.h"
+#include "seccomon.h"
+
+namespace insanity { namespace test {
+
+class OCSPResponseContext
+{
+public:
+  PLArenaPool* arena;
+  // TODO(bug 980538): add a way to specify what certificates are included.
+  pkix::ScopedCERTCertificate cert; // The subject of the OCSP response
+  pkix::ScopedCERTCertificate issuerCert; // The issuer of the subject
+  pkix::ScopedCERTCertificate signerCert; // This cert signs the response
+  uint8_t responseStatus; // See the OCSPResponseStatus enum in rfc 6960
+  // TODO(bug 979070): add ability to generate a response with no responseBytes
+
+  // The following fields are on a per-SingleResponse basis. In the future we
+  // may support including multiple SingleResponses per response.
+  PRTime producedAt;
+  PRTime thisUpdate;
+  PRTime nextUpdate;
+  bool includeNextUpdate;
+  SECOidTag certIDHashAlg;
+  uint8_t certStatus;     // See the CertStatus choice in rfc 6960
+  PRTime revocationTime; // For certStatus == revoked
+  bool badSignature; // If true, alter the signature to fail verification
+
+  enum ResponderIDType {
+    ByName = 1,
+    ByKeyHash = 2
+  };
+  ResponderIDType responderIDType;
+};
+
+// The return value, if non-null, is owned by the arena in the context
+// and MUST NOT be freed.
+// This function does its best to respect the NSPR error code convention
+// (that is, if it returns null, calling PR_GetError() will return the
+// error of the failed operation). However, this is not guaranteed.
+SECItem* CreateEncodedOCSPResponse(OCSPResponseContext& context);
+
+} } // namespace insanity::test
+
+#endif // insanity_test__pkixtestutils_h
--- a/security/manager/ssl/tests/unit/head_psm.js
+++ b/security/manager/ssl/tests/unit/head_psm.js
@@ -39,16 +39,17 @@ const SEC_ERROR_OCSP_TRY_SERVER_LATER   
 const SEC_ERROR_OCSP_REQUEST_NEEDS_SIG                  = SEC_ERROR_BASE + 123;
 const SEC_ERROR_OCSP_UNAUTHORIZED_REQUEST               = SEC_ERROR_BASE + 124;
 const SEC_ERROR_OCSP_UNKNOWN_CERT                       = SEC_ERROR_BASE + 126; // -8066
 const SEC_ERROR_OCSP_MALFORMED_RESPONSE                 = SEC_ERROR_BASE + 129;
 const SEC_ERROR_OCSP_UNAUTHORIZED_RESPONSE              = SEC_ERROR_BASE + 130;
 const SEC_ERROR_OCSP_OLD_RESPONSE                       = SEC_ERROR_BASE + 132;
 const SEC_ERROR_OCSP_INVALID_SIGNING_CERT               = SEC_ERROR_BASE + 144;
 const SEC_ERROR_POLICY_VALIDATION_FAILED                = SEC_ERROR_BASE + 160; // -8032
+const SEC_ERROR_OCSP_BAD_SIGNATURE                      = SEC_ERROR_BASE + 157;
 const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED       = SEC_ERROR_BASE + 176;
 
 const SSL_ERROR_BAD_CERT_DOMAIN                         = SSL_ERROR_BASE +  12;
 
 // Supported Certificate Usages
 const certificateUsageSSLClient              = 0x0001;
 const certificateUsageSSLServer              = 0x0002;
 const certificateUsageSSLCA                  = 0x0008;
--- a/security/manager/ssl/tests/unit/test_ocsp_required.js
+++ b/security/manager/ssl/tests/unit/test_ocsp_required.js
@@ -44,23 +44,19 @@ function add_tests_in_mode(useInsanity)
 {
   add_test(function () {
     Services.prefs.setBoolPref("security.use_insanity_verification",
                                useInsanity);
     run_next_test();
   });
 
   add_connection_test("ocsp-stapling-none.example.com",
-                      getXPCOMStatusFromNSS(SEC_ERROR_OCSP_INVALID_SIGNING_CERT));
-  // bug 964493 - using a cached OCSP response with a bad signature would cause
-  // the verification library to return a failure error code without calling
-  // PORT_SetError with the specific error, violating the expectations
-  // of the error handling code.
+                      getXPCOMStatusFromNSS(SEC_ERROR_OCSP_BAD_SIGNATURE));
   add_connection_test("ocsp-stapling-none.example.com",
-                      getXPCOMStatusFromNSS(SEC_ERROR_OCSP_INVALID_SIGNING_CERT));
+                      getXPCOMStatusFromNSS(SEC_ERROR_OCSP_BAD_SIGNATURE));
   add_test(function () {
     // XXX(bug 915932): special case for insanity::pkix due to the temporary
     // lack of an OCSP cache.
     do_check_eq(gOCSPRequestCount, useInsanity ? 2 : 1);
     gOCSPRequestCount = 0;
     run_next_test();
   });
 }
--- a/security/manager/ssl/tests/unit/test_ocsp_stapling.js
+++ b/security/manager/ssl/tests/unit/test_ocsp_stapling.js
@@ -66,18 +66,20 @@ function add_tests_in_mode(useInsanity, 
 
   // The stapled response is from a CA that is trusted but did not issue the
   // server's certificate.
   add_test(function() {
     certDB.setCertTrust(otherTestCA, Ci.nsIX509Cert.CA_CERT,
                         Ci.nsIX509CertDB.TRUSTED_SSL);
     run_next_test();
   });
+  // TODO(bug 979055): When using ByName instead of ByKey, the error here is
+  // SEC_ERROR_OCSP_UNAUTHORIZED_RESPONSE. We should be testing both cases.
   add_ocsp_test("ocsp-stapling-good-other-ca.example.com",
-                getXPCOMStatusFromNSS(SEC_ERROR_OCSP_UNAUTHORIZED_RESPONSE),
+                getXPCOMStatusFromNSS(SEC_ERROR_OCSP_INVALID_SIGNING_CERT),
                 true);
 
   // TODO: Test the case where the signing cert can't be found at all, which
   // will result in SEC_ERROR_BAD_DATABASE in the NSS classic case.
 
   add_ocsp_test("ocsp-stapling-malformed.example.com",
                 getXPCOMStatusFromNSS(SEC_ERROR_OCSP_MALFORMED_REQUEST), true);
   add_ocsp_test("ocsp-stapling-srverr.example.com",
--- a/security/manager/ssl/tests/unit/tlsserver/cmd/GenerateOCSPResponse.cpp
+++ b/security/manager/ssl/tests/unit/tlsserver/cmd/GenerateOCSPResponse.cpp
@@ -157,10 +157,8 @@ main(int argc, char* argv[])
 
     if (!WriteResponse(filename, &response->items[0])) {
       PR_fprintf(PR_STDERR, "Failed to write file %s\n", filename);
       exit(EXIT_FAILURE);
     }
   }
   return 0;
 }
-
-
--- a/security/manager/ssl/tests/unit/tlsserver/cmd/Makefile.in
+++ b/security/manager/ssl/tests/unit/tlsserver/cmd/Makefile.in
@@ -5,13 +5,15 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include $(topsrcdir)/config/config.mk
 
 LIBS = \
   $(NSPR_LIBS) \
   $(NSS_LIBS) \
   $(MOZALLOC_LIB) \
+  ../../../../../../certverifier/$(LIB_PREFIX)certverifier.$(LIB_SUFFIX) \
+  ../../../../../../insanity/test/lib/$(LIB_PREFIX)pkixtestutil.$(LIB_SUFFIX) \
   ../lib/$(LIB_PREFIX)tlsserver.$(LIB_SUFFIX) \
   $(NULL)
 
 DEFINES += $(TK_CFLAGS)
 
--- a/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
+++ b/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
@@ -1,18 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "OCSPCommon.h"
 
 #include <stdio.h>
 
+#include "ScopedNSSTypes.h"
 #include "TLSServer.h"
-#include "ScopedNSSTypes.h"
+#include "pkixtestutil.h"
 #include "secerr.h"
 
 using namespace mozilla;
 using namespace mozilla::test;
 
 
 SECItemArray *
 GetOCSPResponseForType(OCSPResponseType aORT, CERTCertificate *aCert,
@@ -21,188 +22,108 @@ GetOCSPResponseForType(OCSPResponseType 
   if (aORT == ORTNone) {
     if (gDebugLevel >= DEBUG_WARNINGS) {
       fprintf(stderr, "GetOCSPResponseForType called with type ORTNone, "
                       "which makes no sense.\n");
     }
     return nullptr;
   }
 
+  if (aORT == ORTEmpty) {
+    SECItemArray* arr = SECITEM_AllocArray(aArena, nullptr, 1);
+    arr->items[0].data = nullptr;
+    arr->items[0].len = 0;
+    return arr;
+  }
+
   PRTime now = PR_Now();
-  ScopedCERTOCSPCertID id(CERT_CreateOCSPCertID(aCert, now));
-  if (!id) {
-    PrintPRError("CERT_CreateOCSPCertID failed");
+  PRTime oneDay = 60*60*24 * (PRTime)PR_USEC_PER_SEC;
+  PRTime oldNow = now - (8 * oneDay);
+
+  insanity::test::OCSPResponseContext context;
+  context.arena = aArena;
+  context.cert = CERT_DupCertificate(aCert);
+  context.issuerCert = nullptr;
+  context.signerCert = nullptr;
+  context.responseStatus = 0;
+
+  context.producedAt = now;
+  context.thisUpdate = now;
+  context.nextUpdate = now + 10 * PR_USEC_PER_SEC;
+  context.includeNextUpdate = true;
+  context.certIDHashAlg = SEC_OID_SHA1;
+  context.certStatus = 0;
+  context.revocationTime = 0;
+  context.badSignature = false;
+  context.responderIDType = insanity::test::OCSPResponseContext::ByKeyHash;
+
+  if (aORT == ORTGoodOtherCert) {
+    context.cert = PK11_FindCertFromNickname(aAdditionalCertName, nullptr);
+    if (!context.cert) {
+      PrintPRError("PK11_FindCertFromNickname failed");
+      return nullptr;
+    }
+  }
+  // XXX CERT_FindCertIssuer uses the old, deprecated path-building logic
+  context.issuerCert = CERT_FindCertIssuer(aCert, now, certUsageSSLCA);
+  if (!context.issuerCert) {
+    PrintPRError("CERT_FindCertIssuer failed");
     return nullptr;
   }
-  PRTime nextUpdate = now + 10 * PR_USEC_PER_SEC;
-  PRTime oneDay = 60*60*24 * (PRTime)PR_USEC_PER_SEC;
-  PRTime expiredTime = now - oneDay;
-  PRTime oldNow = now - (8 * oneDay);
-  PRTime oldNextUpdate = oldNow + 10 * PR_USEC_PER_SEC;
-
-  CERTOCSPSingleResponse *sr = nullptr;
-  switch (aORT) {
-    case ORTGood:
-    case ORTGoodOtherCA:
-    case ORTBadSignature:
-      sr = CERT_CreateOCSPSingleResponseGood(aArena, id, now, &nextUpdate);
-      if (!sr) {
-        PrintPRError("CERT_CreateOCSPSingleResponseGood failed");
-        return nullptr;
-      }
-      id.forget(); // owned by sr now
-      break;
-    case ORTRevoked:
-      sr = CERT_CreateOCSPSingleResponseRevoked(aArena, id, now, &nextUpdate,
-                                                expiredTime, nullptr);
-      if (!sr) {
-        PrintPRError("CERT_CreateOCSPSingleResponseRevoked failed");
-        return nullptr;
-      }
-      id.forget(); // owned by sr now
-      break;
-    case ORTUnknown:
-      sr = CERT_CreateOCSPSingleResponseUnknown(aArena, id, now, &nextUpdate);
-      if (!sr) {
-        PrintPRError("CERT_CreateOCSPSingleResponseUnknown failed");
-        return nullptr;
-      }
-      id.forget(); // owned by sr now
-      break;
-    case ORTExpired:
-    case ORTExpiredFreshCA:
-      sr = CERT_CreateOCSPSingleResponseGood(aArena, id, oldNow, &oldNextUpdate);
-      if (!sr) {
-        PrintPRError("CERT_CreateOCSPSingleResponseGood failed");
-        return nullptr;
-      }
-      id.forget(); // owned by sr now
-      break;
-    case ORTGoodOtherCert:
-    {
-      ScopedCERTCertificate otherCert(
-        PK11_FindCertFromNickname(aAdditionalCertName, nullptr));
-      if (!otherCert) {
-        PrintPRError("PK11_FindCertFromNickname failed");
-        return nullptr;
-      }
-
-      ScopedCERTOCSPCertID otherID(CERT_CreateOCSPCertID(otherCert, now));
-      if (!otherID) {
-        PrintPRError("CERT_CreateOCSPCertID failed");
-        return nullptr;
-      }
-      sr = CERT_CreateOCSPSingleResponseGood(aArena, otherID, now, &nextUpdate);
-      if (!sr) {
-        PrintPRError("CERT_CreateOCSPSingleResponseGood failed");
-        return nullptr;
-      }
-      otherID.forget(); // owned by sr now
-      break;
-    }
-    case ORTEmpty:
-    case ORTMalformed:
-    case ORTSrverr:
-    case ORTTryLater:
-    case ORTNeedsSig:
-    case ORTUnauthorized:
-      break;
-    default:
-      if (gDebugLevel >= DEBUG_ERRORS) {
-        fprintf(stderr, "bad ocsp response type: %d\n", aORT);
-      }
-      return nullptr;
-  }
-
-  ScopedCERTCertificate ca;
   if (aORT == ORTGoodOtherCA) {
-    ca = PK11_FindCertFromNickname(aAdditionalCertName, nullptr);
-    if (!ca) {
+    context.signerCert = PK11_FindCertFromNickname(aAdditionalCertName,
+                                                   nullptr);
+    if (!context.signerCert) {
       PrintPRError("PK11_FindCertFromNickname failed");
       return nullptr;
     }
-  } else if (aORT == ORTBadSignature) {
-    // passing in a null responderCert to CERT_CreateEncodedOCSPSuccessResponse
-    // causes it to generate an invalid signature (by design, for testing).
-    ca = nullptr;
-  } else {
-    // XXX CERT_FindCertIssuer uses the old, deprecated path-building logic
-    ca = CERT_FindCertIssuer(aCert, now, certUsageSSLCA);
-    if (!ca) {
-      PrintPRError("CERT_FindCertIssuer failed");
-      return nullptr;
-    }
   }
-
-  PRTime signTime = now;
-  if (aORT == ORTExpired) {
-    signTime = oldNow;
-  }
-
-  CERTOCSPSingleResponse **responses;
-  SECItem *response = nullptr;
   switch (aORT) {
     case ORTMalformed:
-      response = CERT_CreateEncodedOCSPErrorResponse(
-        aArena, SEC_ERROR_OCSP_MALFORMED_REQUEST);
-      if (!response) {
-        PrintPRError("CERT_CreateEncodedOCSPErrorResponse failed");
-        return nullptr;
-      }
+      context.responseStatus = 1;
       break;
     case ORTSrverr:
-      response = CERT_CreateEncodedOCSPErrorResponse(
-        aArena, SEC_ERROR_OCSP_SERVER_ERROR);
-      if (!response) {
-        PrintPRError("CERT_CreateEncodedOCSPErrorResponse failed");
-        return nullptr;
-      }
+      context.responseStatus = 2;
       break;
     case ORTTryLater:
-      response = CERT_CreateEncodedOCSPErrorResponse(
-        aArena, SEC_ERROR_OCSP_TRY_SERVER_LATER);
-      if (!response) {
-        PrintPRError("CERT_CreateEncodedOCSPErrorResponse failed");
-        return nullptr;
-      }
+      context.responseStatus = 3;
       break;
     case ORTNeedsSig:
-      response = CERT_CreateEncodedOCSPErrorResponse(
-        aArena, SEC_ERROR_OCSP_REQUEST_NEEDS_SIG);
-      if (!response) {
-        PrintPRError("CERT_CreateEncodedOCSPErrorResponse failed");
-        return nullptr;
-      }
+      context.responseStatus = 5;
       break;
     case ORTUnauthorized:
-      response = CERT_CreateEncodedOCSPErrorResponse(
-        aArena, SEC_ERROR_OCSP_UNAUTHORIZED_REQUEST);
-      if (!response) {
-        PrintPRError("CERT_CreateEncodedOCSPErrorResponse failed");
-        return nullptr;
-      }
-      break;
-    case ORTEmpty:
+      context.responseStatus = 6;
       break;
     default:
-      // responses is contained in aArena and will be freed when aArena is
-      responses = PORT_ArenaNewArray(aArena, CERTOCSPSingleResponse *, 2);
-      if (!responses) {
-        PrintPRError("PORT_ArenaNewArray failed");
-        return nullptr;
-      }
-      responses[0] = sr;
-      responses[1] = nullptr;
-      response = CERT_CreateEncodedOCSPSuccessResponse(
-        aArena, ca, ocspResponderID_byName, signTime, responses, nullptr);
-      if (!response) {
-        PrintPRError("CERT_CreateEncodedOCSPSuccessResponse failed");
-        return nullptr;
-      }
+      // context.responseStatus is 0 in all other cases, and it has
+      // already been initialized, above.
       break;
   }
+  if (aORT == ORTExpired || aORT == ORTExpiredFreshCA) {
+    context.thisUpdate = oldNow;
+    context.nextUpdate = oldNow + 10 * PR_USEC_PER_SEC;
+  }
+  if (aORT == ORTRevoked) {
+    context.certStatus = 1;
+  }
+  if (aORT == ORTUnknown) {
+    context.certStatus = 2;
+  }
+  if (aORT == ORTBadSignature) {
+    context.badSignature = true;
+  }
 
-  SECItemArray *arr = SECITEM_AllocArray(aArena, nullptr, 1);
+  if (!context.signerCert) {
+    context.signerCert = CERT_DupCertificate(context.issuerCert.get());
+  }
+
+  SECItem* response = insanity::test::CreateEncodedOCSPResponse(context);
+  if (!response) {
+    PrintPRError("CreateEncodedOCSPResponse failed");
+    return nullptr;
+  }
+
+  SECItemArray* arr = SECITEM_AllocArray(aArena, nullptr, 1);
   arr->items[0].data = response ? response->data : nullptr;
   arr->items[0].len = response ? response->len : 0;
 
   return arr;
 }
--- a/security/manager/ssl/tests/unit/tlsserver/lib/moz.build
+++ b/security/manager/ssl/tests/unit/tlsserver/lib/moz.build
@@ -4,9 +4,14 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 UNIFIED_SOURCES += [
     'OCSPCommon.cpp',
     'TLSServer.cpp',
 ]
 
+LOCAL_INCLUDES += [
+    '../../../../../../insanity/include',
+    '../../../../../../insanity/test/lib',
+]
+
 LIBRARY_NAME = 'tlsserver'