diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2018-05-07 18:51:11 -0400 |
---|---|---|
committer | Sara Golemon <sara.golemon@mongodb.com> | 2018-05-15 22:11:45 -0400 |
commit | fa3fae04c144048bebf3f4c58e08b7b0c32743e1 (patch) | |
tree | 32cc44010126bb4e5d94e0c181aeb210f34edab8 /src/mongo | |
parent | cf339b8a8d8708e8b28747fe0cafee7cc79fe9a6 (diff) | |
download | mongo-fa3fae04c144048bebf3f4c58e08b7b0c32743e1.tar.gz |
SERVER-34735 Extract structured data from X509 subject names
Diffstat (limited to 'src/mongo')
-rw-r--r-- | src/mongo/client/async_client.cpp | 2 | ||||
-rw-r--r-- | src/mongo/client/dbclient.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_manager_test.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/auth/authz_manager_external_state.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/commands/authentication_commands.cpp | 14 | ||||
-rw-r--r-- | src/mongo/db/db.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/initialize_server_global_state.cpp | 2 | ||||
-rw-r--r-- | src/mongo/executor/network_interface_asio_auth.cpp | 2 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager.cpp | 123 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager.h | 13 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_apple.cpp | 134 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_openssl.cpp | 82 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_windows.cpp | 66 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_parameters.cpp | 8 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_types.h | 66 |
15 files changed, 351 insertions, 191 deletions
diff --git a/src/mongo/client/async_client.cpp b/src/mongo/client/async_client.cpp index 184f69689a3..374dd2cf7a7 100644 --- a/src/mongo/client/async_client.cpp +++ b/src/mongo/client/async_client.cpp @@ -135,7 +135,7 @@ Future<void> AsyncDBClient::authenticate(const BSONObj& params) { std::string clientName; #ifdef MONGO_CONFIG_SSL if (getSSLManager()) { - clientName = getSSLManager()->getSSLConfiguration().clientSubjectName; + clientName = getSSLManager()->getSSLConfiguration().clientSubjectName.toString(); } #endif diff --git a/src/mongo/client/dbclient.cpp b/src/mongo/client/dbclient.cpp index c93aa177fdd..3a7e95e819c 100644 --- a/src/mongo/client/dbclient.cpp +++ b/src/mongo/client/dbclient.cpp @@ -466,7 +466,7 @@ void DBClientBase::_auth(const BSONObj& params) { std::string clientName = ""; #ifdef MONGO_CONFIG_SSL if (sslManager() != nullptr) { - clientName = sslManager()->getSSLConfiguration().clientSubjectName; + clientName = sslManager()->getSSLConfiguration().clientSubjectName.toString(); } #endif diff --git a/src/mongo/db/auth/authorization_manager_test.cpp b/src/mongo/db/auth/authorization_manager_test.cpp index c382e0e8e00..75d078bfbc7 100644 --- a/src/mongo/db/auth/authorization_manager_test.cpp +++ b/src/mongo/db/auth/authorization_manager_test.cpp @@ -32,6 +32,7 @@ */ #include "mongo/base/status.h" #include "mongo/bson/mutable/document.h" +#include "mongo/config.h" #include "mongo/crypto/mechanism_scram.h" #include "mongo/crypto/sha1_block.h" #include "mongo/crypto/sha256_block.h" @@ -59,6 +60,12 @@ namespace mongo { namespace { +// Construct a simple, structured X509 name equivalent to "CN=mongodb.com" +SSLX509Name buildX509Name() { + return SSLX509Name(std::vector<std::vector<SSLX509Name::Entry>>( + {{{kOID_CommonName.toString(), 19 /* Printable String */, "mongodb.com"}}})); +} + void setX509PeerInfo(const transport::SessionHandle& session, SSLPeerInfo info) { auto& sslPeerInfo = SSLPeerInfo::forSession(session); sslPeerInfo = info; @@ -257,13 +264,14 @@ TEST_F(AuthorizationManagerTest, testAcquireV2User) { authzManager->releaseUser(v2cluster); } +#ifdef MONGO_CONFIG_SSL TEST_F(AuthorizationManagerTest, testLocalX509Authorization) { ServiceContextNoop serviceContext; transport::TransportLayerMock transportLayer{}; transport::SessionHandle session = transportLayer.createSession(); setX509PeerInfo( session, - SSLPeerInfo("CN=mongodb.com", {RoleName("read", "test"), RoleName("readWrite", "test")})); + SSLPeerInfo(buildX509Name(), {RoleName("read", "test"), RoleName("readWrite", "test")})); ServiceContext::UniqueClient client = serviceContext.makeClient("testClient", session); ServiceContext::UniqueOperationContext opCtx = client->makeOperationContext(); @@ -290,6 +298,7 @@ TEST_F(AuthorizationManagerTest, testLocalX509Authorization) { authzManager->releaseUser(x509User); } +#endif TEST_F(AuthorizationManagerTest, testLocalX509AuthorizationInvalidUser) { ServiceContextNoop serviceContext; @@ -297,7 +306,7 @@ TEST_F(AuthorizationManagerTest, testLocalX509AuthorizationInvalidUser) { transport::SessionHandle session = transportLayer.createSession(); setX509PeerInfo( session, - SSLPeerInfo("CN=mongodb.com", {RoleName("read", "test"), RoleName("write", "test")})); + SSLPeerInfo(buildX509Name(), {RoleName("read", "test"), RoleName("write", "test")})); ServiceContext::UniqueClient client = serviceContext.makeClient("testClient", session); ServiceContext::UniqueOperationContext opCtx = client->makeOperationContext(); diff --git a/src/mongo/db/auth/authz_manager_external_state.cpp b/src/mongo/db/auth/authz_manager_external_state.cpp index 1eef2c854d2..49e52a6455b 100644 --- a/src/mongo/db/auth/authz_manager_external_state.cpp +++ b/src/mongo/db/auth/authz_manager_external_state.cpp @@ -28,6 +28,7 @@ #include "mongo/platform/basic.h" +#include "mongo/config.h" #include "mongo/db/auth/authz_manager_external_state.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/operation_context.h" @@ -42,11 +43,17 @@ AuthzManagerExternalState::~AuthzManagerExternalState() = default; bool AuthzManagerExternalState::shouldUseRolesFromConnection(OperationContext* opCtx, const UserName& userName) { - if (!opCtx || !opCtx->getClient() || !opCtx->getClient()->session()) +#ifdef MONGO_CONFIG_SSL + if (!opCtx || !opCtx->getClient() || !opCtx->getClient()->session()) { return false; + } + auto& sslPeerInfo = SSLPeerInfo::forSession(opCtx->getClient()->session()); - return sslPeerInfo.subjectName == userName.getUser() && userName.getDB() == "$external" && - !sslPeerInfo.roles.empty(); + return sslPeerInfo.subjectName.toString() == userName.getUser() && + userName.getDB() == "$external" && !sslPeerInfo.roles.empty(); +#else + return false; +#endif } diff --git a/src/mongo/db/commands/authentication_commands.cpp b/src/mongo/db/commands/authentication_commands.cpp index ffe3fff78ee..00bf6dd23cb 100644 --- a/src/mongo/db/commands/authentication_commands.cpp +++ b/src/mongo/db/commands/authentication_commands.cpp @@ -88,7 +88,7 @@ Status _authenticateX509(OperationContext* opCtx, const UserName& user, const BS if (!getSSLManager()->getSSLConfiguration().hasCA) { return Status(ErrorCodes::AuthenticationFailed, "Unable to verify x.509 certificate, as no CA has been provided."); - } else if (user.getUser() != clientName) { + } else if (user.getUser() != clientName.toString()) { return Status(ErrorCodes::AuthenticationFailed, "There is no x.509 client certificate matching the user."); } else { @@ -236,14 +236,14 @@ bool CmdAuthenticate::run(OperationContext* opCtx, if (mechanism.empty()) { uasserted(ErrorCodes::BadValue, "Auth mechanism not specified"); } - UserName user; - auto& sslPeerInfo = SSLPeerInfo::forSession(opCtx->getClient()->session()); - if (mechanism == kX509AuthMechanism && !cmdObj.hasField("user")) { - user = UserName(sslPeerInfo.subjectName, dbname); - } else { - user = UserName(cmdObj.getStringField("user"), dbname); + UserName user(cmdObj.getStringField("user"), dbname); +#ifdef MONGO_CONFIG_SSL + if (mechanism == kX509AuthMechanism && user.getUser().empty()) { + auto& sslPeerInfo = SSLPeerInfo::forSession(opCtx->getClient()->session()); + user = UserName(sslPeerInfo.subjectName.toString(), dbname); } +#endif uassert(ErrorCodes::AuthenticationFailed, "No user name provided", !user.getUser().empty()); if (getTestCommandsEnabled() && user.getDB() == "admin" && diff --git a/src/mongo/db/db.cpp b/src/mongo/db/db.cpp index f9b47ed91de..5ddf80abdd9 100644 --- a/src/mongo/db/db.cpp +++ b/src/mongo/db/db.cpp @@ -373,7 +373,7 @@ ExitCode _initAndListen(int listenPort) { logMongodStartupWarnings(storageGlobalParams, serverGlobalParams, serviceContext); -#if MONGO_CONFIG_SSL +#ifdef MONGO_CONFIG_SSL if (sslGlobalParams.sslAllowInvalidCertificates && ((serverGlobalParams.clusterAuthMode.load() == ServerGlobalParams::ClusterAuthMode_x509) || sequenceContains(saslGlobalParams.authenticationMechanisms, "MONGODB-X509"))) { diff --git a/src/mongo/db/initialize_server_global_state.cpp b/src/mongo/db/initialize_server_global_state.cpp index 17b17180073..847873010ee 100644 --- a/src/mongo/db/initialize_server_global_state.cpp +++ b/src/mongo/db/initialize_server_global_state.cpp @@ -406,7 +406,7 @@ bool initializeServerGlobalState() { << saslCommandUserDBFieldName << "$external" << saslCommandUserFieldName - << getSSLManager()->getSSLConfiguration().clientSubjectName)); + << getSSLManager()->getSSLConfiguration().clientSubjectName.toString())); } #endif return true; diff --git a/src/mongo/executor/network_interface_asio_auth.cpp b/src/mongo/executor/network_interface_asio_auth.cpp index b9eefb60646..1a981b3d017 100644 --- a/src/mongo/executor/network_interface_asio_auth.cpp +++ b/src/mongo/executor/network_interface_asio_auth.cpp @@ -191,7 +191,7 @@ void NetworkInterfaceASIO::_authenticate(AsyncOp* op) { std::string clientName; #ifdef MONGO_CONFIG_SSL if (getSSLManager()) { - clientName = getSSLManager()->getSSLConfiguration().clientSubjectName; + clientName = getSSLManager()->getSSLConfiguration().clientSubjectName.toString(); } #endif diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index 79a99543475..ae3aa75d222 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -44,7 +44,6 @@ #include "mongo/util/net/ssl_types.h" #include "mongo/util/text.h" - namespace mongo { namespace { @@ -107,6 +106,72 @@ public: #ifdef MONGO_CONFIG_SSL namespace { +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL +// OpenSSL has a more complete library of OID to SN mappings. +std::string x509OidToShortName(const std::string& name) { + const auto* sn = OBJ_nid2sn(OBJ_txt2nid(entry.oid.c_str())); + if (!sn) { + return name; + } + return sn; +} +#else +// On Apple/Windows we have to provide our own mapping. +std::string x509OidToShortName(const std::string& name) { + static const std::map<std::string, std::string> kX509OidToShortNameMappings = { + {"0.9.2342.19200300.100.1.1", "UID"}, + {"0.9.2342.19200300.100.1.25", "DC"}, + {"1.2.840.113549.1.9.1", "emailAddress"}, + {"2.5.4.3", "CN"}, + {"2.5.4.6", "C"}, + {"2.5.4.7", "L"}, + {"2.5.4.8", "ST"}, + {"2.5.4.9", "STREET"}, + {"2.5.4.10", "O"}, + {"2.5.4.11", "OU"}, + }; + + auto it = kX509OidToShortNameMappings.find(name); + if (it == kX509OidToShortNameMappings.end()) { + return name; + } + return it->second; +} +#endif +} // namespace + +StatusWith<std::string> SSLX509Name::getOID(StringData oid) const { + for (const auto& rdn : _entries) { + for (const auto& entry : rdn) { + if (entry.oid == oid) { + return entry.value; + } + } + } + return {ErrorCodes::KeyNotFound, "OID does not exist"}; +} + +StringBuilder& operator<<(StringBuilder& os, const SSLX509Name& name) { + std::string comma; + for (const auto& rdn : name._entries) { + std::string plus; + os << comma; + for (const auto& entry : rdn) { + os << plus << x509OidToShortName(entry.oid) << "=" << escapeRfc2253(entry.value); + plus = "+"; + } + comma = ","; + } + return os; +} + +std::string SSLX509Name::toString() const { + StringBuilder os; + os << *this; + return os.str(); +} + +namespace { void canonicalizeClusterDN(std::vector<std::string>* dn) { // remove all RDNs we don't care about for (size_t i = 0; i < dn->size(); i++) { @@ -121,30 +186,62 @@ void canonicalizeClusterDN(std::vector<std::string>* dn) { } std::stable_sort(dn->begin(), dn->end()); } + +constexpr StringData kOID_DC = "0.9.2342.19200300.100.1.25"_sd; +constexpr StringData kOID_O = "2.5.4.10"_sd; +constexpr StringData kOID_OU = "2.5.4.11"_sd; + +std::vector<SSLX509Name::Entry> canonicalizeClusterDN( + const std::vector<std::vector<SSLX509Name::Entry>>& entries) { + std::vector<SSLX509Name::Entry> ret; + + for (const auto& rdn : entries) { + for (const auto& entry : rdn) { + if ((entry.oid != kOID_DC) && (entry.oid != kOID_O) && (entry.oid != kOID_OU)) { + continue; + } + ret.push_back(entry); + } + } + std::stable_sort(ret.begin(), ret.end()); + return ret; +} } // namespace +/** + * The behavior of isClusterMember() is subtly different when passed + * an SSLX509Name versus a StringData. + * + * The SSLX509Name version (immediately below) compares distinguished + * names in their raw, unescaped forms and provides a more reliable match. + * + * The StringData version attempts to do a simplified string compare + * with the serialized version of the server subject name. + * + * Because escaping is not checked in the StringData version, + * some not-strictly matching RDNs will appear to share O/OU/DC with the + * server subject name. Therefore, that variant should be called with care. + */ +bool SSLConfiguration::isClusterMember(const SSLX509Name& subject) const { + auto client = canonicalizeClusterDN(subject._entries); + auto server = canonicalizeClusterDN(serverSubjectName._entries); + + return !client.empty() && (client == server); +} + bool SSLConfiguration::isClusterMember(StringData subjectName) const { std::vector<std::string> clientRDN = StringSplitter::split(subjectName.toString(), ","); - std::vector<std::string> serverRDN = StringSplitter::split(serverSubjectName, ","); + std::vector<std::string> serverRDN = StringSplitter::split(serverSubjectName.toString(), ","); canonicalizeClusterDN(&clientRDN); canonicalizeClusterDN(&serverRDN); - if (clientRDN.size() == 0 || clientRDN.size() != serverRDN.size()) { - return false; - } - - for (size_t i = 0; i < serverRDN.size(); i++) { - if (clientRDN[i] != serverRDN[i]) { - return false; - } - } - return true; + return !clientRDN.empty() && (clientRDN == serverRDN); } BSONObj SSLConfiguration::getServerStatusBSON() const { BSONObjBuilder security; - security.append("SSLServerSubjectName", serverSubjectName); + security.append("SSLServerSubjectName", serverSubjectName.toString()); security.appendBool("SSLServerHasCertificateAuthority", hasCA); security.appendDate("SSLServerCertificateExpirationDate", serverCertificateExpirationDate); return security.obj(); diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index 71f8b5a5d66..90abfb18721 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -87,18 +87,11 @@ public: }; struct SSLConfiguration { - SSLConfiguration() : serverSubjectName(""), clientSubjectName("") {} - SSLConfiguration(const std::string& serverSubjectName, - const std::string& clientSubjectName, - const Date_t& serverCertificateExpirationDate) - : serverSubjectName(serverSubjectName), - clientSubjectName(clientSubjectName), - serverCertificateExpirationDate(serverCertificateExpirationDate) {} - bool isClusterMember(StringData subjectName) const; + bool isClusterMember(const SSLX509Name& subjectName) const; BSONObj getServerStatusBSON() const; - std::string serverSubjectName; - std::string clientSubjectName; + SSLX509Name serverSubjectName; + SSLX509Name clientSubjectName; Date_t serverCertificateExpirationDate; bool hasCA = false; }; diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp index fbb02049c8f..01569048e24 100644 --- a/src/mongo/util/net/ssl_manager_apple.cpp +++ b/src/mongo/util/net/ssl_manager_apple.cpp @@ -279,34 +279,33 @@ StatusWith<T> extractDictionaryValue(::CFDictionaryRef dict, ::CFStringRef key) return reinterpret_cast<T>(val); } -StatusWith<std::string> mapSubjectLabel(::CFStringRef label) { - if (!::CFStringCompare(label, ::kSecOIDCommonName, 0)) { - return {"CN"}; - } else if (!::CFStringCompare(label, ::kSecOIDCountryName, 0)) { - return {"C"}; - } else if (!::CFStringCompare(label, ::kSecOIDStateProvinceName, 0)) { - return {"ST"}; - } else if (!::CFStringCompare(label, ::kSecOIDLocalityName, 0)) { - return {"L"}; - } else if (!::CFStringCompare(label, ::kSecOIDOrganizationName, 0)) { - return {"O"}; - } else if (!::CFStringCompare(label, ::kSecOIDOrganizationalUnitName, 0)) { - return {"OU"}; - } else if (!::CFStringCompare(label, ::kSecOIDStreetAddress, 0)) { - return {"STREET"}; - } else if (!::CFStringCompare(label, CFSTR("0.9.2342.19200300.100.1.25"), 0)) { - return {"DC"}; - } else if (!::CFStringCompare(label, CFSTR("0.9.2342.19200300.100.1.1"), 0)) { - return {"UID"}; - } else if (!::CFStringCompare(label, ::kSecOIDEmailAddress, 0)) { - return {"emailAddress"}; - } - - return toString(label); +StatusWith<SSLX509Name::Entry> extractSingleOIDEntry(::CFDictionaryRef entry) { + auto swLabel = extractDictionaryValue<::CFStringRef>(entry, ::kSecPropertyKeyLabel); + if (!swLabel.isOK()) { + return swLabel.getStatus(); + } + auto swLabelStr = toString(swLabel.getValue()); + if (!swLabelStr.isOK()) { + return swLabelStr.getStatus(); + } + + auto swValue = extractDictionaryValue<::CFStringRef>(entry, ::kSecPropertyKeyValue); + if (!swValue.isOK()) { + return swValue.getStatus(); + } + auto swValueStr = toString(swValue.getValue()); + if (!swValueStr.isOK()) { + return swValueStr.getStatus(); + } + + // Secure Transport doesn't give us access to the specific string type, + // so regard all strings as ASN1_PRINTABLESTRING on this platform. + return SSLX509Name::Entry( + std::move(swLabelStr.getValue()), 19, std::move(swValueStr.getValue())); } -// Encode a raw DER subject sequence into a human readable subject name (RFC 2253). -StatusWith<std::string> extractSubjectName(::CFDictionaryRef dict) { +// Translate a raw DER subject sequence into a structured subject name. +StatusWith<SSLX509Name> extractSubjectName(::CFDictionaryRef dict) { auto swSubject = extractDictionaryValue<::CFDictionaryRef>(dict, ::kSecOIDX509V1SubjectName); if (!swSubject.isOK()) { return swSubject.getStatus(); @@ -320,7 +319,7 @@ StatusWith<std::string> extractSubjectName(::CFDictionaryRef dict) { auto elems = swElems.getValue(); const auto nElems = ::CFArrayGetCount(elems); - StringBuilder ret; + std::vector<std::vector<SSLX509Name::Entry>> ret; for (auto i = nElems; i; --i) { auto elem = reinterpret_cast<::CFDictionaryRef>(::CFArrayGetValueAtIndex(elems, i - 1)); invariant(elem); @@ -329,30 +328,46 @@ StatusWith<std::string> extractSubjectName(::CFDictionaryRef dict) { "Subject name element is not a dictionary"}; } - auto swLabel = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyLabel); - if (!swLabel.isOK()) { - return swLabel.getStatus(); - } - auto swLabelStr = mapSubjectLabel(swLabel.getValue()); - if (!swLabelStr.isOK()) { - return swLabelStr.getStatus(); + auto swType = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyType); + if (!swType.isOK()) { + return swType.getStatus(); } - auto swValue = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyValue); - if (!swValue.isOK()) { - return swValue.getStatus(); - } - auto swValueStr = toString(swValue.getValue()); - if (!swValueStr.isOK()) { - return swValueStr.getStatus(); - } + if (!::CFStringCompare(swType.getValue(), CFSTR("section"), 0)) { + // Multi-value RDN. + auto swList = extractDictionaryValue<::CFArrayRef>(elem, ::kSecPropertyKeyValue); + if (!swList.isOK()) { + return swList.getStatus(); + } + const auto nRDNAttrs = ::CFArrayGetCount(swList.getValue()); + std::vector<SSLX509Name::Entry> rdn; + for (auto j = nRDNAttrs; j; --j) { + auto rdnElem = reinterpret_cast<::CFDictionaryRef>( + ::CFArrayGetValueAtIndex(swList.getValue(), j - 1)); + invariant(rdnElem); + if (::CFGetTypeID(rdnElem) != ::CFDictionaryGetTypeID()) { + return {ErrorCodes::InvalidSSLConfiguration, + "Subject name sub-element is not a dictionary"}; + } + auto swEntry = extractSingleOIDEntry(rdnElem); + if (!swEntry.isOK()) { + return swEntry.getStatus(); + } + rdn.push_back(std::move(swEntry.getValue())); + } + ret.push_back(std::move(rdn)); - ret << swLabelStr.getValue() << '=' << escapeRfc2253(swValueStr.getValue()); - if (i > 1) { - ret << ","; + } else { + // Single Value RDN. + auto swEntry = extractSingleOIDEntry(elem); + if (!swEntry.isOK()) { + return swEntry.getStatus(); + } + ret.push_back(std::vector<SSLX509Name::Entry>({std::move(swEntry.getValue())})); } } - return ret.str(); + + return SSLX509Name(std::move(ret)); } StatusWith<mongo::Date_t> extractValidityDate(::CFDictionaryRef dict, @@ -635,7 +650,7 @@ StatusWith<CFUniquePtr<::CFArrayRef>> loadPEM(const std::string& keyfilepath, return std::move(cfcerts); } -StatusWith<std::string> certificateGetSubject(::SecCertificateRef cert, Date_t* expire = nullptr) { +StatusWith<SSLX509Name> certificateGetSubject(::SecCertificateRef cert, Date_t* expire = nullptr) { // Fetch expiry range and full subject name. CFUniquePtr<::CFMutableArrayRef> oids( ::CFArrayCreateMutable(nullptr, expire ? 3 : 1, &::kCFTypeArrayCallBacks)); @@ -682,7 +697,7 @@ StatusWith<std::string> certificateGetSubject(::SecCertificateRef cert, Date_t* return subject; } -StatusWith<std::string> certificateGetSubject(::CFArrayRef certs, Date_t* expire = nullptr) { +StatusWith<SSLX509Name> certificateGetSubject(::CFArrayRef certs, Date_t* expire = nullptr) { if (::CFArrayGetCount(certs) <= 0) { return {ErrorCodes::InvalidSSLConfiguration, "No certificates in certificate list"}; } @@ -810,7 +825,7 @@ StatusWith<CFUniquePtr<::CFArrayRef>> copyMatchingCertificate( if (!swCertSubject.isOK()) { return swCertSubject.getStatus(); } - if (swCertSubject.getValue() == selector.subject) { + if (swCertSubject.getValue().toString() == selector.subject) { ::CFArrayAppendValue(cfresult.get(), ident); break; } @@ -1331,18 +1346,17 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCe certErr << san << " "; } - } else if (peerSubjectName.find("CN=") != std::string::npos) { - const auto cnBegin = peerSubjectName.find("CN=") + 3; - auto cnEnd = peerSubjectName.find(",", cnBegin); - if (cnEnd == std::string::npos) { - cnEnd = peerSubjectName.size(); - } - const auto commonName = peerSubjectName.substr(cnBegin, cnEnd - cnBegin); - cnMatch = hostNameMatchForX509Certificates(remoteHost, commonName); - certErr << "CN: " << commonName; - } else { - certErr << "No Common Name (CN) or Subject Alternate Names (SAN) found"; + auto swCN = peerSubjectName.getOID(kOID_CommonName); + if (swCN.isOK()) { + auto commonName = std::move(swCN.getValue()); + if (hostNameMatchForX509Certificates(remoteHost, commonName)) { + cnMatch = true; + } + certErr << "CN: " << commonName; + } else { + certErr << "No Common Name (CN) or Subject Alternate Names (SAN) found"; + } } if (!sanMatch && !cnMatch) { diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index 20abd7b6421..6b48b228926 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -176,6 +176,9 @@ IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(ASN1_SEQUENCE_ANY, ASN1_SET_ANY, ASN const STACK_OF(X509_EXTENSION) * X509_get0_extensions(const X509* peerCert) { return peerCert->cert_info->extensions; } +inline int X509_NAME_ENTRY_set(const X509_NAME_ENTRY* ne) { + return ne->set; +} #endif /** @@ -394,7 +397,7 @@ private: */ bool _parseAndValidateCertificate(const std::string& keyFile, const std::string& keyPassword, - std::string* subjectName, + SSLX509Name* subjectName, Date_t* serverNotAfter); @@ -497,23 +500,42 @@ SSLManagerInterface* getSSLManager() { return NULL; } -std::string getCertificateSubjectName(X509* cert) { - std::string result; +SSLX509Name getCertificateSubjectX509Name(X509* cert) { + std::vector<std::vector<SSLX509Name::Entry>> entries; + + auto name = X509_get_subject_name(cert); + int count = X509_NAME_entry_count(name); + int prevSet = -1; + std::vector<SSLX509Name::Entry> rdn; + for (int i = count - 1; i >= 0; --i) { + auto* entry = X509_NAME_get_entry(name, i); + + const auto currentSet = X509_NAME_ENTRY_set(entry); + if (currentSet != prevSet) { + if (!rdn.empty()) { + entries.push_back(std::move(rdn)); + rdn = std::vector<SSLX509Name::Entry>(); + } + prevSet = currentSet; + } - BIO* out = BIO_new(BIO_s_mem()); - uassert(16884, "unable to allocate BIO memory", NULL != out); - ON_BLOCK_EXIT(BIO_free, out); + char buffer[128]; + // OBJ_obj2txt can only fail if we pass a nullptr from get_object, + // or if OpenSSL's BN library falls over. + // In either case, just panic. + uassert(ErrorCodes::InvalidSSLConfiguration, + "Unable to parse certiciate subject name", + OBJ_obj2txt(buffer, sizeof(buffer), X509_NAME_ENTRY_get_object(entry), 1) > 0); - if (X509_NAME_print_ex(out, X509_get_subject_name(cert), 0, XN_FLAG_RFC2253) >= 0) { - if (BIO_number_written(out) > 0) { - result.resize(BIO_number_written(out)); - BIO_read(out, &result[0], result.size()); - } - } else { - log() << "failed to convert subject name to RFC2253 format"; + const auto* str = X509_NAME_ENTRY_get_data(entry); + rdn.emplace_back( + buffer, str->type, std::string(reinterpret_cast<const char*>(str->data), str->length)); + } + if (!rdn.empty()) { + entries.push_back(std::move(rdn)); } - return result; + return SSLX509Name(std::move(entries)); } SSLConnectionOpenSSL::SSLConnectionOpenSSL(SSL_CTX* context, @@ -803,7 +825,7 @@ unsigned long long SSLManagerOpenSSL::_convertASN1ToMillis(ASN1_TIME* asn1time) bool SSLManagerOpenSSL::_parseAndValidateCertificate(const std::string& keyFile, const std::string& keyPassword, - std::string* subjectName, + SSLX509Name* subjectName, Date_t* serverCertificateExpirationDate) { BIO* inBIO = BIO_new(BIO_s_file()); if (inBIO == NULL) { @@ -830,7 +852,7 @@ bool SSLManagerOpenSSL::_parseAndValidateCertificate(const std::string& keyFile, } ON_BLOCK_EXIT(X509_free, x509); - *subjectName = getCertificateSubjectName(x509); + *subjectName = getCertificateSubjectX509Name(x509); if (serverCertificateExpirationDate != NULL) { unsigned long long notBeforeMillis = _convertASN1ToMillis(X509_get_notBefore(x509)); if (notBeforeMillis == 0) { @@ -1246,8 +1268,8 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer } // TODO: check optional cipher restriction, using cert. - std::string peerSubjectName = getCertificateSubjectName(peerCert); - LOG(2) << "Accepted TLS connection from peer: " << peerSubjectName; + auto peerSubject = getCertificateSubjectX509Name(peerCert); + LOG(2) << "Accepted TLS connection from peer: " << peerSubject; StatusWith<stdx::unordered_set<RoleName>> swPeerCertificateRoles = _parsePeerRoles(peerCert); if (!swPeerCertificateRoles.isOK()) { @@ -1258,7 +1280,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer // perform hostname validation of the remote server if (remoteHost.empty()) { return boost::make_optional( - SSLPeerInfo(peerSubjectName, std::move(swPeerCertificateRoles.getValue()))); + SSLPeerInfo(peerSubject, std::move(swPeerCertificateRoles.getValue()))); } // Try to match using the Subject Alternate Name, if it exists. @@ -1288,19 +1310,19 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer } } sk_GENERAL_NAME_pop_free(sanNames, GENERAL_NAME_free); - } else if (peerSubjectName.find("CN=") != std::string::npos) { + } else { // If Subject Alternate Name (SAN) doesn't exist and Common Name (CN) does, // check Common Name. - int cnBegin = peerSubjectName.find("CN=") + 3; - int cnEnd = peerSubjectName.find(",", cnBegin); - std::string commonName = peerSubjectName.substr(cnBegin, cnEnd - cnBegin); - - if (hostNameMatchForX509Certificates(remoteHost, commonName)) { - cnMatch = true; + auto swCN = peerSubject.getOID(kOID_CommonName); + if (swCN.isOK()) { + auto commonName = std::move(swCN.getValue()); + if (hostNameMatchForX509Certificates(remoteHost, commonName)) { + cnMatch = true; + } + certificateNames << "CN: " << commonName; + } else { + certificateNames << "No Common Name (CN) or Subject Alternate Names (SAN) found"; } - certificateNames << "CN: " << commonName; - } else { - certificateNames << "No Common Name (CN) or Subject Alternate Names (SAN) found"; } if (!sanMatch && !cnMatch) { @@ -1316,7 +1338,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer } } - return boost::make_optional(SSLPeerInfo(peerSubjectName, stdx::unordered_set<RoleName>())); + return boost::make_optional(SSLPeerInfo(peerSubject, stdx::unordered_set<RoleName>())); } diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp index 14ed8c9e68f..d96595f48a5 100644 --- a/src/mongo/util/net/ssl_manager_windows.cpp +++ b/src/mongo/util/net/ssl_manager_windows.cpp @@ -294,7 +294,7 @@ private: void _handshake(SSLConnectionWindows* conn, bool client); Status _validateCertificate(PCCERT_CONTEXT cert, - std::string* subjectName, + SSLX509Name* subjectName, Date_t* serverCertificateExpirationDate); Status _initChainEngines(bool hasCAFile); @@ -1356,34 +1356,8 @@ unsigned long long FiletimeToEpocMillis(FILETIME ft) { return ns100 / 1000; } -StatusWith<std::string> mapSubjectLabel(LPSTR label) { - if (strcmp(label, szOID_COMMON_NAME) == 0) { - return {"CN"}; - } else if (strcmp(label, szOID_COUNTRY_NAME) == 0) { - return {"C"}; - } else if (strcmp(label, szOID_STATE_OR_PROVINCE_NAME) == 0) { - return {"ST"}; - } else if (strcmp(label, szOID_LOCALITY_NAME) == 0) { - return {"L"}; - } else if (strcmp(label, szOID_ORGANIZATION_NAME) == 0) { - return {"O"}; - } else if (strcmp(label, szOID_ORGANIZATIONAL_UNIT_NAME) == 0) { - return {"OU"}; - } else if (strcmp(label, szOID_STREET_ADDRESS) == 0) { - return {"STREET"}; - } else if (strcmp(label, szOID_DOMAIN_COMPONENT) == 0) { - return {"DC"}; - } else if (strcmp(label, szOID_RSA_emailAddr) == 0) { - return {"emailAddress"}; - } else if (strcmp(label, "0.9.2342.19200300.100.1.1") == 0) { - return {"UID"}; - } - - return label; -} - // MongoDB wants RFC 2253 (LDAP) formatted DN names for auth purposes -StatusWith<std::string> getCertificateSubjectName(PCCERT_CONTEXT cert) { +StatusWith<SSLX509Name> getCertificateSubjectName(PCCERT_CONTEXT cert) { auto swBlob = decodeObject(X509_NAME, cert->pCertInfo->Subject.pbData, cert->pCertInfo->Subject.cbData); @@ -1394,14 +1368,13 @@ StatusWith<std::string> getCertificateSubjectName(PCCERT_CONTEXT cert) { PCERT_NAME_INFO nameInfo = reinterpret_cast<PCERT_NAME_INFO>(swBlob.getValue().data()); - StringBuilder output; - - bool addComma = false; + std::vector<std::vector<SSLX509Name::Entry>> entries; // Iterate in reverse order for (int64_t i = nameInfo->cRDN - 1; i >= 0; i--) { - for (DWORD j = 0; j < nameInfo->rgRDN[i].cRDNAttr; j++) { - CERT_RDN_ATTR& rdnAttribute = nameInfo->rgRDN[i].rgRDNAttr[j]; + std::vector<SSLX509Name::Entry> rdn; + for (DWORD j = nameInfo->rgRDN[i].cRDNAttr; j > 0; --j) { + CERT_RDN_ATTR& rdnAttribute = nameInfo->rgRDN[i].rgRDNAttr[j - 1]; DWORD needed = CertRDNValueToStrW(rdnAttribute.dwValueType, &rdnAttribute.Value, NULL, 0); @@ -1413,29 +1386,16 @@ StatusWith<std::string> getCertificateSubjectName(PCCERT_CONTEXT cert) { const_cast<wchar_t*>(wstr.data()), needed); invariant(needed == converted); - - auto swLabel = mapSubjectLabel(rdnAttribute.pszObjId); - if (!swLabel.isOK()) { - return swLabel.getStatus(); - } - - if (addComma) { - output << ','; - } - - output << swLabel.getValue(); - output << '='; - output << escapeRfc2253(toUtf8String(wstr)); - - addComma = true; + rdn.emplace_back(rdnAttribute.pszObjId, rdnAttribute.dwValueType, toUtf8String(wstr)); } + entries.push_back(std::move(rdn)); } - return output.str(); + return SSLX509Name(std::move(entries)); } Status SSLManagerWindows::_validateCertificate(PCCERT_CONTEXT cert, - std::string* subjectName, + SSLX509Name* subjectName, Date_t* serverCertificateExpirationDate) { auto swCert = getCertificateSubjectName(cert); @@ -1512,7 +1472,7 @@ Status validatePeerCertificate(const std::string& remoteHost, HCERTCHAINENGINE certChainEngine, bool allowInvalidCertificates, bool allowInvalidHostnames, - std::string* peerSubjectName) { + SSLX509Name* peerSubjectName) { CERT_CHAIN_PARA certChainPara; memset(&certChainPara, 0, sizeof(certChainPara)); certChainPara.cbSize = sizeof(CERT_CHAIN_PARA); @@ -1619,7 +1579,7 @@ Status validatePeerCertificate(const std::string& remoteHost, << integerToHex(certChainPolicyStatus.dwError) << "): " << errnoWithDescription(certChainPolicyStatus.dwError); warning() << msg.ss.str(); - *peerSubjectName = ""; + *peerSubjectName = SSLX509Name(); return Status::OK(); } else if (allowInvalidHostnames) { warning() << msg.ss.str(); @@ -1667,7 +1627,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerWindows::parseAndValidatePeer } UniqueCertificate certHolder(cert); - std::string peerSubjectName; + SSLX509Name peerSubjectName; // Validate against the local machine store first since it is easier to manage programmatically. Status validateCertMachine = validatePeerCertificate(remoteHost, diff --git a/src/mongo/util/net/ssl_parameters.cpp b/src/mongo/util/net/ssl_parameters.cpp index f98119ef487..2ebbb5cf48d 100644 --- a/src/mongo/util/net/ssl_parameters.cpp +++ b/src/mongo/util/net/ssl_parameters.cpp @@ -177,12 +177,8 @@ public: serverGlobalParams.clusterAuthMode.store(ServerGlobalParams::ClusterAuthMode_sendX509); #ifdef MONGO_CONFIG_SSL setInternalUserAuthParams( - BSON(saslCommandMechanismFieldName - << "MONGODB-X509" - << saslCommandUserDBFieldName - << "$external" - << saslCommandUserFieldName - << getSSLManager()->getSSLConfiguration().clientSubjectName)); + BSON(saslCommandMechanismFieldName << "MONGODB-X509" << saslCommandUserDBFieldName + << "$external")); #endif } else if (str == "x509" && oldMode == ServerGlobalParams::ClusterAuthMode_sendX509) { serverGlobalParams.clusterAuthMode.store(ServerGlobalParams::ClusterAuthMode_x509); diff --git a/src/mongo/util/net/ssl_types.h b/src/mongo/util/net/ssl_types.h index 91a9df3ddbf..0a0fb887274 100644 --- a/src/mongo/util/net/ssl_types.h +++ b/src/mongo/util/net/ssl_types.h @@ -29,22 +29,84 @@ #include <string> +#include "mongo/bson/util/builder.h" #include "mongo/db/auth/role_name.h" #include "mongo/stdx/unordered_set.h" #include "mongo/transport/session.h" namespace mongo { +constexpr StringData kOID_CommonName = "2.5.4.3"_sd; + +/** + * Represents a structed X509 certificate subject name. + * For example: C=US,O=MongoDB,OU=KernelTeam,CN=server + * would be held as a four element vector of Entries. + * The first entry of which yould be broken down something like: + * {{"2.5.4.6", 19, "US"}}. + * Note that _entries is a vector of vectors to accomodate + * multi-value RDNs. + */ +class SSLX509Name { +public: + struct Entry { + Entry(std::string oid, int type, std::string value) + : oid(std::move(oid)), type(type), value(std::move(value)) {} + std::string oid; // e.g. "2.5.4.8" (ST) + int type; // e.g. 19 (PRINTABLESTRING) + std::string value; + auto equalityLens() const { + return std::tie(oid, type, value); + } + }; + + SSLX509Name() = default; + explicit SSLX509Name(std::vector<std::vector<Entry>> entries) : _entries(std::move(entries)) {} + + /** + * Retreive the first instance of the value for a given OID in this name. + * Returns ErrorCodes::KeyNotFound if the OID does not exist. + */ + StatusWith<std::string> getOID(StringData oid) const; + + bool empty() const { + return std::all_of( + _entries.cbegin(), _entries.cend(), [](const auto& e) { return e.empty(); }); + } + + friend StringBuilder& operator<<(StringBuilder&, const SSLX509Name&); + std::string toString() const; + + friend bool operator==(const SSLX509Name& lhs, const SSLX509Name& rhs) { + return lhs._entries == rhs._entries; + } + friend bool operator!=(const SSLX509Name& lhs, const SSLX509Name& rhs) { + return !(lhs._entries == rhs._entries); + } + +private: + friend struct SSLConfiguration; + std::vector<std::vector<Entry>> _entries; +}; + +std::ostream& operator<<(std::ostream&, const SSLX509Name&); +inline bool operator==(const SSLX509Name::Entry& lhs, const SSLX509Name::Entry& rhs) { + return lhs.equalityLens() == rhs.equalityLens(); +} +inline bool operator<(const SSLX509Name::Entry& lhs, const SSLX509Name::Entry& rhs) { + return lhs.equalityLens() < rhs.equalityLens(); +} + /** * Contains information extracted from the peer certificate which is consumed by subsystems * outside of the networking stack. */ struct SSLPeerInfo { - SSLPeerInfo(std::string subjectName, stdx::unordered_set<RoleName> roles) + SSLPeerInfo(SSLX509Name subjectName, stdx::unordered_set<RoleName> roles) : subjectName(std::move(subjectName)), roles(std::move(roles)) {} SSLPeerInfo() = default; - std::string subjectName; + SSLX509Name subjectName; stdx::unordered_set<RoleName> roles; static SSLPeerInfo& forSession(const transport::SessionHandle& session); |