diff options
Diffstat (limited to 'src')
20 files changed, 648 insertions, 128 deletions
diff --git a/src/mongo/client/sasl_scram_client_conversation.h b/src/mongo/client/sasl_scram_client_conversation.h index ba1631115e3..ab35eac775f 100644 --- a/src/mongo/client/sasl_scram_client_conversation.h +++ b/src/mongo/client/sasl_scram_client_conversation.h @@ -140,7 +140,7 @@ public: if (std::is_same<SHA1Block, HashBlock>::value) { return val.toString(); } else { - return mongo::saslPrep(val); + return icuSaslPrep(val); } } diff --git a/src/mongo/db/auth/sasl_scram_server_conversation.h b/src/mongo/db/auth/sasl_scram_server_conversation.h index 5c1e56f8e64..d5d77a870df 100644 --- a/src/mongo/db/auth/sasl_scram_server_conversation.h +++ b/src/mongo/db/auth/sasl_scram_server_conversation.h @@ -64,7 +64,7 @@ public: if (std::is_same<SHA1Block, HashBlock>::value) { return str.toString(); } else { - return mongo::saslPrep(str); + return icuSaslPrep(str); } } diff --git a/src/mongo/db/auth/security_key.cpp b/src/mongo/db/auth/security_key.cpp index 00344494374..9aa4b3c7626 100644 --- a/src/mongo/db/auth/security_key.cpp +++ b/src/mongo/db/auth/security_key.cpp @@ -76,7 +76,7 @@ public: return boost::none; } - auto swSaslPassword = saslPrep(password); + auto swSaslPassword = icuSaslPrep(password); if (!swSaslPassword.isOK()) { error() << "Could not prep security key file for SCRAM-SHA-256: " << swSaslPassword.getStatus(); diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index 967af4dfdbe..df1d33ba54f 100644 --- a/src/mongo/db/commands/user_management_commands.cpp +++ b/src/mongo/db/commands/user_management_commands.cpp @@ -704,7 +704,7 @@ Status buildCredentials(BSONObjBuilder* builder, const auth::CreateOrUpdateUserA if (!args.digestPassword) { return {ErrorCodes::BadValue, "Use of SCRAM-SHA-256 requires undigested passwords"}; } - const auto swPwd = saslPrep(args.password); + const auto swPwd = icuSaslPrep(args.password); if (!swPwd.isOK()) { return swPwd.getStatus(); } diff --git a/src/mongo/util/icu.cpp b/src/mongo/util/icu.cpp index 4b9d797a5f5..9825da56fde 100644 --- a/src/mongo/util/icu.cpp +++ b/src/mongo/util/icu.cpp @@ -172,11 +172,18 @@ private: }; } // namespace -} // namespace mongo -mongo::StatusWith<std::string> mongo::saslPrep(StringData str, UStringPrepOptions options) try { +StatusWith<std::string> icuSaslPrep(StringData str, UStringPrepOptions options) try { const auto opts = (options == kUStringPrepDefault) ? USPREP_DEFAULT : USPREP_ALLOW_UNASSIGNED; return USPrep(USPREP_RFC4013_SASLPREP).prepare(UString::fromUTF8(str), opts).toUTF8(); } catch (const DBException& e) { return e.toStatus(); } + +StatusWith<std::string> icuX509DNPrep(StringData str) try { + return USPrep(USPREP_RFC4518_LDAP).prepare(UString::fromUTF8(str), USPREP_DEFAULT).toUTF8(); +} catch (const DBException& e) { + return e.toStatus(); +} + +} // namespace mongo diff --git a/src/mongo/util/icu.h b/src/mongo/util/icu.h index 52056e8b806..99baf411db7 100644 --- a/src/mongo/util/icu.h +++ b/src/mongo/util/icu.h @@ -52,6 +52,12 @@ enum UStringPrepOptions { * Attempt to apply RFC4013 saslPrep to the target string. * Normalizes unicode sequences for SCRAM authentication. */ -StatusWith<std::string> saslPrep(StringData str, UStringPrepOptions = kUStringPrepDefault); +StatusWith<std::string> icuSaslPrep(StringData str, UStringPrepOptions = kUStringPrepDefault); + +/** + * Attempt to apply RFC4518 string prep to the target string, this normalizes an X509 DN + * so it can be compared against other X509 DNs + */ +StatusWith<std::string> icuX509DNPrep(StringData str); } // namespace mongo diff --git a/src/mongo/util/icu_test.cpp b/src/mongo/util/icu_test.cpp index a14faeb42c0..38a5bd2a265 100644 --- a/src/mongo/util/icu_test.cpp +++ b/src/mongo/util/icu_test.cpp @@ -42,7 +42,7 @@ struct testCases { bool success; }; -TEST(ICUTest, saslPrep) { +TEST(ICUTest, icuSaslPrep) { const testCases tests[] = { // U+0065 LATIN SMALL LETTER E + U+0301 COMBINING ACUTE ACCENT // U+00E9 LATIN SMALL LETTER E WITH ACUTE @@ -66,7 +66,7 @@ TEST(ICUTest, saslPrep) { }; for (const auto test : tests) { - auto ret = saslPrep(test.original); + auto ret = icuSaslPrep(test.original); ASSERT_EQ(ret.isOK(), test.success); if (test.success) { ASSERT_OK(ret); diff --git a/src/mongo/util/net/SConscript b/src/mongo/util/net/SConscript index 0918e6a9d9f..caca867099f 100644 --- a/src/mongo/util/net/SConscript +++ b/src/mongo/util/net/SConscript @@ -130,6 +130,7 @@ env.Library( '$BUILD_DIR/mongo/db/service_context', '$BUILD_DIR/mongo/idl/server_parameter', '$BUILD_DIR/mongo/util/background_job', + '$BUILD_DIR/mongo/util/icu', '$BUILD_DIR/mongo/util/winutil', ], ) diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index c6e869e7f7a..bce07236b3e 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -47,6 +47,7 @@ #include "mongo/platform/overflow_arithmetic.h" #include "mongo/transport/session.h" #include "mongo/util/hex.h" +#include "mongo/util/icu.h" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" #include "mongo/util/net/ssl_options.h" @@ -54,101 +55,411 @@ namespace mongo { namespace { + +// Some of these duplicate the std::isalpha/std::isxdigit because we don't want them to be +// affected by the current locale. +inline bool isAlpha(char ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); +} + +inline bool isDigit(char ch) { + return (ch >= '0' && ch <= '9'); +} + +inline bool isHex(char ch) { + return isDigit(ch) || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); +} + +// This function returns true if the character is supposed to be escaped according to the rules +// in RFC4514. The exception to the RFC the space character ' ' and the '#', because we've not +// required users to escape spaces or sharps in DNs in the past. +inline bool isEscaped(char ch) { + switch (ch) { + case '"': + case '+': + case ',': + case ';': + case '<': + case '>': + case '\\': + return true; + default: + return false; + } +} + +// These characters may appear escaped in a string or not, but must not appear as the first +// character. +inline bool isMayBeEscaped(char ch) { + switch (ch) { + case ' ': + case '#': + case '=': + return true; + default: + return false; + } +} + +/* + * This class parses out the components of a DN according to RFC4514. + * + * It takes in a StringData to the DN to be parsed, the buffer containing the StringData + * must remain in scope for the duration that it is being parsed. + */ +class RFC4514Parser { +public: + explicit RFC4514Parser(StringData sd) : _str(sd), _it(_str.begin()) {} + + std::string extractAttributeName(); + + enum ValueTerminator { + NewRDN, // The value ended in ',' + MultiValue, // The value ended in '+' + Done // The value ended with the end of the string + }; + + // Returns a decoded string representing one value in an RDN, and the way the value was + // terminated. + std::pair<std::string, ValueTerminator> extractValue(); + + bool done() const { + return _it == _str.end(); + } + + void skipSpaces() { + while (!done() && _cur() == ' ') { + _advance(); + } + } + +private: + char _cur() const { + uassert(51036, "Overflowed string while parsing DN string", !done()); + return *_it; + } + + char _advance() { + invariant(!done()); + ++_it; + return done() ? '\0' : _cur(); + } + + StringData _str; + StringData::const_iterator _it; +}; + +// Parses an attribute name according to the rules for the "descr" type defined in +// https://tools.ietf.org/html/rfc4512 +std::string RFC4514Parser::extractAttributeName() { + StringBuilder sb; + + auto ch = _cur(); + stdx::function<bool(char ch)> characterCheck; + // If the first character is a digit, then this is an OID and can only contain + // numbers and '.' + if (isDigit(ch)) { + characterCheck = [](char ch) { return (isDigit(ch) || ch == '.'); }; + // If the first character is an alpha, then this is a short name and can only + // contain alpha/digit/hyphen characters. + } else if (isAlpha(ch)) { + characterCheck = [](char ch) { return (isAlpha(ch) || isDigit(ch) || ch == '-'); }; + // Otherwise this is an invalid attribute name + } else { + uasserted(ErrorCodes::BadValue, + str::stream() << "DN attribute names must begin with either a digit or an alpha" + << " not \'" + << ch + << "\'"); + } + + for (; ch != '=' && !done(); ch = _advance()) { + if (ch == ' ') { + continue; + } + uassert(ErrorCodes::BadValue, + str::stream() << "DN attribute name contains an invalid character \'" << ch << "\'", + characterCheck(ch)); + sb << ch; + } + + if (!done()) { + _advance(); + } + + return sb.str(); +} + +std::pair<std::string, RFC4514Parser::ValueTerminator> RFC4514Parser::extractValue() { + StringBuilder sb; + + // The RFC states the spaces at the beginning and end of the value must be escaped, which + // means we should skip any leading unescaped spaces. + skipSpaces(); + + // Every time we see an escaped space ("\ "), we increment this counter. Every time we see + // anything non-space character we reset this counter to zero. That way we'll know the number + // of consecutive escaped spaces at the end of the string there are. + int trailingSpaces = 0; + + char ch = _cur(); + uassert(ErrorCodes::BadValue, "Raw DER sequences are not supported in DN strings", ch != '#'); + for (; ch != ',' && ch != '+' && !done(); ch = _advance()) { + if (ch == '\\') { + ch = _advance(); + if (isEscaped(ch)) { + sb << ch; + trailingSpaces = 0; + } else if (isHex(ch)) { + const std::array<char, 2> hexValStr = {ch, _advance()}; + + uassert(ErrorCodes::BadValue, + str::stream() << "Escaped hex value contains invalid character \'" + << hexValStr[1] + << "\'", + isHex(hexValStr[1])); + const char hexVal = uassertStatusOK(fromHex(StringData(hexValStr.data(), 2))); + sb << hexVal; + if (hexVal != ' ') { + trailingSpaces = 0; + } else { + trailingSpaces++; + } + } else if (isMayBeEscaped(ch)) { + // It is legal to escape whitespace, but we don't count it as an "escaped" + // character because we don't require it to be escaped within the value, that is + // "C=New York" is legal, and so is "C=New\ York" + // + // The exception is that leading and trailing whitespace must be escaped or else + // it will be trimmed. + sb << ch; + if (ch == ' ') { + trailingSpaces++; + } else { + trailingSpaces = 0; + } + } else { + uasserted(ErrorCodes::BadValue, + str::stream() << "Invalid escaped character \'" << ch << "\'"); + } + } else if (isEscaped(ch)) { + uasserted(ErrorCodes::BadValue, + str::stream() << "Found unescaped character that should be escaped: \'" << ch + << "\'"); + } else { + if (ch != ' ') { + trailingSpaces = 0; + } + sb << ch; + } + } + + std::string val = sb.str(); + // It's legal to have trailing spaces as long as they are escaped, so if we have some trailing + // escaped spaces, trim the size of the string to the last non-space character + the number of + // escaped trailing spaces. + if (trailingSpaces > 0) { + auto lastNonSpace = val.find_last_not_of(' '); + lastNonSpace += trailingSpaces + 1; + val.erase(lastNonSpace); + } + + // Consume the + or , character + if (!done()) { + _advance(); + } + + switch (ch) { + case '+': + return {std::move(val), MultiValue}; + case ',': + return {std::move(val), NewRDN}; + default: + invariant(done()); + return {std::move(val), Done}; + } +} + +const auto getTLSVersionCounts = ServiceContext::declareDecoration<TLSVersionCounts>(); + +// These represent the ASN.1 type bytes for strings used in an X509 DirectoryString +constexpr int kASN1UTF8String = 12; +constexpr int kASN1PrintableString = 19; +constexpr int kASN1TeletexString = 20; +constexpr int kASN1UniversalString = 28; +constexpr int kASN1BMPString = 30; +constexpr int kASN1OctetString = 4; +} // namespace + +StatusWith<SSLX509Name> parseDN(StringData sd) try { + uassert(ErrorCodes::BadValue, "DN strings must be valid UTF-8 strings", isValidUTF8(sd)); + RFC4514Parser parser(sd); + + std::vector<std::vector<SSLX509Name::Entry>> entries; + auto curRDN = entries.emplace(entries.end()); + while (!parser.done()) { + // Allow spaces to separate RDNs for readability, e.g. "CN=foo, OU=bar, DC=bizz" + parser.skipSpaces(); + auto attributeName = parser.extractAttributeName(); + auto oid = x509ShortNameToOid(attributeName); + uassert(ErrorCodes::BadValue, str::stream() << "DN contained an unknown OID " << oid, oid); + std::string value; + char terminator; + std::tie(value, terminator) = parser.extractValue(); + curRDN->emplace_back(std::move(*oid), kASN1UTF8String, std::move(value)); + if (terminator == RFC4514Parser::NewRDN) { + curRDN = entries.emplace(entries.end()); + } + } + + uassert(ErrorCodes::BadValue, + "Cannot parse empty DN", + entries.size() > 1 || !entries.front().empty()); + + return SSLX509Name(std::move(entries)); +} catch (const DBException& e) { + return e.toStatus(); +} #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 nid = OBJ_txt2nid(name.c_str()); +std::string x509OidToShortName(StringData name) { + const auto nid = OBJ_txt2nid(name.rawData()); if (nid == 0) { - return name; + return name.toString(); } const auto* sn = OBJ_nid2sn(nid); if (!sn) { - return name; + return name.toString(); } return sn; } + +boost::optional<std::string> x509ShortNameToOid(StringData name) { + // Converts the OID to an ASN1_OBJECT + const auto obj = OBJ_txt2obj(name.rawData(), 0); + if (!obj) { + return boost::none; + } + + // OBJ_obj2txt doesn't let you pass in a NULL buffer and a negative size to discover how + // big the buffer should be, but the man page gives 80 as a good guess for buffer size. + constexpr auto kDefaultBufferSize = 80; + std::vector<char> buffer(kDefaultBufferSize); + size_t realSize = OBJ_obj2txt(buffer.data(), buffer.size(), obj, 1); + + // Resize the buffer down or up to the real size. + buffer.resize(realSize); + + // If the real size is greater than the default buffer size we picked, then just call + // OBJ_obj2txt again now that the buffer is correctly sized. + if (realSize > kDefaultBufferSize) { + OBJ_obj2txt(buffer.data(), buffer.size(), obj, 1); + } + + return std::string(buffer.data(), buffer.size()); +} #else // On Apple/Windows we have to provide our own mapping. // Generate the 2.5.4.* portions of this list from OpenSSL sources with: // grep -E '^X509 ' "$OPENSSL/crypto/objects/objects.txt" | tr -d '\t' | // sed -e 's/^X509 *\([0-9]\+\) *\(: *\)\+\([[:alnum:]]\+\).*/{"2.5.4.\1", "\3"},/g' -std::string x509OidToShortName(const std::string& name) { - static const StringMap<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.29.17", "subjectAltName"}, +static const std::initializer_list<std::pair<StringData, StringData>> kX509OidToShortNameMappings = + { + {"0.9.2342.19200300.100.1.1"_sd, "UID"_sd}, + {"0.9.2342.19200300.100.1.25"_sd, "DC"_sd}, + {"1.2.840.113549.1.9.1"_sd, "emailAddress"_sd}, + {"2.5.29.17"_sd, "subjectAltName"_sd}, // X509 OIDs Generated from objects.txt - {"2.5.4.3", "CN"}, - {"2.5.4.4", "SN"}, - {"2.5.4.5", "serialNumber"}, - {"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"}, - {"2.5.4.12", "title"}, - {"2.5.4.13", "description"}, - {"2.5.4.14", "searchGuide"}, - {"2.5.4.15", "businessCategory"}, - {"2.5.4.16", "postalAddress"}, - {"2.5.4.17", "postalCode"}, - {"2.5.4.18", "postOfficeBox"}, - {"2.5.4.19", "physicalDeliveryOfficeName"}, - {"2.5.4.20", "telephoneNumber"}, - {"2.5.4.21", "telexNumber"}, - {"2.5.4.22", "teletexTerminalIdentifier"}, - {"2.5.4.23", "facsimileTelephoneNumber"}, - {"2.5.4.24", "x121Address"}, - {"2.5.4.25", "internationaliSDNNumber"}, - {"2.5.4.26", "registeredAddress"}, - {"2.5.4.27", "destinationIndicator"}, - {"2.5.4.28", "preferredDeliveryMethod"}, - {"2.5.4.29", "presentationAddress"}, - {"2.5.4.30", "supportedApplicationContext"}, - {"2.5.4.31", "member"}, - {"2.5.4.32", "owner"}, - {"2.5.4.33", "roleOccupant"}, - {"2.5.4.34", "seeAlso"}, - {"2.5.4.35", "userPassword"}, - {"2.5.4.36", "userCertificate"}, - {"2.5.4.37", "cACertificate"}, - {"2.5.4.38", "authorityRevocationList"}, - {"2.5.4.39", "certificateRevocationList"}, - {"2.5.4.40", "crossCertificatePair"}, - {"2.5.4.41", "name"}, - {"2.5.4.42", "GN"}, - {"2.5.4.43", "initials"}, - {"2.5.4.44", "generationQualifier"}, - {"2.5.4.45", "x500UniqueIdentifier"}, - {"2.5.4.46", "dnQualifier"}, - {"2.5.4.47", "enhancedSearchGuide"}, - {"2.5.4.48", "protocolInformation"}, - {"2.5.4.49", "distinguishedName"}, - {"2.5.4.50", "uniqueMember"}, - {"2.5.4.51", "houseIdentifier"}, - {"2.5.4.52", "supportedAlgorithms"}, - {"2.5.4.53", "deltaRevocationList"}, - {"2.5.4.54", "dmdName"}, - {"2.5.4.65", "pseudonym"}, - {"2.5.4.72", "role"}, - }; + {"2.5.4.3"_sd, "CN"_sd}, + {"2.5.4.4"_sd, "SN"_sd}, + {"2.5.4.5"_sd, "serialNumber"_sd}, + {"2.5.4.6"_sd, "C"_sd}, + {"2.5.4.7"_sd, "L"_sd}, + {"2.5.4.8"_sd, "ST"_sd}, + {"2.5.4.9"_sd, "street"_sd}, + {"2.5.4.10"_sd, "O"_sd}, + {"2.5.4.11"_sd, "OU"_sd}, + {"2.5.4.12"_sd, "title"_sd}, + {"2.5.4.13"_sd, "description"_sd}, + {"2.5.4.14"_sd, "searchGuide"_sd}, + {"2.5.4.15"_sd, "businessCategory"_sd}, + {"2.5.4.16"_sd, "postalAddress"_sd}, + {"2.5.4.17"_sd, "postalCode"_sd}, + {"2.5.4.18"_sd, "postOfficeBox"_sd}, + {"2.5.4.19"_sd, "physicalDeliveryOfficeName"_sd}, + {"2.5.4.20"_sd, "telephoneNumber"_sd}, + {"2.5.4.21"_sd, "telexNumber"_sd}, + {"2.5.4.22"_sd, "teletexTerminalIdentifier"_sd}, + {"2.5.4.23"_sd, "facsimileTelephoneNumber"_sd}, + {"2.5.4.24"_sd, "x121Address"_sd}, + {"2.5.4.25"_sd, "internationaliSDNNumber"_sd}, + {"2.5.4.26"_sd, "registeredAddress"_sd}, + {"2.5.4.27"_sd, "destinationIndicator"_sd}, + {"2.5.4.28"_sd, "preferredDeliveryMethod"_sd}, + {"2.5.4.29"_sd, "presentationAddress"_sd}, + {"2.5.4.30"_sd, "supportedApplicationContext"_sd}, + {"2.5.4.31"_sd, "member"_sd}, + {"2.5.4.32"_sd, "owner"_sd}, + {"2.5.4.33"_sd, "roleOccupant"_sd}, + {"2.5.4.34"_sd, "seeAlso"_sd}, + {"2.5.4.35"_sd, "userPassword"_sd}, + {"2.5.4.36"_sd, "userCertificate"_sd}, + {"2.5.4.37"_sd, "cACertificate"_sd}, + {"2.5.4.38"_sd, "authorityRevocationList"_sd}, + {"2.5.4.39"_sd, "certificateRevocationList"_sd}, + {"2.5.4.40"_sd, "crossCertificatePair"_sd}, + {"2.5.4.41"_sd, "name"_sd}, + {"2.5.4.42"_sd, "GN"_sd}, + {"2.5.4.43"_sd, "initials"_sd}, + {"2.5.4.44"_sd, "generationQualifier"_sd}, + {"2.5.4.45"_sd, "x500UniqueIdentifier"_sd}, + {"2.5.4.46"_sd, "dnQualifier"_sd}, + {"2.5.4.47"_sd, "enhancedSearchGuide"_sd}, + {"2.5.4.48"_sd, "protocolInformation"_sd}, + {"2.5.4.49"_sd, "distinguishedName"_sd}, + {"2.5.4.50"_sd, "uniqueMember"_sd}, + {"2.5.4.51"_sd, "houseIdentifier"_sd}, + {"2.5.4.52"_sd, "supportedAlgorithms"_sd}, + {"2.5.4.53"_sd, "deltaRevocationList"_sd}, + {"2.5.4.54"_sd, "dmdName"_sd}, + {"2.5.4.65"_sd, "pseudonym"_sd}, + {"2.5.4.72"_sd, "role"_sd}, +}; + +std::string x509OidToShortName(StringData oid) { + auto it = std::find_if( + kX509OidToShortNameMappings.begin(), + kX509OidToShortNameMappings.end(), + [&](const std::pair<StringData, StringData>& entry) { return entry.first == oid; }); - auto it = kX509OidToShortNameMappings.find(name); if (it == kX509OidToShortNameMappings.end()) { - return name; + return oid.toString(); } - return it->second; + return it->second.toString(); } -#endif -const auto getTLSVersionCounts = ServiceContext::declareDecoration<TLSVersionCounts>(); +boost::optional<std::string> x509ShortNameToOid(StringData name) { + auto it = std::find_if( + kX509OidToShortNameMappings.begin(), + kX509OidToShortNameMappings.end(), + [&](const std::pair<StringData, StringData>& entry) { return entry.second == name; }); -} // namespace + if (it == kX509OidToShortNameMappings.end()) { + // If the name is a known oid in our mapping list then just return it. + if (std::find_if(kX509OidToShortNameMappings.begin(), + kX509OidToShortNameMappings.end(), + [&](const auto& entry) { return entry.first == name; }) != + kX509OidToShortNameMappings.end()) { + return name.toString(); + } + return boost::none; + } + return it->first.toString(); +} +#endif TLSVersionCounts& TLSVersionCounts::get(ServiceContext* serviceContext) { return getTLSVersionCounts(serviceContext); @@ -161,8 +472,8 @@ MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManagerLogger, ("SSLManager", "GlobalLog if (!config.clientSubjectName.empty()) { LOG(1) << "Client Certificate Name: " << config.clientSubjectName; } - if (!config.serverSubjectName.empty()) { - LOG(1) << "Server Certificate Name: " << config.serverSubjectName; + if (!config.serverSubjectName().empty()) { + LOG(1) << "Server Certificate Name: " << config.serverSubjectName(); LOG(1) << "Server Certificate Expiration: " << config.serverCertificateExpirationDate; } } @@ -170,6 +481,32 @@ MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManagerLogger, ("SSLManager", "GlobalLog return Status::OK(); } +Status SSLX509Name::normalizeStrings() { + for (auto& rdn : _entries) { + for (auto& entry : rdn) { + switch (entry.type) { + // For each type of valid DirectoryString, do the string prep algorithm. + case kASN1UTF8String: + case kASN1PrintableString: + case kASN1TeletexString: + case kASN1UniversalString: + case kASN1BMPString: + case kASN1OctetString: { + auto res = icuX509DNPrep(entry.value); + if (!res.isOK()) { + return res.getStatus(); + } + entry.value = std::move(res.getValue()); + entry.type = kASN1UTF8String; + break; + } + } + } + } + + return Status::OK(); +} + StatusWith<std::string> SSLX509Name::getOID(StringData oid) const { for (const auto& rdn : _entries) { for (const auto& entry : rdn) { @@ -238,40 +575,58 @@ std::vector<SSLX509Name::Entry> canonicalizeClusterDN( } } // namespace +Status SSLConfiguration::setServerSubjectName(SSLX509Name name) { + auto status = name.normalizeStrings(); + if (!status.isOK()) { + return status; + } + _serverSubjectName = std::move(name); + _canonicalServerSubjectName = canonicalizeClusterDN(_serverSubjectName.entries()); + return Status::OK(); +} + /** * 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. + * names in their normalized, unescaped forms and provides a more reliable match. * - * 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. + * The StringData version attempts to canonicalize the stringified subject name + * according to RFC4514 and compare that to the normalized/unescaped version of + * the server's distinguished name. */ -bool SSLConfiguration::isClusterMember(const SSLX509Name& subject) const { - auto client = canonicalizeClusterDN(subject._entries); - auto server = canonicalizeClusterDN(serverSubjectName._entries); +bool SSLConfiguration::isClusterMember(SSLX509Name subject) const { + if (!subject.normalizeStrings().isOK()) { + return false; + } + + auto client = canonicalizeClusterDN(subject.entries()); - return !client.empty() && (client == server); + return !client.empty() && (client == _canonicalServerSubjectName); } bool SSLConfiguration::isClusterMember(StringData subjectName) const { - std::vector<std::string> clientRDN = StringSplitter::split(subjectName.toString(), ","); - std::vector<std::string> serverRDN = StringSplitter::split(serverSubjectName.toString(), ","); + auto swClient = parseDN(subjectName); + if (!swClient.isOK()) { + warning() << "Unable to parse client subject name: " << swClient.getStatus(); + return false; + } + auto& client = swClient.getValue(); + auto status = client.normalizeStrings(); + if (!status.isOK()) { + warning() << "Unable to normalize client subject name: " << status; + return false; + } - canonicalizeClusterDN(&clientRDN); - canonicalizeClusterDN(&serverRDN); + auto canonicalClient = canonicalizeClusterDN(client.entries()); - return !clientRDN.empty() && (clientRDN == serverRDN); + return !canonicalClient.empty() && (canonicalClient == _canonicalServerSubjectName); } BSONObj SSLConfiguration::getServerStatusBSON() const { BSONObjBuilder security; - security.append("SSLServerSubjectName", serverSubjectName.toString()); + 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 d93728aa466..fa2abc656a5 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -101,14 +101,24 @@ public: virtual std::string getSNIServerName() const = 0; }; -struct SSLConfiguration { +class SSLConfiguration { +public: bool isClusterMember(StringData subjectName) const; - bool isClusterMember(const SSLX509Name& subjectName) const; + bool isClusterMember(SSLX509Name subjectName) const; BSONObj getServerStatusBSON() const; - SSLX509Name serverSubjectName; + Status setServerSubjectName(SSLX509Name name); + + const SSLX509Name& serverSubjectName() const { + return _serverSubjectName; + } + SSLX509Name clientSubjectName; Date_t serverCertificateExpirationDate; bool hasCA = false; + +private: + SSLX509Name _serverSubjectName; + std::vector<SSLX509Name::Entry> _canonicalServerSubjectName; }; /** @@ -244,13 +254,26 @@ StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(ConstDataRange cdrExten std::string removeFQDNRoot(std::string name); /** - * Escape a string per RGC 2253 + * Escape a string per RFC 2253 * * See "2.4 Converting an AttributeValue from ASN.1 to a String" in RFC 2243 */ std::string escapeRfc2253(StringData str); /** + * Parse a DN from a string per RFC 4514 + */ +StatusWith<SSLX509Name> parseDN(StringData str); + +/** + * These functions map short names for RDN components to numeric OID's and the other way around. + * + * The x509ShortNameToOid returns boost::none if no mapping exists for that oid. + */ +std::string x509OidToShortName(StringData name); +boost::optional<std::string> x509ShortNameToOid(StringData name); + +/** * Platform neutral TLS version enum */ enum class TLSVersion { diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp index 23c03910983..30de09c63c1 100644 --- a/src/mongo/util/net/ssl_manager_apple.cpp +++ b/src/mongo/util/net/ssl_manager_apple.cpp @@ -1153,8 +1153,9 @@ SSLManagerApple::SSLManagerApple(const SSLParams& params, bool isServer) if (isServer) { uassertStatusOK(initSSLContext(&_serverCtx, params, ConnectionDirection::kIncoming)); if (_serverCtx.certs) { - _sslConfiguration.serverSubjectName = uassertStatusOK(certificateGetSubject( - _serverCtx.certs.get(), &_sslConfiguration.serverCertificateExpirationDate)); + uassertStatusOK( + _sslConfiguration.setServerSubjectName(uassertStatusOK(certificateGetSubject( + _serverCtx.certs.get(), &_sslConfiguration.serverCertificateExpirationDate)))); static auto task = CertificateExpirationMonitor(_sslConfiguration.serverCertificateExpirationDate); } @@ -1558,7 +1559,7 @@ std::unique_ptr<SSLManagerInterface> SSLManagerInterface::create(const SSLParams return stdx::make_unique<SSLManagerApple>(params, isServer); } -MONGO_INITIALIZER(SSLManager)(InitializerContext*) { +MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManager, ("LoadICUData"))(InitializerContext*) { kMongoDBRolesOID = ::CFStringCreateWithCString( nullptr, mongodbRolesOID.identifier.c_str(), ::kCFStringEncodingUTF8); diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index 52f4a37e126..11a0ebc4dcb 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -662,7 +662,8 @@ MONGO_INITIALIZER(SetupOpenSSL)(InitializerContext*) { return Status::OK(); } -MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManager, ("SetupOpenSSL"))(InitializerContext*) { +MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManager, ("SetupOpenSSL", "LoadICUData")) +(InitializerContext*) { stdx::lock_guard<SimpleMutex> lck(sslManagerMtx); if (!isSSLServer || (sslGlobalParams.sslMode.load() != SSLParams::SSLMode_disabled)) { theSSLManager = new SSLManagerOpenSSL(sslGlobalParams, isSSLServer); @@ -846,13 +847,16 @@ SSLManagerOpenSSL::SSLManagerOpenSSL(const SSLParams& params, bool isServer) uasserted(16562, "ssl initialization problem"); } + SSLX509Name serverSubjectName; if (!_parseAndValidateCertificate(params.sslPEMKeyFile, &_serverPEMPassword, - &_sslConfiguration.serverSubjectName, + &serverSubjectName, &_sslConfiguration.serverCertificateExpirationDate)) { uasserted(16942, "ssl initialization problem"); } + uassertStatusOK(_sslConfiguration.setServerSubjectName(std::move(serverSubjectName))); + static CertificateExpirationMonitor task = CertificateExpirationMonitor(_sslConfiguration.serverCertificateExpirationDate); } diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp index 3abc6fc72da..1c7d866815a 100644 --- a/src/mongo/util/net/ssl_manager_test.cpp +++ b/src/mongo/util/net/ssl_manager_test.cpp @@ -46,7 +46,6 @@ namespace mongo { namespace { TEST(SSLManager, matchHostname) { -#ifdef MONGO_CONFIG_SSL enum Expected : bool { match = true, mismatch = false }; const struct { Expected expected; @@ -87,12 +86,8 @@ TEST(SSLManager, matchHostname) { } } ASSERT_FALSE(failure); -#endif -} } -#ifdef MONGO_CONFIG_SSL - std::vector<RoleName> getSortedRoles(const stdx::unordered_set<RoleName>& roles) { std::vector<RoleName> vec; vec.reserve(roles.size()); @@ -273,7 +268,115 @@ TEST(SSLManager, DHCheckRFC7919) { } #endif -#endif +struct FlattenedX509Name { + using EntryVector = std::vector<std::pair<std::string, std::string>>; + + FlattenedX509Name(std::initializer_list<EntryVector::value_type> forVector) + : value(forVector) {} + + FlattenedX509Name() = default; + + void addPair(std::string oid, std::string val) { + value.emplace_back(std::move(oid), std::move(val)); + } + + std::string toString() const { + bool first = true; + StringBuilder sb; + for (const auto& entry : value) { + sb << (first ? "\"" : ",\"") << entry.first << "\"=\"" << entry.second << "\""; + first = false; + } + + return sb.str(); + } + + EntryVector value; + + bool operator==(const FlattenedX509Name& other) const { + return value == other.value; + } +}; + +std::ostream& operator<<(std::ostream& o, const FlattenedX509Name& name) { + o << name.toString(); + return o; +} + +FlattenedX509Name flattenX509Name(const SSLX509Name& name) { + FlattenedX509Name ret; + for (const auto& entry : name.entries()) { + for (const auto& rdn : entry) { + ret.addPair(rdn.oid, rdn.value); + } + } + + return ret; +} + +TEST(SSLManager, DNParsingAndNormalization) { + std::vector<std::pair<std::string, FlattenedX509Name>> tests = { + // Basic DN parsing + {"UID=jsmith,DC=example,DC=net", + {{"0.9.2342.19200300.100.1.1", "jsmith"}, + {"0.9.2342.19200300.100.1.25", "example"}, + {"0.9.2342.19200300.100.1.25", "net"}}}, + {"OU=Sales+CN=J. Smith,DC=example,DC=net", + {{"2.5.4.11", "Sales"}, + {"2.5.4.3", "J. Smith"}, + {"0.9.2342.19200300.100.1.25", "example"}, + {"0.9.2342.19200300.100.1.25", "net"}}}, + {R"(CN=James \"Jim\" Smith\, III,DC=example,DC=net)", + {{"2.5.4.3", R"(James "Jim" Smith, III)"}, + {"0.9.2342.19200300.100.1.25", "example"}, + {"0.9.2342.19200300.100.1.25", "net"}}}, + // Per RFC4518, control sequences are mapped to nothing and whitepace is mapped to ' ' + {"CN=Before\\0aAfter,O=tabs\tare\tspaces\u200B,DC=\\07\\08example,DC=net", + {{"2.5.4.3", "Before After"}, + {"2.5.4.10", "tabs are spaces"}, + {"0.9.2342.19200300.100.1.25", "example"}, + {"0.9.2342.19200300.100.1.25", "net"}}}, + // Check that you can't fake a cluster dn with poor comma escaping + {R"(CN=evil\,OU\=Kernel,O=MongoDB Inc.,L=New York City,ST=New York,C=US)", + {{"2.5.4.3", "evil,OU=Kernel"}, + {"2.5.4.10", "MongoDB Inc."}, + {"2.5.4.7", "New York City"}, + {"2.5.4.8", "New York"}, + {"2.5.4.6", "US"}}}, + // check space handling (must be escaped at the beginning and end of strings) + {R"(CN= \ escaped spaces\20\ )", {{"2.5.4.3", " escaped spaces "}}}, + {"CN=server, O=MongoDB Inc.", {{"2.5.4.3", "server"}, {"2.5.4.10", "MongoDB Inc."}}}, + // Check that escaped #'s work correctly at the beginning of the string and throughout. + {R"(CN=\#1 = \\#1)", {{"2.5.4.3", "#1 = \\#1"}}}, + {R"(CN== \#1)", {{"2.5.4.3", "= #1"}}}, + // check that escaped utf8 string properly parse to utf8 + {R"(CN=Lu\C4\8Di\C4\87)", {{"2.5.4.3", "Lučić"}}}, + // check that unescaped utf8 strings round trip correctly + {"CN = Калоян, O=مُنظّمة الدُّول المُصدِّرة للنّفْط, L=大田区\\, 東京都", + {{"2.5.4.3", "Калоян"}, + {"2.5.4.10", "مُنظّمة الدُّول المُصدِّرة للنّفْط"}, + {"2.5.4.7", "大田区, 東京都"}}}}; + + for (const auto& test : tests) { + log() << "Testing DN \"" << test.first << "\""; + auto swDN = parseDN(test.first); + ASSERT_OK(swDN.getStatus()); + ASSERT_OK(swDN.getValue().normalizeStrings()); + auto decoded = flattenX509Name(swDN.getValue()); + ASSERT_EQ(decoded, test.second); + } +} + +TEST(SSLManager, BadDNParsing) { + std::vector<std::string> tests = {"CN=#12345", + R"(CN=\B)", + R"(CN=<", "\)"}; + for (const auto& test : tests) { + log() << "Testing bad DN: \"" << test << "\""; + auto swDN = parseDN(test); + ASSERT_NOT_OK(swDN.getStatus()); + } +} -// // namespace +} // namespace } // namespace mongo diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp index 90eb6de52d0..7feee26e213 100644 --- a/src/mongo/util/net/ssl_manager_windows.cpp +++ b/src/mongo/util/net/ssl_manager_windows.cpp @@ -344,7 +344,7 @@ private: UniqueCertificate _sslClusterCertificate; }; -MONGO_INITIALIZER(SSLManager)(InitializerContext*) { +MONGO_INITIALIZER_WITH_PREREQUISITES(SSLManager, ("LoadICUData"))(InitializerContext*) { stdx::lock_guard<SimpleMutex> lck(sslManagerMtx); if (!isSSLServer || (sslGlobalParams.sslMode.load() != SSLParams::SSLMode_disabled)) { theSSLManager = new SSLManagerWindows(sslGlobalParams, isSSLServer); @@ -417,10 +417,12 @@ SSLManagerWindows::SSLManagerWindows(const SSLParams& params, bool isServer) uassertStatusOK(initSSLContext(&_serverCred, params, ConnectionDirection::kIncoming)); if (_serverCertificates[0] != nullptr) { + SSLX509Name subjectName; uassertStatusOK( _validateCertificate(_serverCertificates[0], - &_sslConfiguration.serverSubjectName, + &subjectName, &_sslConfiguration.serverCertificateExpirationDate)); + uassertStatusOK(_sslConfiguration.setServerSubjectName(std::move(subjectName))); } // Monitor the server certificate's expiration diff --git a/src/mongo/util/net/ssl_types.h b/src/mongo/util/net/ssl_types.h index 76b15e952f1..7bcca37700e 100644 --- a/src/mongo/util/net/ssl_types.h +++ b/src/mongo/util/net/ssl_types.h @@ -87,8 +87,19 @@ public: return !(lhs._entries == rhs._entries); } + const std::vector<std::vector<Entry>>& entries() const { + return _entries; + } + + /* + * This will go through every entry, verify that it's type is a valid DirectoryString + * according to https://tools.ietf.org/html/rfc5280#section-4.1.2.4, and perform + * the RFC 4518 string prep algorithm on it to normalize the values so they can be + * directly compared. After this, all entries should have the type 12 (utf8String). + */ + Status normalizeStrings(); + private: - friend struct SSLConfiguration; std::vector<std::vector<Entry>> _entries; }; diff --git a/src/mongo/util/text.cpp b/src/mongo/util/text.cpp index e522ac5aeab..4d10fc88f83 100644 --- a/src/mongo/util/text.cpp +++ b/src/mongo/util/text.cpp @@ -121,14 +121,9 @@ inline int leadingOnes(unsigned char c) { return _leadingOnes[c & 0x7f]; } -bool isValidUTF8(const std::string& s) { - return isValidUTF8(s.c_str()); -} - -bool isValidUTF8(const char* s) { +bool isValidUTF8(StringData s) { int left = 0; // how many bytes are left in the current codepoint - while (*s) { - const unsigned char c = (unsigned char)*(s++); + for (unsigned char c : s) { const int ones = leadingOnes(c); if (left) { if (ones != 1) diff --git a/src/mongo/util/text.h b/src/mongo/util/text.h index 3f747ff1751..94979610b34 100644 --- a/src/mongo/util/text.h +++ b/src/mongo/util/text.h @@ -36,6 +36,7 @@ #include <vector> #include "mongo/base/disallow_copying.h" +#include "mongo/base/string_data.h" #include "mongo/config.h" namespace mongo { @@ -72,8 +73,7 @@ private: * std::string can be converted to sequence of codepoints. However, it doesn't * guarantee that the codepoints are valid. */ -bool isValidUTF8(const char* s); -bool isValidUTF8(const std::string& s); +bool isValidUTF8(StringData s); #if defined(_WIN32) diff --git a/src/third_party/icu4c-57.1/source/mongo_sources/icudt57b.dat b/src/third_party/icu4c-57.1/source/mongo_sources/icudt57b.dat Binary files differindex 59eab7860da..fbd961fba72 100644 --- a/src/third_party/icu4c-57.1/source/mongo_sources/icudt57b.dat +++ b/src/third_party/icu4c-57.1/source/mongo_sources/icudt57b.dat diff --git a/src/third_party/icu4c-57.1/source/mongo_sources/icudt57l.dat b/src/third_party/icu4c-57.1/source/mongo_sources/icudt57l.dat Binary files differindex d29853e2481..b088f9b9c7b 100644 --- a/src/third_party/icu4c-57.1/source/mongo_sources/icudt57l.dat +++ b/src/third_party/icu4c-57.1/source/mongo_sources/icudt57l.dat diff --git a/src/third_party/scripts/icu_get_sources.sh b/src/third_party/scripts/icu_get_sources.sh index 71c102b0370..df06b11a421 100755 --- a/src/third_party/scripts/icu_get_sources.sh +++ b/src/third_party/scripts/icu_get_sources.sh @@ -11,7 +11,16 @@ # # The script accepts a single optional argument, which is the path to the .dat archive to use as the # source of the trimmed-down ICU .dat files generated as output. If omitted, the .dat archive which -# is included in the ICU source code is used by default. +# is included in the ICU source code is used by default. The included .dat file included with some +# versions of ICU does not contain all the collations needed by mongodb. If this script fails while +# checking the data list for collations, it must be invoked with the path to our custom .dat +# archive: +# ./src/third_party/scripts/icu_get_sources.sh \ +# $(pwd)/src/third_party/icu4c-57.1/source/mongo_sources/icudt57l.dat +# +# That archive can be generated by the ICU tool here: http://apps.icu-project.org/datacustom/, you +# should check all the boxes and download the zipfile for the major version of ICU. This script +# will strip out anything that isn't needed. # # This script returns a zero exit code on success. @@ -99,11 +108,13 @@ BASE_FILES="root.res ucadata.icu" for DESIRED_DATA_DIRECTORY in $DESIRED_DATA_DIRECTORIES; do for BASE_FILE in $BASE_FILES; do + echo "Checking $ORIGINAL_DATA_LIST for $DESIRED_DATA_DIRECTORY/$BASE_FILE" # Using grep to sanity-check that the file indeed appears in the original data list. grep -E "^${DESIRED_DATA_DIRECTORY}/${BASE_FILE}$" "$ORIGINAL_DATA_LIST" >> "$NEW_DATA_LIST" done for LANGUAGE in $(grep -Ev "^#" "$LANGUAGE_FILE_IN"); do # Ditto above. + echo "Checking $ORIGINAL_DATA_LIST for $DESIRED_DATA_DIRECTORY/$LANGUAGE" grep -E "^${DESIRED_DATA_DIRECTORY}/${LANGUAGE}.res$" "$ORIGINAL_DATA_LIST" \ >> "$NEW_DATA_LIST" done @@ -111,6 +122,7 @@ done # UStringPrepProfile: USPREP_RFC4013_SASLPREP and NFKC normalization. grep -E "^rfc4013.spp$" "$ORIGINAL_DATA_LIST" >> "$NEW_DATA_LIST" +grep -E "^rfc4518.spp$" "$ORIGINAL_DATA_LIST" >> "$NEW_DATA_LIST" grep -E "^nfkc.nrm$" "$ORIGINAL_DATA_LIST" >> "$NEW_DATA_LIST" # |