Skip to content

Commit f299e8b

Browse files
sgolemon-corpevergreen
authored andcommitted
SERVER-41121 Warn when a peer certificate is about to expire
1 parent 6729eaa commit f299e8b

File tree

9 files changed

+167
-14
lines changed

9 files changed

+167
-14
lines changed

jstests/ssl/x509_expiring.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Verify a warning is emitted when a certificate is about to expire.
2+
3+
(function() {
4+
'use strict';
5+
6+
const SERVER_CERT = "jstests/libs/server.pem";
7+
const CA_CERT = "jstests/libs/ca.pem";
8+
const CLIENT_USER = "CN=client,OU=KernelUser,O=MongoDB,L=New York City,ST=New York,C=US";
9+
10+
function test(expiration, expect) {
11+
const options = {
12+
auth: '',
13+
tlsMode: "requireTLS",
14+
tlsCertificateKeyFile: SERVER_CERT,
15+
tlsCAFile: CA_CERT,
16+
setParameter: 'tlsX509ExpirationWarningThresholdDays=' + expiration,
17+
};
18+
const mongo = MongoRunner.runMongod(options);
19+
const external = mongo.getDB("$external");
20+
21+
external.createUser({
22+
user: CLIENT_USER,
23+
roles: [
24+
{'role': 'userAdminAnyDatabase', 'db': 'admin'},
25+
{'role': 'readWriteAnyDatabase', 'db': 'admin'},
26+
{'role': 'clusterMonitor', 'db': 'admin'},
27+
]
28+
});
29+
30+
assert(external.auth({user: CLIENT_USER, mechanism: 'MONGODB-X509'}),
31+
"authentication with valid user failed");
32+
33+
// Check that there's a "Successfully authenticated" message that includes the client IP
34+
const log =
35+
assert.commandWorked(external.getSiblingDB("admin").runCommand({getLog: "global"})).log;
36+
const warning = `Peer certificate '${CLIENT_USER}' expires`;
37+
38+
assert.eq(log.some(line => line.includes(warning)), expect);
39+
40+
MongoRunner.stopMongod(mongo);
41+
}
42+
43+
test(30, false);
44+
test(7300, true); // Work so long as certs expire no more than 20 years from now
45+
})();

src/mongo/util/duration.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ Stream& streamPut(Stream& os, Hours hrs) {
6767
return os << hrs.count() << "hr";
6868
}
6969

70+
template <typename Stream>
71+
Stream& streamPut(Stream& os, Days days) {
72+
return os << days.count() << "d";
73+
}
74+
7075
} // namespace
7176

7277
std::ostream& operator<<(std::ostream& os, Nanoseconds ns) {
@@ -92,6 +97,10 @@ std::ostream& operator<<(std::ostream& os, Hours h) {
9297
return streamPut(os, h);
9398
}
9499

100+
std::ostream& operator<<(std::ostream& os, Days d) {
101+
return streamPut(os, d);
102+
}
103+
95104
template <typename Allocator>
96105
StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Nanoseconds ns) {
97106
return streamPut(os, ns);
@@ -122,6 +131,11 @@ StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Hours
122131
return streamPut(os, h);
123132
}
124133

134+
template <typename Allocator>
135+
StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Days d) {
136+
return streamPut(os, d);
137+
}
138+
125139
template StringBuilderImpl<StackAllocator>& operator<<(StringBuilderImpl<StackAllocator>&,
126140
Nanoseconds);
127141
template StringBuilderImpl<StackAllocator>& operator<<(StringBuilderImpl<StackAllocator>&,

src/mongo/util/duration.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ using Milliseconds = Duration<std::milli>;
5555
using Seconds = Duration<std::ratio<1>>;
5656
using Minutes = Duration<std::ratio<60>>;
5757
using Hours = Duration<std::ratio<3600>>;
58+
using Days = Duration<std::ratio<86400>>;
5859

5960
//
6061
// Streaming output operators for common duration types. Writes the numerical value followed by
@@ -70,6 +71,7 @@ std::ostream& operator<<(std::ostream& os, Milliseconds ms);
7071
std::ostream& operator<<(std::ostream& os, Seconds s);
7172
std::ostream& operator<<(std::ostream& os, Minutes m);
7273
std::ostream& operator<<(std::ostream& os, Hours h);
74+
std::ostream& operator<<(std::ostream& os, Days h);
7375

7476
template <typename Allocator>
7577
StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Nanoseconds ns);
@@ -89,6 +91,9 @@ StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Minut
8991
template <typename Allocator>
9092
StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Hours h);
9193

