diff options
-rw-r--r-- | src/mongo/util/net/ssl_manager.cpp | 309 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager.h | 6 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_test.cpp | 166 |
3 files changed, 480 insertions, 1 deletions
diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index 26af7f699ec..82207251d6e 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -36,6 +36,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/config.h" #include "mongo/db/server_parameters.h" +#include "mongo/platform/overflow_arithmetic.h" #include "mongo/transport/session.h" #include "mongo/util/mongoutils/str.h" #include "mongo/util/net/ssl_options.h" @@ -151,6 +152,314 @@ SSLManagerInterface::~SSLManagerInterface() {} SSLConnectionInterface::~SSLConnectionInterface() {} +namespace { + +/** + * Enum of supported Abstract Syntax Notation One (ASN.1) Distinguished Encoding Rules (DER) types. + * + * This is a subset of all DER types. + */ +enum class DERType : char { + // Primitive, not supported by the parser + // Only exists when BER indefinite form is used which is not valid DER. + EndOfContent = 0, + + // Primitive + UTF8String = 12, + + // Sequence or Sequence Of, Constructed + SEQUENCE = 16, + + // Set or Set Of, Constructed + SET = 17, +}; + +/** + * Distinguished Encoding Rules (DER) are a strict subset of Basic Encoding Rules (BER). + * + * For more details, see X.690 from ITU-T. + * + * It is a Tag + Length + Value format. The tag is generally 1 byte, the length is 1 or more + * and then followed by the value. + */ +class DERToken { +public: + DERToken() {} + DERToken(DERType type, size_t length, const char* const data) + : _type(type), _length(length), _data(data) {} + + /** + * Get the ASN.1 type of the current token. + */ + DERType getType() const { + return _type; + } + + /** + * Get a ConstDataRange for the value of this SET or SET OF. + */ + ConstDataRange getSetRange() { + invariant(_type == DERType::SET); + return ConstDataRange(_data, _data + _length); + } + + /** + * Get a ConstDataRange for the value of this SEQUENCE or SEQUENCE OF. + */ + ConstDataRange getSequenceRange() { + invariant(_type == DERType::SEQUENCE); + return ConstDataRange(_data, _data + _length); + } + + /** + * Get a std::string for the value of this Utf8String. + */ + std::string readUtf8String() { + invariant(_type == DERType::UTF8String); + return std::string(_data, _length); + } + + /** + * Parse a buffer of bytes and return the number of bytes we read for this token. + * + * Returns a DERToken which consists of the (tag, length, value) tuple. + */ + static StatusWith<DERToken> parse(ConstDataRange cdr, size_t* outLength); + +private: + DERType _type{DERType::EndOfContent}; + size_t _length{0}; + const char* _data{nullptr}; +}; + +} // namespace + +template <> +struct DataType::Handler<DERToken> { + static Status load(DERToken* t, + const char* ptr, + size_t length, + size_t* advanced, + std::ptrdiff_t debug_offset) { + size_t outLength; + + auto swPair = DERToken::parse(ConstDataRange(ptr, length), &outLength); + + if (!swPair.isOK()) { + return swPair.getStatus(); + } + + if (t) { + *t = std::move(swPair.getValue()); + } + + if (advanced) { + *advanced = outLength; + } + + return Status::OK(); + } + + static DERToken defaultConstruct() { + return DERToken(); + } +}; + +namespace { + +StatusWith<std::string> readDERString(ConstDataRangeCursor& cdc) { + auto swString = cdc.readAndAdvance<DERToken>(); + if (!swString.isOK()) { + return swString.getStatus(); + } + + auto derString = swString.getValue(); + + if (derString.getType() != DERType::UTF8String) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Unexpected DER Tag, Got " + << static_cast<char>(derString.getType()) + << ", Expected UTF8String"); + } + + return derString.readUtf8String(); +} + + +StatusWith<DERToken> DERToken::parse(ConstDataRange cdr, size_t* outLength) { + const size_t kTagLength = 1; + const size_t kTagLengthAndInitialLengthByteLength = kTagLength + 1; + + ConstDataRangeCursor cdrc(cdr); + + auto swTagByte = cdrc.readAndAdvance<char>(); + if (!swTagByte.getStatus().isOK()) { + return swTagByte.getStatus(); + } + + const char tagByte = swTagByte.getValue(); + + // Get the tag number from the first 5 bits + const char tag = tagByte & 0x1f; + + // Check the 6th bit + const bool constructed = tagByte & 0x20; + const bool primitive = !constructed; + + // Check bits 7 and 8 for the tag class, we only want Universal (i.e. 0) + const char tagClass = tagByte & 0xC0; + if (tagClass != 0) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Unsupported tag class"); + } + + // Validate the 6th bit is correct, and it is a known type + switch (static_cast<DERType>(tag)) { + case DERType::UTF8String: + if (!primitive) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Unknown DER tag"); + } + break; + case DERType::SEQUENCE: + case DERType::SET: + if (!constructed) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Unknown DER tag"); + } + break; + default: + return Status(ErrorCodes::InvalidSSLConfiguration, "Unknown DER tag"); + } + + // Do we have at least 1 byte for the length + if (cdrc.length() < kTagLengthAndInitialLengthByteLength) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Invalid DER length"); + } + + // Read length + // Depending on the high bit, either read 1 byte or N bytes + auto swInitialLengthByte = cdrc.readAndAdvance<char>(); + if (!swInitialLengthByte.getStatus().isOK()) { + return swInitialLengthByte.getStatus(); + } + + const char initialLengthByte = swInitialLengthByte.getValue(); + + + uint64_t derLength = 0; + + // How many bytes does it take to encode the length? + size_t encodedLengthBytesCount = 1; + + if (initialLengthByte & 0x80) { + // Length is > 127 bytes, i.e. Long form of length + const size_t lengthBytesCount = 0x7f & initialLengthByte; + + // If length is encoded in more then 8 bytes, we disallow it + if (lengthBytesCount > 8) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Invalid DER length"); + } + + // Ensure we have enough data for the length bytes + const char* lengthLongFormPtr = cdrc.data(); + + Status statusLength = cdrc.advance(lengthBytesCount); + if (!statusLength.isOK()) { + return statusLength; + } + + encodedLengthBytesCount = 1 + lengthBytesCount; + + std::array<char, 8> lengthBuffer; + lengthBuffer.fill(0); + + // Copy the length into the end of the buffer + memcpy(lengthBuffer.data() + (8 - lengthBytesCount), lengthLongFormPtr, lengthBytesCount); + + // We now have 0x00..NN in the buffer and it can be properly decoded as BigEndian + derLength = ConstDataView(lengthBuffer.data()).read<BigEndian<uint64_t>>(); + } else { + // Length is <= 127 bytes, i.e. short form of length + derLength = initialLengthByte; + } + + // This is the total length of the TLV and all data + // This will not overflow since encodedLengthBytesCount <= 9 + const uint64_t tagAndLengthByteCount = kTagLength + encodedLengthBytesCount; + + // This may overflow since derLength is from user data so check our arithmetic carefully. + if (mongoUnsignedAddOverflow64(tagAndLengthByteCount, derLength, outLength) || + *outLength > cdr.length()) { + return Status(ErrorCodes::InvalidSSLConfiguration, "Invalid DER length"); + } + + return DERToken(static_cast<DERType>(tag), derLength, cdr.data() + tagAndLengthByteCount); +} +} // namespace + +StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(ConstDataRange cdrExtension) { + stdx::unordered_set<RoleName> roles; + + ConstDataRangeCursor cdcExtension(cdrExtension); + + /** + * MongoDBAuthorizationGrants ::= SET OF MongoDBAuthorizationGrant + * + * MongoDBAuthorizationGrant ::= CHOICE { + * MongoDBRole, + * ...!UTF8String:"Unrecognized entity in MongoDBAuthorizationGrant" + * } + */ + auto swSet = cdcExtension.readAndAdvance<DERToken>(); + if (!swSet.isOK()) { + return swSet.getStatus(); + } + + if (swSet.getValue().getType() != DERType::SET) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Unexpected DER Tag, Got " + << static_cast<char>(swSet.getValue().getType()) + << ", Expected SET"); + } + + ConstDataRangeCursor cdcSet(swSet.getValue().getSetRange()); + + while (!cdcSet.empty()) { + /** + * MongoDBRole ::= SEQUENCE { + * role UTF8String, + * database UTF8String + * } + */ + auto swSequence = cdcSet.readAndAdvance<DERToken>(); + if (!swSequence.isOK()) { + return swSequence.getStatus(); + } + + auto sequenceStart = swSequence.getValue(); + + if (sequenceStart.getType() != DERType::SEQUENCE) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Unexpected DER Tag, Got " + << static_cast<char>(sequenceStart.getType()) + << ", Expected SEQUENCE"); + } + + ConstDataRangeCursor cdcSequence(sequenceStart.getSequenceRange()); + + auto swRole = readDERString(cdcSequence); + if (!swRole.isOK()) { + return swRole.getStatus(); + } + + auto swDatabase = readDERString(cdcSequence); + if (!swDatabase.isOK()) { + return swDatabase.getStatus(); + } + + roles.emplace(swRole.getValue(), swDatabase.getValue()); + } + + return roles; +} #endif } // namespace mongo diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index bc640dc4b88..9e0edccb649 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -211,5 +211,11 @@ const SSLParams& getSSLGlobalParams(); * x.509 certificate. Matches a remote host name to an x.509 host name, including wildcards. */ bool hostNameMatchForX509Certificates(std::string nameToMatch, std::string certHostName); + +/** + * Parse a binary blob of DER encoded ASN.1 into a set of RoleNames. + */ +StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(ConstDataRange cdrExtension); + } // namespace mongo #endif // #ifdef MONGO_CONFIG_SSL diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp index cb3140a1c0f..c2b0101637d 100644 --- a/src/mongo/util/net/ssl_manager_test.cpp +++ b/src/mongo/util/net/ssl_manager_test.cpp @@ -83,5 +83,169 @@ TEST(SSLManager, matchHostname) { ASSERT_FALSE(failure); #endif } -} // namespace +} + +#ifdef MONGO_CONFIG_SSL + +std::vector<RoleName> getSortedRoles(const stdx::unordered_set<RoleName>& roles) { + std::vector<RoleName> vec; + vec.reserve(roles.size()); + std::copy(roles.begin(), roles.end(), std::back_inserter<std::vector<RoleName>>(vec)); + std::sort(vec.begin(), vec.end()); + return vec; +} + +TEST(SSLManager, MongoDBRolesParser) { + /* + openssl asn1parse -genconf mongodbroles.cnf -out foo.der + + -------- mongodbroles.cnf -------- + asn1 = SET:MongoDBAuthorizationGrant + + [MongoDBAuthorizationGrant] + grant1 = SEQUENCE:MongoDBRole + + [MongoDBRole] + role = UTF8:role_name + database = UTF8:Third field + */ + // Positive: Simple parsing test + { + unsigned char derData[] = {0x31, 0x1a, 0x30, 0x18, 0x0c, 0x09, 0x72, 0x6f, 0x6c, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x0c, 0x0b, 0x54, 0x68, 0x69, + 0x72, 0x64, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_OK(swPeer.getStatus()); + auto item = *(swPeer.getValue().begin()); + ASSERT_EQ(item.getRole(), "role_name"); + ASSERT_EQ(item.getDB(), "Third field"); + } + + // Positive: Very long role_name, and long form lengths + { + unsigned char derData[] = { + 0x31, 0x82, 0x01, 0x3e, 0x30, 0x82, 0x01, 0x3a, 0x0c, 0x82, 0x01, 0x29, 0x72, 0x6f, + 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, + 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, + 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, + 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, + 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x0c, 0x0b, 0x54, 0x68, 0x69, 0x72, 0x64, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_OK(swPeer.getStatus()); + + auto item = *(swPeer.getValue().begin()); + ASSERT_EQ(item.getRole(), + "role_namerole_namerole_namerole_namerole_namerole_namerole_namerole_namerole_" + "namerole_namerole_namerole_namerole_namerole_namerole_namerole_namerole_" + "namerole_namerole_namerole_namerole_namerole_namerole_namerole_namerole_" + "namerole_namerole_namerole_namerole_namerole_namerole_namerole_namerole_name"); + ASSERT_EQ(item.getDB(), "Third field"); + } + + // Negative: Encode MAX_INT64 into a length + { + unsigned char derData[] = {0x31, 0x88, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x3e, 0x18, 0x0c, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x0c, 0x0b, 0x54, 0x68, 0x69, 0x72, + 0x64, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64}; + + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Negative: Runt, only a tag + { + unsigned char derData[] = {0x31}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Negative: Runt, only a tag and short length + { + unsigned char derData[] = {0x31, 0x0b}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Negative: Runt, only a tag and long length with wrong missing length + { + unsigned char derData[] = { + 0x31, 0x88, 0xff, 0xff, + }; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Negative: Runt, only a tag and long length + { + unsigned char derData[] = { + 0x31, 0x82, 0xff, 0xff, + }; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Negative: Single UTF8 String + { + unsigned char derData[] = { + 0x0c, 0x0b, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Negative: Unknown type - IAString + { + unsigned char derData[] = { + 0x16, 0x0b, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_NOT_OK(swPeer.getStatus()); + } + + // Positive: two roles + { + unsigned char derData[] = {0x31, 0x2b, 0x30, 0x0f, 0x0c, 0x06, 0x62, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x0c, 0x05, 0x61, 0x64, 0x6d, 0x69, + 0x6e, 0x30, 0x18, 0x0c, 0x0f, 0x72, 0x65, 0x61, 0x64, + 0x41, 0x6e, 0x79, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, + 0x73, 0x65, 0x0c, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e}; + auto swPeer = parsePeerRoles(ConstDataRange(reinterpret_cast<char*>(derData), + std::extent<decltype(derData)>::value)); + ASSERT_OK(swPeer.getStatus()); + + auto roles = getSortedRoles(swPeer.getValue()); + ASSERT_EQ(roles[0].getRole(), "backup"); + ASSERT_EQ(roles[0].getDB(), "admin"); + ASSERT_EQ(roles[1].getRole(), "readAnyDatabase"); + ASSERT_EQ(roles[1].getDB(), "admin"); + } +} +#endif + +// // namespace } // namespace mongo |