mirror of https://github.com/mongodb/mongo
SERVER-84645 OCSP stapling log messages should indicate response validity (#44121)
GitOrigin-RevId: b92d8880dd30e6d0600df7bb453a52ad61cc645e
This commit is contained in:
parent
cae6396315
commit
1f7f8f8c54
|
|
@ -111,16 +111,37 @@ export class MockOCSPServer {
|
|||
args.push("--issuer_hash_algorithm=" + this.issuer_hash_algorithm);
|
||||
}
|
||||
|
||||
clearRawMongoProgramOutput();
|
||||
const MAX_ATTEMPTS = 10;
|
||||
let backoff_ms = 1000;
|
||||
let portInUse = false;
|
||||
for (let attempts = 0; attempts < MAX_ATTEMPTS; attempts++) {
|
||||
clearRawMongoProgramOutput();
|
||||
|
||||
this.pid = _startMongoProgram({args: args});
|
||||
assert(checkProgram(this.pid).alive);
|
||||
const pid = _startMongoProgram({args: args});
|
||||
assert(checkProgram(pid).alive);
|
||||
|
||||
assert.soon(function () {
|
||||
// Change this line if the OCSP endpoint changes
|
||||
return rawMongoProgramOutput(".*").search("Launching debugserver on port 8100") !== -1;
|
||||
});
|
||||
portInUse = false;
|
||||
assert.soon(function () {
|
||||
// Change this line if the OCSP endpoint changes
|
||||
let output = rawMongoProgramOutput(".*");
|
||||
portInUse = output.search("Address already in use") !== -1;
|
||||
return portInUse || output.search("Running on http") !== -1;
|
||||
});
|
||||
|
||||
if (!portInUse) {
|
||||
this.pid = pid;
|
||||
break;
|
||||
}
|
||||
|
||||
assert.soon(function () {
|
||||
return !checkProgram(pid).alive;
|
||||
});
|
||||
print(`Retrying OCSP mock responder startup after ${backoff_ms / 1000} seconds.`);
|
||||
sleep(backoff_ms);
|
||||
backoff_ms *= 2;
|
||||
}
|
||||
|
||||
assert(!portInUse);
|
||||
sleep(2000);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
// Tests that OCSP for intracluster TLS logs helpful error messages when verification
|
||||
// fails due to revoked peer certificates.
|
||||
// @tags: [requires_http_client, requires_ocsp_stapling]
|
||||
|
||||
import {FAULT_REVOKED, MockOCSPServer} from "jstests/ocsp/lib/mock_ocsp.js";
|
||||
import {funWithArgs} from "jstests/libs/parallel_shell_helpers.js";
|
||||
import {
|
||||
OCSP_CA_PEM,
|
||||
OCSP_CLIENT_CERT,
|
||||
OCSP_SERVER_CERT_REVOKED,
|
||||
supportsStapling,
|
||||
waitForServer,
|
||||
assertClientConnectFails,
|
||||
} from "jstests/ocsp/lib/ocsp_helpers.js";
|
||||
|
||||
if (!supportsStapling()) {
|
||||
quit();
|
||||
}
|
||||
|
||||
const repl_set = "ocsp-test";
|
||||
const with_stapling_options = {
|
||||
tlsMode: "requireTLS",
|
||||
tlsCertificateKeyFile: OCSP_SERVER_CERT_REVOKED,
|
||||
tlsCAFile: OCSP_CA_PEM,
|
||||
tlsAllowInvalidHostnames: "",
|
||||
setParameter: {
|
||||
"ocspEnabled": "true",
|
||||
},
|
||||
replSet: repl_set,
|
||||
waitForConnect: false,
|
||||
};
|
||||
const no_stapling_options = Object.assign({}, with_stapling_options);
|
||||
no_stapling_options.setParameter = {
|
||||
"ocspEnabled": "true",
|
||||
"failpoint.disableStapling": "{'mode':'alwaysOn'}",
|
||||
};
|
||||
|
||||
const revoked_status = {
|
||||
"code": ErrorCodes.OCSPCertificateStatusRevoked,
|
||||
"codeName": "OCSPCertificateStatusRevoked",
|
||||
"errmsg": "OCSP Certificate Status: Revoked. Reason: unspecified",
|
||||
};
|
||||
|
||||
const mock_ocsp = new MockOCSPServer(FAULT_REVOKED, 1);
|
||||
mock_ocsp.start();
|
||||
|
||||
// Start the replica set nodes:
|
||||
// - conn1 does not staple
|
||||
// - conn2 staples revoked status,
|
||||
let conn1 = MongoRunner.runMongod(Object.assign({}, no_stapling_options));
|
||||
let conn2 = MongoRunner.runMongod(Object.assign({}, with_stapling_options));
|
||||
waitForServer(conn1);
|
||||
waitForServer(conn2);
|
||||
|
||||
// Assert the current shell can't connect because of the revoked server certs.
|
||||
assertClientConnectFails(conn1);
|
||||
assertClientConnectFails(conn2);
|
||||
|
||||
// In a separate shell (that ignores OCSP), initiate the replica set from conn1.
|
||||
// Expect conn1 connects to conn2; fails because conn2's stapled cert is revoked.
|
||||
async function bgValidateFunc(rs_name, ports, has_stapling_logs, expected_logid, expected_attr) {
|
||||
function waitForLog(code, attr) {
|
||||
assert.soon(
|
||||
function () {
|
||||
return checkLog.checkContainsWithAtLeastCountJson(db, code, attr, 1, "I", true);
|
||||
},
|
||||
`Could not find log entries containing id ${code} and attr: ${tojson(attr)}`,
|
||||
undefined,
|
||||
undefined,
|
||||
{runHangAnalyzer: false},
|
||||
);
|
||||
}
|
||||
|
||||
// If this node staples, assert there are logs for both fetch/staple status AND
|
||||
// the certificate revocation status.
|
||||
if (has_stapling_logs) {
|
||||
waitForLog(577163 /* OCSP fetch status */, {"status": {"code": 0}});
|
||||
waitForLog(8464500 /* OCSP revocation status */, {
|
||||
"status": {"code": ErrorCodes.OCSPCertificateStatusRevoked},
|
||||
});
|
||||
} else {
|
||||
assert.eq(checkLog.checkContainsOnceJson(db, 577163, {}), false);
|
||||
}
|
||||
|
||||
// Initiate the replica set to start egress connection to the other node.
|
||||
assert.eq(checkLog.checkContainsOnceJson(db, expected_logid, {}), false);
|
||||
rs.initiate({
|
||||
_id: rs_name,
|
||||
members: ports.map((port, idx) => {
|
||||
return {_id: idx, host: `localhost:${port}`};
|
||||
}),
|
||||
});
|
||||
waitForLog(expected_logid, expected_attr);
|
||||
}
|
||||
|
||||
let pshell = startParallelShell(
|
||||
funWithArgs(bgValidateFunc, repl_set, [conn1.port, conn2.port], false, 23225, {"error": revoked_status}),
|
||||
conn1.port,
|
||||
false,
|
||||
"--tls",
|
||||
"--tlsCAFile",
|
||||
OCSP_CA_PEM,
|
||||
"--tlsCertificateKeyFile",
|
||||
OCSP_CLIENT_CERT,
|
||||
"--tlsAllowInvalidHostnames",
|
||||
"--tlsAllowInvalidCertificates",
|
||||
"--verbose",
|
||||
1,
|
||||
);
|
||||
pshell();
|
||||
|
||||
// Use a parallel shell to initiate the replica set from conn2.
|
||||
// Expect conn2 connects to conn1, does OCSP on-demand status checking,
|
||||
// and fails because peer cert is revoked.
|
||||
pshell = startParallelShell(
|
||||
funWithArgs(bgValidateFunc, repl_set, [conn1.port, conn2.port], true, 8464502, {"status": revoked_status}),
|
||||
conn2.port,
|
||||
false,
|
||||
"--tls",
|
||||
"--tlsCAFile",
|
||||
OCSP_CA_PEM,
|
||||
"--tlsCertificateKeyFile",
|
||||
OCSP_CLIENT_CERT,
|
||||
"--tlsAllowInvalidHostnames",
|
||||
"--tlsAllowInvalidCertificates",
|
||||
"--verbose",
|
||||
1,
|
||||
);
|
||||
pshell();
|
||||
|
||||
MongoRunner.stopMongod(conn1);
|
||||
MongoRunner.stopMongod(conn2);
|
||||
|
||||
mock_ocsp.stop();
|
||||
|
|
@ -480,8 +480,7 @@ using UniqueCertId =
|
|||
|
||||
Status getSSLFailure(ErrorCodes::Error code, StringData errorMsg) {
|
||||
return Status(code,
|
||||
str::stream() << "SSL peer certificate revocation status checking failed: "
|
||||
<< errorMsg << " "
|
||||
str::stream() << errorMsg << " "
|
||||
<< SSLManagerInterface::getSSLErrorMessage(ERR_get_error()));
|
||||
}
|
||||
|
||||
|
|
@ -526,7 +525,7 @@ StatusWith<UniqueX509> getIssuerCertForCert(SSL_CTX* context,
|
|||
storeCtx.get(), X509_LU_X509, X509_get_issuer_name(cert)));
|
||||
|
||||
if (obj == nullptr) {
|
||||
return getSSLFailure("Could not get X509 Object from store.");
|
||||
return getSSLFailure("Unable to find issuer certificate from X509 store.");
|
||||
}
|
||||
return UniqueX509(X509_dup(X509_OBJECT_get0_X509(obj.get())));
|
||||
}
|
||||
|
|
@ -748,13 +747,13 @@ Future<UniqueOCSPResponse> retrieveOCSPResponse(const std::string& host,
|
|||
auto len = i2d_OCSP_REQUEST(ocspReq.get(), nullptr);
|
||||
std::vector<uint8_t> buffer;
|
||||
if (len <= 0) {
|
||||
return getSSLFailure("Could not decode response from responder.");
|
||||
return getSSLFailure("Failed to encode OCSP request into DER format.");
|
||||
}
|
||||
|
||||
buffer.resize(len);
|
||||
auto bufferData = buffer.data();
|
||||
if (i2d_OCSP_REQUEST(ocspReq.get(), &bufferData) < 0) {
|
||||
return getSSLFailure("Could not convert type OCSP Response to DER encoded object.");
|
||||
return getSSLFailure("Failed to encode OCSP request into DER format.");
|
||||
}
|
||||
|
||||
// Query the OCSP responder
|
||||
|
|
@ -768,7 +767,7 @@ Future<UniqueOCSPResponse> retrieveOCSPResponse(const std::string& host,
|
|||
d2i_OCSP_RESPONSE(nullptr, &respDataPtr, responseData.size()));
|
||||
|
||||
if (response == nullptr) {
|
||||
return getSSLFailure("Could not retrieve OCSP Response.");
|
||||
return getSSLFailure("Failed to decode OCSP response from DER format.");
|
||||
}
|
||||
|
||||
return std::move(response);
|
||||
|
|
@ -865,9 +864,9 @@ StatusWith<std::pair<OCSPCertIDSet, boost::optional<Date_t>>> iterateResponse(
|
|||
auto status = OCSP_single_get0_status(singleResp, &reason, &revtime, &thisupd, &nextupd);
|
||||
|
||||
if (status == V_OCSP_CERTSTATUS_REVOKED) {
|
||||
return getSSLFailure(ErrorCodes::OCSPCertificateStatusRevoked,
|
||||
str::stream() << "OCSP Certificate Status: Revoked. Reason: "
|
||||
<< OCSP_crl_reason_str(reason));
|
||||
return Status(ErrorCodes::OCSPCertificateStatusRevoked,
|
||||
str::stream() << "OCSP Certificate Status: Revoked. Reason: "
|
||||
<< OCSP_crl_reason_str(reason));
|
||||
} else if (status != V_OCSP_CERTSTATUS_GOOD) {
|
||||
return getSSLFailure(str::stream()
|
||||
<< "Unexpected OCSP Certificate Status. Reason: " << status);
|
||||
|
|
@ -907,29 +906,29 @@ StatusWith<std::pair<OCSPCertIDSet, boost::optional<Date_t>>> parseAndValidateOC
|
|||
X509* issuerCert,
|
||||
const OCSPCertIDSet& certIdsInRequest) {
|
||||
// Read the overall status of the OCSP response
|
||||
int responseStatus = OCSP_response_status(response);
|
||||
switch (responseStatus) {
|
||||
case OCSP_RESPONSE_STATUS_SUCCESSFUL:
|
||||
break;
|
||||
case OCSP_RESPONSE_STATUS_MALFORMEDREQUEST:
|
||||
case OCSP_RESPONSE_STATUS_UNAUTHORIZED:
|
||||
case OCSP_RESPONSE_STATUS_SIGREQUIRED:
|
||||
return getSSLFailure(str::stream()
|
||||
<< "Error querying the OCSP responder, issue with OCSP request. "
|
||||
<< "Response Status: " << responseStatus);
|
||||
case OCSP_RESPONSE_STATUS_TRYLATER:
|
||||
case OCSP_RESPONSE_STATUS_INTERNALERROR:
|
||||
return getSSLFailure(str::stream()
|
||||
<< "Error querying the OCSP responder, an error occured in the "
|
||||
<< "responder itself. Response Status: " << responseStatus);
|
||||
default:
|
||||
return getSSLFailure(str::stream() << "Error querying the OCSP responder. "
|
||||
<< "Response Status: " << responseStatus);
|
||||
if (auto responseStatus = OCSP_response_status(response);
|
||||
responseStatus != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
|
||||
auto& stream = str::stream() << "Failed to query the OCSP responder - ";
|
||||
switch (responseStatus) {
|
||||
case OCSP_RESPONSE_STATUS_MALFORMEDREQUEST:
|
||||
return getSSLFailure(stream << "Malformed request.");
|
||||
case OCSP_RESPONSE_STATUS_UNAUTHORIZED:
|
||||
return getSSLFailure(stream << "Unauthorized.");
|
||||
case OCSP_RESPONSE_STATUS_SIGREQUIRED:
|
||||
return getSSLFailure(stream << "Signature required.");
|
||||
case OCSP_RESPONSE_STATUS_TRYLATER:
|
||||
return getSSLFailure(stream << "Received 'try later'.");
|
||||
case OCSP_RESPONSE_STATUS_INTERNALERROR:
|
||||
return getSSLFailure(stream << "Internal error in responder.");
|
||||
default:
|
||||
return getSSLFailure(stream << "Response Status: " << responseStatus);
|
||||
}
|
||||
MONGO_UNREACHABLE;
|
||||
}
|
||||
|
||||
UniqueOcspBasicResp basicResponse(OCSP_response_get1_basic(response));
|
||||
if (!basicResponse) {
|
||||
return getSSLFailure("incomplete OCSP response.");
|
||||
return getSSLFailure("Got incomplete OCSP response.");
|
||||
}
|
||||
|
||||
X509_STORE* store = ca == nullptr ? SSL_CTX_get_cert_store(context) : ca;
|
||||
|
|
@ -1015,9 +1014,10 @@ Future<OCSPFetchResponse> dispatchOCSPRequests(SSL_CTX* context,
|
|||
});
|
||||
if (!swResponse.isOK()) {
|
||||
if (state->finishLine.arriveWeakly()) {
|
||||
state->promise.setError(
|
||||
Status(ErrorCodes::OCSPCertificateStatusUnknown,
|
||||
"Could not obtain status information of certificates."));
|
||||
state->promise.setError(Status(ErrorCodes::OCSPCertificateStatusUnknown,
|
||||
swResponse.getStatus().reason())
|
||||
.withContext("Failed to obtain OCSP revocation "
|
||||
"status of server certificate."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1050,11 +1050,10 @@ Future<OCSPFetchResponse> dispatchOCSPRequests(SSL_CTX* context,
|
|||
}
|
||||
} else {
|
||||
if (state->finishLine.arriveWeakly()) {
|
||||
state->promise.setError(
|
||||
Status(ErrorCodes::OCSPCertificateStatusUnknown,
|
||||
swCertIDSetAndDuration.getStatus().reason())
|
||||
.withContext(
|
||||
"Could not obtain status information of certificates"));
|
||||
state->promise.setError(Status(ErrorCodes::OCSPCertificateStatusUnknown,
|
||||
swCertIDSetAndDuration.getStatus().reason())
|
||||
.withContext("Failed to obtain OCSP revocation "
|
||||
"status of server certificate."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1142,6 +1141,19 @@ boost::optional<OCSPFetchResponse> lookupOCSPForClient(const OCSPCacheKey& key)
|
|||
std::move(swOCSPContext.getValue()),
|
||||
OCSPPurpose::kClientVerify)
|
||||
.getNoThrow();
|
||||
|
||||
// Assert dispatchOCSPRequests always sets a OCSPCertificateStatusUnknown status on
|
||||
// future completion when an SSL error is encountered and it was unable to obtain
|
||||
// a valid OCSP response.
|
||||
dassert(swResponse.isOK() ||
|
||||
swResponse.getStatus() == ErrorCodes::OCSPCertificateStatusUnknown);
|
||||
LOGV2_INFO(8464501, "OCSP fetch status", "status"_attr = swResponse.getStatus());
|
||||
if (swResponse.isOK()) {
|
||||
LOGV2_INFO(8464502,
|
||||
"OCSP peer certificate revocation status",
|
||||
"status"_attr = swResponse.getValue().statusOfResponse);
|
||||
}
|
||||
|
||||
if (!swResponse.isOK() || !swResponse.getValue().cacheable()) {
|
||||
// if the response is unknown or not cacheable, (ie. because of
|
||||
// a missing nextUpdate field), then return none so that the
|
||||
|
|
@ -2003,10 +2015,9 @@ int ocspClientCallback(SSL* ssl, void* arg) {
|
|||
// CRLs or check with the OCSP responder ourselves. If it is true, then we are done.
|
||||
if (!swStapleOK.isOK()) {
|
||||
if (swStapleOK.getStatus() == ErrorCodes::OCSPCertificateStatusRevoked) {
|
||||
LOGV2_DEBUG(23225,
|
||||
1,
|
||||
"Stapled OCSP Response validation failed",
|
||||
"error"_attr = swStapleOK.getStatus());
|
||||
LOGV2_INFO(23225,
|
||||
"Stapled OCSP Response validation failed",
|
||||
"error"_attr = swStapleOK.getStatus());
|
||||
return OCSP_CLIENT_RESPONSE_NOT_ACCEPTABLE;
|
||||
}
|
||||
|
||||
|
|
@ -2016,10 +2027,9 @@ int ocspClientCallback(SSL* ssl, void* arg) {
|
|||
|
||||
return OCSP_CLIENT_RESPONSE_ERROR;
|
||||
} else if (!swStapleOK.getValue()) {
|
||||
LOGV2_DEBUG(23226,
|
||||
1,
|
||||
"Stapled Certificate validation failed: Stapled response does not contain "
|
||||
"status information regarding the peer certificate.");
|
||||
LOGV2_INFO(23226,
|
||||
"Stapled Certificate validation failed: Stapled response does not contain "
|
||||
"status information regarding the peer certificate.");
|
||||
return OCSP_CLIENT_RESPONSE_NOT_ACCEPTABLE;
|
||||
}
|
||||
|
||||
|
|
@ -2317,12 +2327,12 @@ Future<Milliseconds> OCSPFetcher::fetchAndStaple(Promise<void>* promise) {
|
|||
// Generate a new verified X509StoreContext to get our own certificate chain
|
||||
UniqueX509StoreCtx storeCtx(X509_STORE_CTX_new());
|
||||
if (!storeCtx) {
|
||||
return getSSLFailure("Could not create X509 store.");
|
||||
return getSSLFailure("Failed to create an X509 store context.");
|
||||
}
|
||||
|
||||
X509_STORE* store = _ca ? _ca.get() : SSL_CTX_get_cert_store(_context);
|
||||
if (X509_STORE_CTX_init(storeCtx.get(), store, NULL, NULL) == 0) {
|
||||
return getSSLFailure("Could not initialize the X509 Store Context.");
|
||||
return getSSLFailure("Failed to initialize the X509 store context.");
|
||||
}
|
||||
|
||||
X509_STORE_CTX_set_cert(storeCtx.get(), _cert);
|
||||
|
|
@ -2332,7 +2342,7 @@ Future<Milliseconds> OCSPFetcher::fetchAndStaple(Promise<void>* promise) {
|
|||
X509_STORE_CTX_set_chain(storeCtx.get(), sk);
|
||||
|
||||
if (X509_verify_cert(storeCtx.get()) <= 0) {
|
||||
return getSSLFailure("Could not verify X509 certificate store for OCSP Stapling.");
|
||||
return getSSLFailure("Failed to verify the configured server X509 certificate.");
|
||||
}
|
||||
|
||||
// Extract the chain from the verified X509StoreCtx
|
||||
|
|
@ -2359,6 +2369,12 @@ Future<Milliseconds> OCSPFetcher::fetchAndStaple(Promise<void>* promise) {
|
|||
return kOCSPUnknownStatusRefreshRate;
|
||||
}
|
||||
|
||||
// Assert dispatchOCSPRequests always sets a OCSPCertificateStatusUnknown status on
|
||||
// future completion when an SSL error is encountered and it was unable to obtain
|
||||
// a valid OCSP response.
|
||||
dassert(swResponse.isOK() ||
|
||||
swResponse.getStatus() == ErrorCodes::OCSPCertificateStatusUnknown);
|
||||
|
||||
// protect against pf going out of scope when asynchronous
|
||||
if (promise != nullptr) {
|
||||
promise->setWith([&] {
|
||||
|
|
@ -2368,7 +2384,12 @@ Future<Milliseconds> OCSPFetcher::fetchAndStaple(Promise<void>* promise) {
|
|||
});
|
||||
}
|
||||
|
||||
LOGV2_INFO(577163, "OCSP response", "status"_attr = swResponse.getStatus());
|
||||
LOGV2_INFO(577163, "OCSP fetch status", "status"_attr = swResponse.getStatus());
|
||||
if (swResponse.isOK()) {
|
||||
LOGV2_INFO(8464500,
|
||||
"OCSP revocation status",
|
||||
"status"_attr = swResponse.getValue().statusOfResponse);
|
||||
}
|
||||
return _manager->updateOcspStaplingContextWithResponse(std::move(swResponse));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue