bug 997509 - heed expired Revoked or Unknown OCSP responses r=briansmith

This commit is contained in:
David Keeler 2014-06-20 09:01:57 -07:00
Родитель 7ce0e02be8
Коммит c13f6d39c7
8 изменённых файлов: 95 добавлений и 23 удалений

Просмотреть файл

@ -212,13 +212,18 @@ NSSCertDBTrustDomain::CheckRevocation(
// 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);
@ -226,17 +231,19 @@ NSSCertDBTrustDomain::CheckRevocation(
("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
@ -334,11 +341,14 @@ NSSCertDBTrustDomain::CheckRevocation(
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,
@ -392,11 +402,11 @@ NSSCertDBTrustDomain::CheckRevocation(
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;
}
@ -406,10 +416,15 @@ NSSCertDBTrustDomain::CheckRevocation(
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"));
@ -422,11 +437,11 @@ NSSCertDBTrustDomain::CheckRevocation(
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;
}
@ -440,14 +455,20 @@ 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

Просмотреть файл

@ -91,7 +91,7 @@ private:
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;

Просмотреть файл

@ -135,6 +135,34 @@ function add_tests_in_mode(useMozillaPKIX)
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
@ -158,8 +186,8 @@ function check_ocsp_stapling_telemetry() {
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();
}

Просмотреть файл

@ -23,7 +23,9 @@ 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 },

Просмотреть файл

@ -109,7 +109,8 @@ GetOCSPResponseForType(OCSPResponseType aORT, CERTCertificate *aCert,
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;
}
@ -119,10 +120,10 @@ GetOCSPResponseForType(OCSPResponseType aORT, CERTCertificate *aCert,
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) {

Просмотреть файл

@ -16,7 +16,9 @@ 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

Просмотреть файл

@ -109,6 +109,10 @@ 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
@ -122,6 +126,7 @@ SECStatus VerifyEncodedOCSPResponse(TrustDomain& trustDomain,
PRTime time,
uint16_t maxLifetimeInDays,
const SECItem* encodedResponse,
/* out */ bool& expired,
/* optional out */ PRTime* thisUpdate,
/* optional out */ PRTime* validThrough);

Просмотреть файл

@ -69,6 +69,7 @@ public:
, certStatus(CertStatus::Unknown)
, thisUpdate(thisUpdate)
, validThrough(validThrough)
, expired(false)
{
if (thisUpdate) {
*thisUpdate = 0;
@ -87,6 +88,7 @@ public:
CertStatus certStatus;
PRTime* thisUpdate;
PRTime* validThrough;
bool expired;
private:
Context(const Context&); // delete
@ -319,6 +321,7 @@ VerifyEncodedOCSPResponse(TrustDomain& trustDomain,
CERTCertificate* issuerCert, PRTime time,
uint16_t maxOCSPLifetimeInDays,
const SECItem* encodedResponse,
bool& expired,
PRTime* thisUpdate,
PRTime* validThrough)
{
@ -331,6 +334,9 @@ VerifyEncodedOCSPResponse(TrustDomain& trustDomain,
return SECFailure;
}
// Always initialize this to something reasonable.
expired = false;
der::Input input;
if (input.Init(encodedResponse->data, encodedResponse->len) != der::Success) {
SetErrorToMalformedResponseOnBadDERError();
@ -351,8 +357,14 @@ VerifyEncodedOCSPResponse(TrustDomain& trustDomain,
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);
@ -658,8 +670,9 @@ SingleResponse(der::Input& input, Context& context)
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()) {