Bug 997509 - Heed expired Revoked or Unknown OCSP responses. r=briansmith, a=sledru
authorDavid Keeler <dkeeler@mozilla.com>
Fri, 20 Jun 2014 09:01:57 -0700
changeset 208319 547ce0caa77eb19c2b6a095345719557d734f883
parent 208318 1d30ac49f168060e5c49f3a72d9ba3d087164d0c
child 208320 55ebb748ff9a109160bb7b802a7df6f85ad858ec
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbriansmith, sledru
bugs997509
milestone32.0a2
Bug 997509 - Heed expired Revoked or Unknown OCSP responses. r=briansmith, a=sledru
security/certverifier/NSSCertDBTrustDomain.cpp
security/certverifier/NSSCertDBTrustDomain.h
security/manager/ssl/tests/unit/test_ocsp_stapling_expired.js
security/manager/ssl/tests/unit/tlsserver/cmd/OCSPStaplingServer.cpp
security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.h
security/pkix/include/pkix/pkix.h
security/pkix/lib/pkixocsp.cpp
--- a/security/certverifier/NSSCertDBTrustDomain.cpp
+++ b/security/certverifier/NSSCertDBTrustDomain.cpp
@@ -207,41 +207,48 @@ NSSCertDBTrustDomain::CheckRevocation(
   if (endEntityOrCA == EndEntityOrCA::MustBeCA) {
     maxOCSPLifetimeInDays = 365;
   }
 
   // If we have a stapled OCSP response then the verification of that response
   // determines the result unless the OCSP response is expired. We make an
   // exception for expired responses because some servers, nginx in particular,
   // are known to serve expired responses due to bugs.
+  // We keep track of the result of verifying the stapled response but don't
+  // immediately return failure if the response has expired.
+  PRErrorCode stapledOCSPResponseErrorCode = 0;
   if (stapledOCSPResponse) {
     PR_ASSERT(endEntityOrCA == EndEntityOrCA::MustBeEndEntity);
+    bool expired;
     SECStatus rv = VerifyAndMaybeCacheEncodedOCSPResponse(cert, issuerCert,
                                                           time,
                                                           maxOCSPLifetimeInDays,
                                                           stapledOCSPResponse,
-                                                          ResponseWasStapled);
+                                                          ResponseWasStapled,
+                                                          expired);
     if (rv == SECSuccess) {
       // stapled OCSP response present and good
       Telemetry::Accumulate(Telemetry::SSL_OCSP_STAPLING, 1);
       PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
              ("NSSCertDBTrustDomain: stapled OCSP response: good"));
       return rv;
     }
-    if (PR_GetError() != SEC_ERROR_OCSP_OLD_RESPONSE) {
+    stapledOCSPResponseErrorCode = PR_GetError();
+    if (stapledOCSPResponseErrorCode == SEC_ERROR_OCSP_OLD_RESPONSE ||
+        expired) {
+      // stapled OCSP response present but expired
+      Telemetry::Accumulate(Telemetry::SSL_OCSP_STAPLING, 3);
+      PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
+             ("NSSCertDBTrustDomain: expired stapled OCSP response"));
+    } else {
       // stapled OCSP response present but invalid for some reason
       Telemetry::Accumulate(Telemetry::SSL_OCSP_STAPLING, 4);
       PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
              ("NSSCertDBTrustDomain: stapled OCSP response: failure"));
       return rv;
-    } else {
-      // stapled OCSP response present but expired
-      Telemetry::Accumulate(Telemetry::SSL_OCSP_STAPLING, 3);
-      PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
-             ("NSSCertDBTrustDomain: expired stapled OCSP response"));
     }
   } else {
     // no stapled OCSP response
     Telemetry::Accumulate(Telemetry::SSL_OCSP_STAPLING, 2);
     PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
            ("NSSCertDBTrustDomain: no stapled OCSP response"));
   }
 
@@ -329,21 +336,24 @@ NSSCertDBTrustDomain::CheckRevocation(
     url(CERT_GetOCSPAuthorityInfoAccessLocation(cert));
 
   if (!url) {
     if (mOCSPFetching == FetchOCSPForEV ||
         cachedResponseErrorCode == SEC_ERROR_OCSP_UNKNOWN_CERT) {
       PR_SetError(SEC_ERROR_OCSP_UNKNOWN_CERT, 0);
       return SECFailure;
     }
-    if (stapledOCSPResponse ||
-        cachedResponseErrorCode == SEC_ERROR_OCSP_OLD_RESPONSE) {
+    if (cachedResponseErrorCode == SEC_ERROR_OCSP_OLD_RESPONSE) {
       PR_SetError(SEC_ERROR_OCSP_OLD_RESPONSE, 0);
       return SECFailure;
     }
+    if (stapledOCSPResponseErrorCode != 0) {
+      PR_SetError(stapledOCSPResponseErrorCode, 0);
+      return SECFailure;
+    }
 
     // Nothing to do if we don't have an OCSP responder URI for the cert; just
     // assume it is good. Note that this is the confusing, but intended,
     // interpretation of "strict" revocation checking in the face of a
     // certificate that lacks an OCSP responder URI.
     return SECSuccess;
   }
 
@@ -387,72 +397,83 @@ NSSCertDBTrustDomain::CheckRevocation(
     }
     if (cachedResponseErrorCode == SEC_ERROR_OCSP_UNKNOWN_CERT) {
       PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
              ("NSSCertDBTrustDomain: returning SECFailure from cached "
               "response after OCSP request failure"));
       PR_SetError(cachedResponseErrorCode, 0);
       return SECFailure;
     }
-    if (stapledOCSPResponse) {
+    if (stapledOCSPResponseErrorCode != 0) {
       PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
              ("NSSCertDBTrustDomain: returning SECFailure from expired "
               "stapled response after OCSP request failure"));
-      PR_SetError(SEC_ERROR_OCSP_OLD_RESPONSE, 0);
+      PR_SetError(stapledOCSPResponseErrorCode, 0);
       return SECFailure;
     }
 
     PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
            ("NSSCertDBTrustDomain: returning SECSuccess after "
             "OCSP request failure"));
     return SECSuccess; // Soft fail -> success :(
   }
 
+  // If the response from the network has expired but indicates a revoked
+  // or unknown certificate, PR_GetError() will return the appropriate error.
+  // We actually ignore expired here.
+  bool expired;
   SECStatus rv = VerifyAndMaybeCacheEncodedOCSPResponse(cert, issuerCert, time,
                                                         maxOCSPLifetimeInDays,
                                                         response,
-                                                        ResponseIsFromNetwork);
+                                                        ResponseIsFromNetwork,
+                                                        expired);
   if (rv == SECSuccess || mOCSPFetching != FetchOCSPForDVSoftFail) {
     PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
       ("NSSCertDBTrustDomain: returning after VerifyEncodedOCSPResponse"));
     return rv;
   }
 
   PRErrorCode error = PR_GetError();
   if (error == SEC_ERROR_OCSP_UNKNOWN_CERT ||
       error == SEC_ERROR_REVOKED_CERTIFICATE) {
     return rv;
   }
 
-  if (stapledOCSPResponse) {
+  if (stapledOCSPResponseErrorCode != 0) {
     PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
            ("NSSCertDBTrustDomain: returning SECFailure from expired stapled "
             "response after OCSP request verification failure"));
-    PR_SetError(SEC_ERROR_OCSP_OLD_RESPONSE, 0);
+    PR_SetError(stapledOCSPResponseErrorCode, 0);
     return SECFailure;
   }
 
   PR_LOG(gCertVerifierLog, PR_LOG_DEBUG,
          ("NSSCertDBTrustDomain: end of CheckRevocation"));
 
   return SECSuccess; // Soft fail -> success :(
 }
 
 SECStatus
 NSSCertDBTrustDomain::VerifyAndMaybeCacheEncodedOCSPResponse(
   const CERTCertificate* cert, CERTCertificate* issuerCert, PRTime time,
   uint16_t maxLifetimeInDays, const SECItem* encodedResponse,
-  EncodedResponseSource responseSource)
+  EncodedResponseSource responseSource, /*out*/ bool& expired)
 {
   PRTime thisUpdate = 0;
   PRTime validThrough = 0;
   SECStatus rv = VerifyEncodedOCSPResponse(*this, cert, issuerCert, time,
                                            maxLifetimeInDays, encodedResponse,
-                                           &thisUpdate, &validThrough);
+                                           expired, &thisUpdate, &validThrough);
   PRErrorCode error = (rv == SECSuccess ? 0 : PR_GetError());