94+
template <typename Allocator>
95+
StringBuilderImpl<Allocator>& operator<<(StringBuilderImpl<Allocator>& os, Days h);
96+
9297

9398
template <typename Duration1, typename Duration2>
9499
using HigherPrecisionDuration =

src/mongo/util/net/ssl_manager.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,4 +1171,12 @@ bool hostNameMatchForX509Certificates(std::string nameToMatch, std::string certH
11711171
}
11721172
}
11731173

1174+
void tlsEmitWarningExpiringClientCertificate(const SSLX509Name& peer) {
1175+
warning() << "Peer certificate '" << peer << "' expires soon";
1176+
}
1177+
1178+
void tlsEmitWarningExpiringClientCertificate(const SSLX509Name& peer, Days days) {
1179+
warning() << "Peer certificate '" << peer << "' expires in " << days;
1180+
}
1181+
11741182
} // namespace mongo

src/mongo/util/net/ssl_manager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ StatusWith<TLSVersion> mapTLSVersion(SSLConnectionType conn);
320320
*/
321321
void recordTLSVersion(TLSVersion version, const HostAndPort& hostForLogging);
322322

323+
/**
324+
* Emit a warning() explaining that a client certificate is about to expire.
325+
*/
326+
void tlsEmitWarningExpiringClientCertificate(const SSLX509Name& peer);
327+
void tlsEmitWarningExpiringClientCertificate(const SSLX509Name& peer, Days days);
323328

324329
} // namespace mongo
325330
#endif // #ifdef MONGO_CONFIG_SSL

src/mongo/util/net/ssl_manager_apple.cpp

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#include "mongo/util/net/ssl/apple.hpp"
5454
#include "mongo/util/net/ssl_manager.h"
5555
#include "mongo/util/net/ssl_options.h"
56+
#include "mongo/util/net/ssl_parameters_gen.h"
5657

5758
using asio::ssl::apple::CFUniquePtr;
5859

@@ -1534,9 +1535,10 @@ StatusWith<SSLPeerInfo> SSLManagerApple::parseAndValidatePeerCertificate(
15341535
}
15351536