+  // If a response was stapled and expired, we don't want to cache it. Return
+  // early to simplify the logic here.
+  if (responseSource == ResponseWasStapled && expired) {
+    PR_ASSERT(rv != SECSuccess);
+    return rv;
+  }
   // validThrough is only trustworthy if the response successfully verifies
   // or it indicates a revoked or unknown certificate.
   // If this isn't the case, store an indication of failure (to prevent
   // repeatedly requesting a response from a failing server).
   if (rv != SECSuccess && error != SEC_ERROR_REVOKED_CERTIFICATE &&
       error != SEC_ERROR_OCSP_UNKNOWN_CERT) {
     validThrough = time + ServerFailureDelay;
   }
--- a/security/certverifier/NSSCertDBTrustDomain.h
+++ b/security/certverifier/NSSCertDBTrustDomain.h
@@ -86,17 +86,17 @@ private:
   enum EncodedResponseSource {
     ResponseIsFromNetwork = 1,
     ResponseWasStapled = 2
   };
   static const PRTime ServerFailureDelay = 5 * 60 * PR_USEC_PER_SEC;
   SECStatus VerifyAndMaybeCacheEncodedOCSPResponse(
     const CERTCertificate* cert, CERTCertificate* issuerCert, PRTime time,
     uint16_t maxLifetimeInDays, const SECItem* encodedResponse,
-    EncodedResponseSource responseSource);
+    EncodedResponseSource responseSource, /*out*/ bool& expired);
 
   const SECTrustType mCertDBTrustType;
   const OCSPFetching mOCSPFetching;
   OCSPCache& mOCSPCache; // non-owning!
   void* mPinArg; // non-owning!
   const CertVerifier::ocsp_get_config mOCSPGetConfig;
   CERTChainVerifyCallback* mCheckChainCallback; // non-owning!
 };
--- a/security/manager/ssl/tests/unit/test_ocsp_stapling_expired.js
+++ b/security/manager/ssl/tests/unit/test_ocsp_stapling_expired.js
@@ -131,16 +131,44 @@ function add_tests_in_mode(useMozillaPKI
   add_ocsp_test("ocsp-stapling-expired.example.com",
                 getXPCOMStatusFromNSS(SEC_ERROR_OCSP_UNKNOWN_CERT),
                 ocspResponseUnknown);
   add_ocsp_test("ocsp-stapling-expired-fresh-ca.example.com",
                 getXPCOMStatusFromNSS(SEC_ERROR_OCSP_UNKNOWN_CERT),
                 ocspResponseUnknown);
 
   if (useMozillaPKIX) {
+    // If the response is expired but indicates Revoked or Unknown and a
+    // newer status can't be fetched, the Revoked or Unknown status will
+    // be returned.
+    add_ocsp_test("ocsp-stapling-revoked-old.example.com",
+                  getXPCOMStatusFromNSS(SEC_ERROR_REVOKED_CERTIFICATE),
+                  null);
+    add_ocsp_test("ocsp-stapling-unknown-old.example.com",
+                  getXPCOMStatusFromNSS(SEC_ERROR_OCSP_UNKNOWN_CERT),
+                  null);
+    // If the response is expired but indicates Revoked or Unknown and
+    // a newer status can be fetched and successfully verified, this
+    // should result in a successful certificate verification.
+    add_ocsp_test("ocsp-stapling-revoked-old.example.com", Cr.NS_OK,
+                  ocspResponseGood);
+    add_ocsp_test("ocsp-stapling-unknown-old.example.com", Cr.NS_OK,
+                  ocspResponseGood);
+    // If a newer status can be fetched but it fails to verify, the
+    // Revoked or Unknown status of the expired stapled response
+    // should be returned.
+    add_ocsp_test("ocsp-stapling-revoked-old.example.com",
+                  getXPCOMStatusFromNSS(SEC_ERROR_REVOKED_CERTIFICATE),
+                  expiredOCSPResponseGood);
+    add_ocsp_test("ocsp-stapling-unknown-old.example.com",
+                  getXPCOMStatusFromNSS(SEC_ERROR_OCSP_UNKNOWN_CERT),
+                  expiredOCSPResponseGood);
+  }
+
+  if (useMozillaPKIX) {
     // These tests are verifying that an valid but very old response
     // is rejected as a valid stapled response, requiring a fetch
     // from the ocsp responder.
     add_ocsp_test("ocsp-stapling-ancient-valid.example.com", Cr.NS_OK,
                   ocspResponseGood);
     add_ocsp_test("ocsp-stapling-ancient-valid.example.com",
                   getXPCOMStatusFromNSS(SEC_ERROR_REVOKED_CERTIFICATE),
                   ocspResponseRevoked);
@@ -153,13 +181,13 @@ function add_tests_in_mode(useMozillaPKI
 function check_ocsp_stapling_telemetry() {
   let histogram = Cc["@mozilla.org/base/telemetry;1"]
                     .getService(Ci.nsITelemetry)
                     .getHistogramById("SSL_OCSP_STAPLING")
                     .snapshot();
   do_check_eq(histogram.counts[0], 2 * 0); // histogram bucket 0 is unused
   do_check_eq(histogram.counts[1], 2 * 0); // 0 connections with a good response
   do_check_eq(histogram.counts[2], 2 * 0); // 0 connections with no stapled resp.
-  do_check_eq(histogram.counts[3], 2 * 12 + 3); // 12 connections with an expired response
-                                                // +3 more mozilla::pkix-only expired responses
+  do_check_eq(histogram.counts[3], 2 * 12 + 9); // 12 connections with an expired response
+                                                // +9 more mozilla::pkix-only expired responses
   do_check_eq(histogram.counts[4], 2 * 0); // 0 connections with bad responses
   run_next_test();
 }
--- a/security/manager/ssl/tests/unit/tlsserver/cmd/OCSPStaplingServer.cpp
+++ b/security/manager/ssl/tests/unit/tlsserver/cmd/OCSPStaplingServer.cpp
@@ -18,17 +18,19 @@
 
 using namespace mozilla;
 using namespace mozilla::test;
 
 const OCSPHost sOCSPHosts[] =
 {
   { "ocsp-stapling-good.example.com", ORTGood, nullptr },
   { "ocsp-stapling-revoked.example.com", ORTRevoked, nullptr },
+  { "ocsp-stapling-revoked-old.example.com", ORTRevokedOld, nullptr },
   { "ocsp-stapling-unknown.example.com", ORTUnknown, nullptr },
+  { "ocsp-stapling-unknown-old.example.com", ORTUnknownOld, nullptr },
   { "ocsp-stapling-good-other.example.com", ORTGoodOtherCert, "ocspOtherEndEntity" },
   { "ocsp-stapling-good-other-ca.example.com", ORTGoodOtherCA, "otherCA" },
   { "ocsp-stapling-expired.example.com", ORTExpired, nullptr },
   { "ocsp-stapling-expired-fresh-ca.example.com", ORTExpiredFreshCA, nullptr },
   { "ocsp-stapling-none.example.com", ORTNone, nullptr },
   { "ocsp-stapling-empty.example.com", ORTEmpty, nullptr },
   { "ocsp-stapling-malformed.example.com", ORTMalformed, nullptr },
   { "ocsp-stapling-srverr.example.com", ORTSrverr, nullptr },
--- a/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
+++ b/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
@@ -104,30 +104,31 @@ GetOCSPResponseForType(OCSPResponseType 
     default:
       // context.responseStatus is 0 in all other cases, and it has
       // already been initialized in the constructor.
       break;
   }
   if (aORT == ORTSkipResponseBytes) {
     context.skipResponseBytes = true;
   }
-  if (aORT == ORTExpired || aORT == ORTExpiredFreshCA) {
+  if (aORT == ORTExpired || aORT == ORTExpiredFreshCA ||
+      aORT == ORTRevokedOld || aORT == ORTUnknownOld) {
     context.thisUpdate = oldNow;
     context.nextUpdate = oldNow + 10 * PR_USEC_PER_SEC;
   }
   if (aORT == ORTLongValidityAlmostExpired) {
     context.thisUpdate = now - (320 * oneDay);
   }
   if (aORT == ORTAncientAlmostExpired) {
     context.thisUpdate = now - (640 * oneDay);
   }
-  if (aORT == ORTRevoked) {
+  if (aORT == ORTRevoked || aORT == ORTRevokedOld) {
     context.certStatus = 1;
   }
-  if (aORT == ORTUnknown) {
+  if (aORT == ORTUnknown || aORT == ORTUnknownOld) {
     context.certStatus = 2;
   }
   if (aORT == ORTBadSignature) {
     context.badSignature = true;
   }
   OCSPResponseExtension extension;
   if (aORT == ORTCriticalExtension || aORT == ORTNoncriticalExtension) {
     SECItem oidItem = {
--- a/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.h
+++ b/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.h
@@ -11,17 +11,19 @@
 #include "certt.h"
 #include "seccomon.h"
 
 enum OCSPResponseType
 {
   ORTNull = 0,
   ORTGood,             // the certificate is good
   ORTRevoked,          // the certificate has been revoked
+  ORTRevokedOld,       // same, but the response is old
   ORTUnknown,          // the responder doesn't know if the cert is good
+  ORTUnknownOld,       // same, but the response is old
   ORTGoodOtherCert,    // the response references a different certificate
   ORTGoodOtherCA,      // the wrong CA has signed the response
   ORTExpired,          // the signature on the response has expired
   ORTExpiredFreshCA,   // fresh signature, but old validity period
   ORTNone,             // no stapled response
   ORTEmpty,            // an empty stapled response
   ORTMalformed,        // the response from the responder was malformed
   ORTSrverr,           // the response indicates there was a server error
--- a/security/pkix/include/pkix/pkix.h
+++ b/security/pkix/include/pkix/pkix.h
@@ -104,27 +104,32 @@ SECStatus VerifySignedData(const CERTSig
                            const SECItem& subjectPublicKeyInfo,
                            void* pkcs11PinArg);
 
 // The return value, if non-null, is owned by the arena and MUST NOT be freed.
 SECItem* CreateEncodedOCSPRequest(PLArenaPool* arena,
                                   const CERTCertificate* cert,
                                   const CERTCertificate* issuerCert);
 
+// The out parameter expired will be true if the response has expired. If the
+// response also indicates a revoked or unknown certificate, that error
+// will be returned by PR_GetError(). Otherwise, SEC_ERROR_OCSP_OLD_RESPONSE
+// will be returned by PR_GetError() for an expired response.
 // The optional parameter thisUpdate will be the thisUpdate value of
 // the encoded response if it is considered trustworthy. Only
 // good, unknown, or revoked responses that verify correctly are considered
 // trustworthy. If the response is not trustworthy, thisUpdate will be 0.
 // Similarly, the optional parameter validThrough will be the time through
 // which the encoded response is considered trustworthy (that is, if a response had a
 // thisUpdate time of validThrough, it would be considered trustworthy).
 SECStatus VerifyEncodedOCSPResponse(TrustDomain& trustDomain,
                                     const CERTCertificate* cert,
                                     CERTCertificate* issuerCert,
                                     PRTime time,
                                     uint16_t maxLifetimeInDays,
                                     const SECItem* encodedResponse,
+                          /* out */ bool& expired,
                  /* optional out */ PRTime* thisUpdate,
                  /* optional out */ PRTime* validThrough);
 
 } } // namespace mozilla::pkix
 
 #endif // mozilla_pkix__pkix_h
--- a/security/pkix/lib/pkixocsp.cpp
+++ b/security/pkix/lib/pkixocsp.cpp
@@ -64,16 +64,17 @@ public:
     , certSerialNumber(certSerialNumber)
     , issuerSubject(issuerSubject)
     , issuerSubjectPublicKeyInfo(issuerSubjectPublicKeyInfo)
     , time(time)
     , maxLifetimeInDays(maxLifetimeInDays)
     , certStatus(CertStatus::Unknown)
     , thisUpdate(thisUpdate)
     , validThrough(validThrough)
+    , expired(false)
   {
     if (thisUpdate) {
       *thisUpdate = 0;
     }
     if (validThrough) {
       *validThrough = 0;
     }
   }
@@ -82,16 +83,17 @@ public:
   const SECItem& certSerialNumber;
   const SECItem& issuerSubject;
   const SECItem& issuerSubjectPublicKeyInfo;
   const PRTime time;
   const uint16_t maxLifetimeInDays;
   CertStatus certStatus;
   PRTime* thisUpdate;
   PRTime* validThrough;
+  bool expired;
 
 private:
   Context(const Context&); // delete
   void operator=(const Context&); // delete
 };
 
 static der::Result
 HashBuf(const SECItem& item, /*out*/ uint8_t *hashBuf, size_t hashBufLen)
@@ -313,28 +315,32 @@ SetErrorToMalformedResponseOnBadDERError
 }
 
 SECStatus
 VerifyEncodedOCSPResponse(TrustDomain& trustDomain,
                           const CERTCertificate* cert,
                           CERTCertificate* issuerCert, PRTime time,
                           uint16_t maxOCSPLifetimeInDays,
                           const SECItem* encodedResponse,
+                          bool& expired,
                           PRTime* thisUpdate,
                           PRTime* validThrough)
 {
   PR_ASSERT(cert);
   PR_ASSERT(issuerCert);
   // TODO: PR_Assert(pinArg)
   PR_ASSERT(encodedResponse);
   if (!cert || !issuerCert || !encodedResponse || !encodedResponse->data) {
     PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
     return SECFailure;
   }
 
+  // Always initialize this to something reasonable.
+  expired = false;
+
   der::Input input;
   if (input.Init(encodedResponse->data, encodedResponse->len) != der::Success) {
     SetErrorToMalformedResponseOnBadDERError();
     return SECFailure;
   }
   Context context(trustDomain, cert->serialNumber, issuerCert->derSubject,
                   issuerCert->derPublicKey, time, maxOCSPLifetimeInDays,
                   thisUpdate, validThrough);
@@ -345,18 +351,24 @@ VerifyEncodedOCSPResponse(TrustDomain& t
     return SECFailure;
   }
 
   if (der::End(input) != der::Success) {
     SetErrorToMalformedResponseOnBadDERError();
     return SECFailure;
   }
 
+  expired = context.expired;
+
   switch (context.certStatus) {
     case CertStatus::Good:
+      if (expired) {
+        PR_SetError(SEC_ERROR_OCSP_OLD_RESPONSE, 0);
+        return SECFailure;
+      }
       return SECSuccess;
     case CertStatus::Revoked:
       PR_SetError(SEC_ERROR_REVOKED_CERTIFICATE, 0);
       return SECFailure;
     case CertStatus::Unknown:
       PR_SetError(SEC_ERROR_OCSP_UNKNOWN_CERT, 0);
       return SECFailure;
   }
@@ -652,18 +664,19 @@ SingleResponse(der::Input& input, Contex
     // NSS requires all OCSP responses without a nextUpdate to be recent.
     // Match that stricter behavior.
     notAfter = thisUpdate + ONE_DAY;
   }
 
   if (context.time < SLOP) { // prevent underflow
     return der::Fail(SEC_ERROR_INVALID_ARGS);
   }
+
   if (context.time - SLOP > notAfter) {
-    return der::Fail(SEC_ERROR_OCSP_OLD_RESPONSE);
+    context.expired = true;
   }
 
   if (!input.AtEnd()) {
     if (der::Nested(input, der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1,
                     CheckExtensionsForCriticality) != der::Success) {
       return der::Failure;
     }
   }