15361537
CFUniquePtr<::CFMutableArrayRef> oids(
1537-
::CFArrayCreateMutable(nullptr, remoteHost.empty() ? 3 : 2, &::kCFTypeArrayCallBacks));
1538+
::CFArrayCreateMutable(nullptr, remoteHost.empty() ? 4 : 3, &::kCFTypeArrayCallBacks));
15381539
::CFArrayAppendValue(oids.get(), ::kSecOIDX509V1SubjectName);
15391540
::CFArrayAppendValue(oids.get(), ::kSecOIDSubjectAltName);
1541+
::CFArrayAppendValue(oids.get(), ::kSecOIDX509V1ValidityNotAfter);
15401542
if (remoteHost.empty()) {
15411543
::CFArrayAppendValue(oids.get(), kMongoDBRolesOID);
15421544
}
@@ -1556,18 +1558,37 @@ StatusWith<SSLPeerInfo> SSLManagerApple::parseAndValidatePeerCertificate(
15561558
const auto peerSubjectName = std::move(swPeerSubjectName.getValue());
15571559
LOG(2) << "Accepted TLS connection from peer: " << peerSubjectName;
15581560

1559-
// If this is a server and client and server certificate are the same, log a warning.
1560-
if (_sslConfiguration.serverSubjectName() == peerSubjectName) {
1561-
warning() << "Client connecting with server's own TLS certificate";
1562-
}
1563-
1561+
// Server side.
15641562
if (remoteHost.empty()) {
1563+
const auto exprThreshold = tlsX509ExpirationWarningThresholdDays;
1564+
if (exprThreshold > 0) {
1565+
auto swExpiration =
1566+
extractValidityDate(cfdict.get(), ::kSecOIDX509V1ValidityNotAfter, "valid-until");
1567+
if (!swExpiration.isOK()) {
1568+
return badCert("Unable to parse expiration date from certificate",
1569+
_allowInvalidCertificates);
1570+
}
1571+
const auto expiration = swExpiration.getValue();
1572+
const auto now = Date_t::now();
1573+
1574+
if ((now + Days(exprThreshold)) > expiration) {
1575+
tlsEmitWarningExpiringClientCertificate(peerSubjectName,
1576+
duration_cast<Days>(expiration - now));
1577+
}
1578+
}
1579+
1580+
// If client and server certificate are the same, log a warning.
1581+
if (_sslConfiguration.serverSubjectName() == peerSubjectName) {
1582+
warning() << "Client connecting with server's own TLS certificate";
1583+
}
1584+
15651585
// If this is an SSL server context (on a mongod/mongos)
15661586
// parse any client roles out of the client certificate.
15671587
auto swPeerCertificateRoles = parsePeerRoles(cfdict.get());
15681588
if (!swPeerCertificateRoles.isOK()) {
15691589
return swPeerCertificateRoles.getStatus();
15701590
}
1591+
15711592
return SSLPeerInfo(peerSubjectName, sniName, std::move(swPeerCertificateRoles.getValue()));
15721593
}
15731594

src/mongo/util/net/ssl_manager_openssl.cpp

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
#include "mongo/util/net/private/ssl_expiration.h"
6060
#include "mongo/util/net/socket_exception.h"
6161
#include "mongo/util/net/ssl_options.h"
62+
#include "mongo/util/net/ssl_parameters_gen.h"
6263
#include "mongo/util/net/ssl_types.h"
6364
#include "mongo/util/scopeguard.h"
6465
#include "mongo/util/str.h"
@@ -240,10 +241,21 @@ IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(ASN1_SEQUENCE_ANY, ASN1_SET_ANY, ASN
240241
const STACK_OF(X509_EXTENSION) * X509_get0_extensions(const X509* peerCert) {
241242
return peerCert->cert_info->extensions;
242243
}
244+
245+
inline ASN1_TIME* X509_get0_notAfter(const X509* cert) {
246+
return X509_get_notAfter(cert);
247+
}
248+
243249
inline int X509_NAME_ENTRY_set(const X509_NAME_ENTRY* ne) {
244250
return ne->set;
245251
}
246252

253+
#if OPENSSL_VESION_NUMBER < 0x10002000L
254+
inline bool ASN1_TIME_diff(int*, int*, const ASN1_TIME*, const ASN1_TIME*) {
255+
return false;
256+
}
257+
#endif
258+
247259
int DH_set0_pqg(DH* dh, BIGNUM* p, BIGNUM* q, BIGNUM* g) {
248260
dh->p = p;
249261
dh->g = g;
@@ -1531,22 +1543,39 @@ StatusWith<SSLPeerInfo> SSLManagerOpenSSL::parseAndValidatePeerCertificate(
15311543
auto peerSubject = getCertificateSubjectX509Name(peerCert);
15321544
LOG(2) << "Accepted TLS connection from peer: " << peerSubject;
15331545

1534-
// If this is a server and client and server certificate are the same, log a warning.
1535-
if (remoteHost.empty() && _sslConfiguration.serverSubjectName() == peerSubject) {
1536-
warning() << "Client connecting with server's own TLS certificate";
1537-
}
1538-
15391546
StatusWith<stdx::unordered_set<RoleName>> swPeerCertificateRoles = _parsePeerRoles(peerCert);
15401547
if (!swPeerCertificateRoles.isOK()) {
15411548
return swPeerCertificateRoles.getStatus();
15421549
}
15431550

1544-
// If this is an SSL client context (on a MongoDB server or client)
1545-
// perform hostname validation of the remote server
1551+
// Server side.
15461552
if (remoteHost.empty()) {
1553+
const auto exprThreshold = tlsX509ExpirationWarningThresholdDays;
1554+
if (exprThreshold > 0) {
1555+
const auto expiration = X509_get0_notAfter(peerCert);
1556+
time_t threshold = (Date_t::now() + Days(exprThreshold)).toTimeT();
1557+
1558+
if (X509_cmp_time(expiration, &threshold) < 0) {
1559+
int days = 0, secs = 0;
1560+
if (!ASN1_TIME_diff(&days, &secs, nullptr /* now */, expiration)) {
1561+
tlsEmitWarningExpiringClientCertificate(peerSubject);
1562+
} else {
1563+
tlsEmitWarningExpiringClientCertificate(peerSubject, Days(days));
1564+
}
1565+
}
1566+
}
1567+
1568+
// If client and server certificate are the same, log a warning.
1569+
if (_sslConfiguration.serverSubjectName() == peerSubject) {
1570+
warning() << "Client connecting with server's own TLS certificate";
1571+
}
1572+
15471573
return SSLPeerInfo(peerSubject, sniName, std::move(swPeerCertificateRoles.getValue()));
15481574
}
15491575

1576+
// If this is an SSL client context (on a MongoDB server or client)
1577+
// perform hostname validation of the remote server.
1578+
15501579
// This is to standardize the IPAddress format for comparison.
15511580
auto swCIDRRemoteHost = CIDR::parse(remoteHost);
15521581

src/mongo/util/net/ssl_manager_windows.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
#include "mongo/util/net/socket_exception.h"
5959
#include "mongo/util/net/ssl.hpp"
6060
#include "mongo/util/net/ssl_options.h"
61+
#include "mongo/util/net/ssl_parameters_gen.h"
6162
#include "mongo/util/net/ssl_types.h"
6263
#include "mongo/util/str.h"
6364
#include "mongo/util/text.h"
@@ -1429,7 +1430,7 @@ unsigned long long FiletimeToEpocMillis(FILETIME ft) {
14291430

14301431
uint64_t ns100 = ((static_cast<int64_t>(ft.dwHighDateTime) << 32) + ft.dwLowDateTime) -
14311432
kOneHundredNanosecondsSinceEpoch;
1432-
return ns100 / 1000;
1433+
return ns100 / 10000;
14331434
}
14341435

14351436
// MongoDB wants RFC 2253 (LDAP) formatted DN names for auth purposes
@@ -1679,6 +1680,20 @@ Status validatePeerCertificate(const std::string& remoteHost,
16791680
invariant(peerSubjectName);
16801681
*peerSubjectName = swSubjectName.getValue();
16811682

1683+
if (remoteHost.empty()) {
1684+
const auto exprThreshold = tlsX509ExpirationWarningThresholdDays;
1685+
if (exprThreshold > 0) {
1686+
const auto now = Date_t::now();
1687+
const auto expiration =
1688+
Date_t::fromMillisSinceEpoch(FiletimeToEpocMillis(cert->pCertInfo->NotAfter));
1689+
1690+
if ((now + Days(exprThreshold)) > expiration) {
1691+
tlsEmitWarningExpiringClientCertificate(*peerSubjectName,
1692+
duration_cast<Days>(expiration - now));
1693+
}
1694+
}
1695+
}
1696+
16821697
// This means the certificate chain is not valid.
16831698
// Ignore CRYPT_E_NO_REVOCATION_CHECK since most CAs lack revocation information especially test
16841699
// certificates

src/mongo/util/net/ssl_parameters.idl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,14 @@ server_parameters:
7878
set_at: [startup, runtime]
7979
cpp_class: ClusterMemberDNOverride
8080

81+
tlsX509ExpirationWarningThresholdDays:
82+
description: >-
83+
If a client connects to the server using an X509 certificate expiring in less
84+
than the configured number of days, a warning will be emitted to the server logs.
85+
Set this value to 0 to disable the warning.
86+
set_at: startup
87+
cpp_vartype: std::int32_t
88+
cpp_varname: "tlsX509ExpirationWarningThresholdDays"
89+
default: 30
90+
validator:
91+
gte: 0

0 commit comments

Comments
 (0)