diff options
-rw-r--r-- | jstests/ssl/tlsCATrusts.js | 189 | ||||
-rw-r--r-- | jstests/ssl/x509/certs.yml | 10 | ||||
-rw-r--r-- | jstests/ssl/x509/trusted-client-testdb-roles.pem | 55 | ||||
-rw-r--r-- | src/mongo/crypto/hash_block.h | 27 | ||||
-rw-r--r-- | src/mongo/crypto/sha1_block.idl | 7 | ||||
-rw-r--r-- | src/mongo/crypto/sha256_block.idl | 7 | ||||
-rw-r--r-- | src/mongo/crypto/sha512_block.idl | 7 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_apple.cpp | 2 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_openssl.cpp | 65 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_manager_windows.cpp | 2 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_options.h | 9 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_parameters.cpp | 92 | ||||
-rw-r--r-- | src/mongo/util/net/ssl_parameters.idl | 24 |
13 files changed, 496 insertions, 0 deletions
diff --git a/jstests/ssl/tlsCATrusts.js b/jstests/ssl/tlsCATrusts.js new file mode 100644 index 00000000000..2d6f08064bf --- /dev/null +++ b/jstests/ssl/tlsCATrusts.js @@ -0,0 +1,189 @@ +// Test restricting role authorization via X509 extensions. +load('jstests/ssl/libs/ssl_helpers.js'); + +requireSSLProvider('openssl', function() { + "use strict"; + + const SERVER_CERT = 'jstests/libs/server.pem'; + const COMBINED_CA_CERT = 'jstests/ssl/x509/root-and-trusted-ca.pem'; + const CA_HASH = '539D91F8202641BF85C0C36C88FF69F3062D4AB370CECBF9B950A8B97DE72EAE'; + const TRUSTED_CA_HASH = 'AEAEBB1BA947A7C1428D39EF6166B83409D0245D28013C9FDD71DF9E69BEA52B'; + + // Common suffix, keep the lines short. + const RDN_SUFFIX = ',O=MongoDB,L=New York City,ST=New York,C=US'; + const USERS = []; + + const CLIENT = { + cert: 'jstests/libs/client.pem', + roles: [], + }; + USERS.push('CN=client,OU=KernelUser'); + + const CLIENT_ROLES = { + cert: 'jstests/libs/client_roles.pem', + roles: [{role: 'backup', db: 'admin'}, {role: 'readAnyDatabase', db: 'admin'}], + }; + USERS.push('CN=Kernel Client Peer Role,OU=Kernel Users'); + + const TRUSTED_CLIENT_TESTDB_ROLES = { + cert: 'jstests/ssl/x509/trusted-client-testdb-roles.pem', + roles: [{role: 'role1', db: 'testDB'}, {role: 'role2', db: 'testDB'}], + }; + USERS.push('CN=Trusted Kernel Test Client With Roles,OU=Kernel Users'); + + function test(tlsCATrusts, success, failure) { + const options = { + auth: '', + tlsMode: 'requireTLS', + tlsCertificateKeyFile: SERVER_CERT, + tlsCAFile: COMBINED_CA_CERT, + }; + + if (tlsCATrusts !== null) { + options.setParameter = { + tlsCATrusts: tojson(tlsCATrusts), + }; + } + + const mongod = MongoRunner.runMongod(options); + + const admin = mongod.getDB('admin'); + admin.createUser({user: 'admin', pwd: 'pwd', roles: ['root']}); + admin.auth({user: 'admin', pwd: 'pwd'}); + + const external = mongod.getDB('$external'); + USERS.forEach((u) => external.createUser({user: u + RDN_SUFFIX, roles: []})); + + const testDB = mongod.getDB('test'); + testDB.createRole({role: 'role1', privileges: [], roles: []}); + testDB.createRole({role: 'role2', privileges: [], roles: []}); + + // Sorting JS arrays of objects with arbitrary order is... complex. + const serverTrusts = + assert.commandWorked(admin.runCommand({getParameter: 1, tlsCATrusts: 1})).tlsCATrusts; + function sortAndNormalizeRoles(roles) { + return roles.map((r) => r.role + '.' + r.db).sort().join('/'); + } + function sortAndNormalizeTrusts(trusts) { + if (trusts === null) { + return "(unconfigured)"; + } + return trusts.map((t) => t.sha256 + '/' + sortAndNormalizeRoles(t.roles)).sort(); + } + assert.eq(sortAndNormalizeTrusts(tlsCATrusts), sortAndNormalizeTrusts(serverTrusts)); + + function impl(user, expect) { + const snRoles = tojson(sortAndNormalizeRoles(user.roles)); + const uri = 'mongodb://localhost:' + mongod.port + '/admin'; + const script = tojson(sortAndNormalizeRoles) + + 'assert(db.getSiblingDB("$external").auth({mechanism: "MONGODB-X509"}));' + + 'const status = assert.commandWorked(db.runCommand({connectionStatus: 1}));' + + 'const roles = status.authInfo.authenticatedUserRoles;' + + 'assert.eq(' + snRoles + ', sortAndNormalizeRoles(roles));'; + const mongo = runMongoProgram('mongo', + '--tls', + '--tlsCertificateKeyFile', + user.cert, + '--tlsCAFile', + CA_CERT, + uri, + '--eval', + script); + expect(mongo, 0); + } + + success.forEach((u) => impl(u, assert.eq)); + failure.forEach((u) => impl(u, assert.neq)); + + MongoRunner.stopMongod(mongod); + } + + // Positive tests. + const unconfigured = null; + test(unconfigured, [CLIENT, CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES], []); + + const allRoles = [ + {sha256: CA_HASH, roles: [{role: '', db: ''}]}, + {sha256: TRUSTED_CA_HASH, roles: [{role: '', db: ''}]} + ]; + test(allRoles, [CLIENT, CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES], []); + + const allRolesOnAdmin = [{sha256: CA_HASH, roles: [{role: '', db: 'admin'}]}]; + test(allRolesOnAdmin, [CLIENT, CLIENT_ROLES], [TRUSTED_CLIENT_TESTDB_ROLES]); + + const specificRolesOnAnyDB = + [{sha256: CA_HASH, roles: [{role: 'backup', db: ''}, {role: 'readAnyDatabase', db: ''}]}]; + test(specificRolesOnAnyDB, [CLIENT, CLIENT_ROLES], [TRUSTED_CLIENT_TESTDB_ROLES]); + + const exactRoles = [{ + sha256: CA_HASH, + roles: [{role: 'backup', db: 'admin'}, {role: 'readAnyDatabase', db: 'admin'}] + }]; + test(exactRoles, [CLIENT, CLIENT_ROLES], [TRUSTED_CLIENT_TESTDB_ROLES]); + + const extraRoles = [{ + sha256: CA_HASH, + roles: [ + {role: 'backup', db: 'admin'}, + {role: 'readAnyDatabase', db: 'admin'}, + {role: 'readWrite', db: 'admin'} + ] + }]; + test(extraRoles, [CLIENT, CLIENT_ROLES], [TRUSTED_CLIENT_TESTDB_ROLES]); + + const similarRoles = [ + { + sha256: CA_HASH, + roles: [ + {role: 'backup', db: 'test'}, + {role: 'readAnyDatabase', db: ''}, + {role: 'backup', db: 'admin'} + ] + }, + { + sha256: TRUSTED_CA_HASH, + roles: [ + {role: 'role1', db: 'admin'}, + {role: 'role2', db: 'testDB'}, + {role: 'role1', db: 'testDB'}, + ] + } + ]; + test(similarRoles, [CLIENT, CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES], []); + + const withUntrusted = + [{sha256: CA_HASH, roles: [{role: '', db: ''}]}, {sha256: TRUSTED_CA_HASH, roles: []}]; + test(withUntrusted, [CLIENT, CLIENT_ROLES], [TRUSTED_CLIENT_TESTDB_ROLES]); + + const customRoles = [{ + sha256: TRUSTED_CA_HASH, + roles: [ + {role: 'role1', db: 'testDB'}, + {role: 'role2', db: 'testDB'}, + ] + }]; + test(customRoles, [CLIENT, TRUSTED_CLIENT_TESTDB_ROLES], [CLIENT_ROLES]); + + // Negative tests. CLIENT_CERT is okay because it doesn't ask for roles. + const noTrustedCAs = []; + test(noTrustedCAs, [CLIENT], [CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES]); + + const noRoles = [{sha256: CA_HASH, roles: []}]; + test(noRoles, [CLIENT], [CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES]); + + const insufficientRoles1 = [ + {sha256: CA_HASH, roles: [{role: 'backup', db: ''}]}, + {sha256: TRUSTED_CA_HASH, roles: [{role: 'role1', db: 'testDB'}]} + ]; + test(insufficientRoles1, [CLIENT], [CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES]); + + const insufficientRoles2 = [ + {sha256: CA_HASH, roles: [{role: 'readWriteAnyDatabase', db: ''}]}, + {sha256: TRUSTED_CA_HASH, roles: [{role: 'role2', db: 'testDB'}]} + ]; + test(insufficientRoles2, [CLIENT], [CLIENT_ROLES, TRUSTED_CLIENT_TESTDB_ROLES]); + + const withTrusted = + [{sha256: CA_HASH, roles: []}, {sha256: TRUSTED_CA_HASH, roles: [{role: '', db: ''}]}]; + test(withTrusted, [CLIENT, TRUSTED_CLIENT_TESTDB_ROLES], [CLIENT_ROLES]); +}); diff --git a/jstests/ssl/x509/certs.yml b/jstests/ssl/x509/certs.yml index a8feae3f6c1..46d0f3a96c2 100644 --- a/jstests/ssl/x509/certs.yml +++ b/jstests/ssl/x509/certs.yml @@ -517,6 +517,16 @@ certs: subjectAltName: DNS: ['localhost', '127.0.0.1'] +- name: 'trusted-client-testdb-roles.pem' + description: Client certificate with X509 role grants via trusted chain. + Subject: {OU: 'Kernel Users', CN: 'Trusted Kernel Test Client With Roles'} + Issuer: 'trusted-ca.pem' + output_path: 'jstests/ssl/x509/' + extensions: + mongoRoles: + - {role: role1, db: testDB} + - {role: role2, db: testDB} + # Both ca.pem and trusted-ca.pem - name: 'root-and-trusted-ca.pem' description: Combined ca.pem and trusted-ca.pem diff --git a/jstests/ssl/x509/trusted-client-testdb-roles.pem b/jstests/ssl/x509/trusted-client-testdb-roles.pem new file mode 100644 index 00000000000..858ae8a773a --- /dev/null +++ b/jstests/ssl/x509/trusted-client-testdb-roles.pem @@ -0,0 +1,55 @@ +# Autogenerated file, do not edit. +# Generate using jstests/ssl/x509/mkcert.py --config jstests/ssl/x509/certs.yml trusted-client-testdb-roles.pem +# +# Client certificate with X509 role grants via trusted chain. +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIEQvQH6zANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJV +UzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5ldyBZb3JrIENpdHkxEDAO +BgNVBAoMB01vbmdvREIxDzANBgNVBAsMBktlcm5lbDEfMB0GA1UEAwwWVHJ1c3Rl +ZCBLZXJuZWwgVGVzdCBDQTAeFw0yMDAxMDcxNzMxNDhaFw00MDAxMDkxNzMxNDha +MIGRMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5l +dyBZb3JrIENpdHkxEDAOBgNVBAoMB01vbmdvREIxFTATBgNVBAsMDEtlcm5lbCBV +c2VyczEuMCwGA1UEAwwlVHJ1c3RlZCBLZXJuZWwgVGVzdCBDbGllbnQgV2l0aCBS +b2xlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPgWO/0KXHIk0KH/ +jePx+uC8M34bx8ncAWvUXKZtaNGkv2+LI0k/1U5ybOTD8kg8tnIMYkquuuG2zeIB +99vq2Ve+3j62PlqR4HDzXTt3M3eYp6muRzNn78yxVRn+eiIrdwbnvr28l3ikUaVV +/u9fsHGZOXto+I6tWSWB7MNEVcPtIu2d8XU2gMrqKfpnG0paUKVWkaKyjUX1DsBL +FUybBbjQj0zK5cUeKoZjSmMtRfqV6ngKmOK4xTBsQ2VKi7AntpALq/knAYU8BaqS +wWbVuj5sJX86tdRGGhZ6QKIODTQENPprFaJhy34qrhRkD+YHy7tQ+7vc1JpGodiu +C7/5K+kCAwEAAaM3MDUwMwYLKwYBBAGCjikCAQEEJDEiMA8MBXJvbGUxDAZ0ZXN0 +REIwDwwFcm9sZTIMBnRlc3REQjANBgkqhkiG9w0BAQsFAAOCAQEAlYR0WB/0yHxM +gvS+hjxQWyRFOJdWcFn0xresIBd4PmQO8cnOz8iuFrg8DKnYroBRFp5tR9VSLFpq +EH5xoEUMYEAGryYNp8jjOqxy6lIFUZIOf5Li0CtnnV2qHqsiq0kLpSEt+SbpGXtt +zS1CkgKwj0VMXwl+3HY73Xj6EVUPqqMf+Frc68S0ey1S7+pgr1fHzFN309tcGt4r +uxDsSAvYJcYTYDj4KaycXovUsIq+kB+E+k5DnbwJYqHErx2r86QCasK9QIE2Eujl +t+sBpj8JIObPdpsxEiQ9r1+lurWhyEB4qrtI8fzys/0yHP+EYvra3+HftHXY/t32 +jZ79J4C3YQ== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD4Fjv9ClxyJNCh +/43j8frgvDN+G8fJ3AFr1FymbWjRpL9viyNJP9VOcmzkw/JIPLZyDGJKrrrhts3i +Affb6tlXvt4+tj5akeBw8107dzN3mKeprkczZ+/MsVUZ/noiK3cG5769vJd4pFGl +Vf7vX7BxmTl7aPiOrVklgezDRFXD7SLtnfF1NoDK6in6ZxtKWlClVpGiso1F9Q7A +SxVMmwW40I9MyuXFHiqGY0pjLUX6lep4CpjiuMUwbENlSouwJ7aQC6v5JwGFPAWq +ksFm1bo+bCV/OrXURhoWekCiDg00BDT6axWiYct+Kq4UZA/mB8u7UPu73NSaRqHY +rgu/+SvpAgMBAAECggEALVaa5fajyHRz8HcsrjDF4ZZjbrOTApADbnpj6EJseou6 +NJ9f9n4E9I4y2mf4+jymNxeOSwm9u4xV+ezUKEu2JrQKF7nkkVbBhsLjEgAJ1tx+ +H6Nq/bkL+QObguGf3mjFGuz1TeWOZQzaovWhXovFSi1vdN9NNX32ocUpyNHPPrvW +nc3Gun/hws4qWwBFpR+8fzMHPJc/NCwZpDRoJl0yXEAkGTKtIEGEJ7tlWLSb5Bz2 +5N0Dkn2S3t8uozPIuv0rjdYd1t+FfOUUAZGI09LCIu9ndBYO6Vj+Vh99xedZ2oBa +9lHQp3vhLaCXg7O3bY3ac9BIOwdAqcbWAJV/oQl5gQKBgQD90geToF81xZYgLZoU +iU8RRUSmdurZtNirDkMU+/u5Fwu7yn8M53l++TPP76Hi1yGP9803LwIXNXfyg+sb +BRAPJg+bJ8N6m1vFdfg509oqlrzoxnmulwBshqt5HbpiOjYAc1cSOpYSXGJjHoFL ++Au4MRsfDh5RhT1zrUT11+6ZEQKBgQD6N5nGaPOsLHdYXk2//yZF1Ol9kl3L0VWM +XT0F9m/KSCg1kSf+2XCt1U/b1JsrjMTOZWVHNV3yebPs9/pR2ffmyeXtySFuEVeb +ZVNSxaCSVVbTJL9W+mpXdqzcTj9IL9tMN6J5PE8eQ6pjG299sBmdj5S92a3uoxQr +5RmGn36lWQKBgQCix4XQaXNmGteSv2wna3/nxZKnZ3BqOo8R9M2UsZ3YMC14O/+L +GRBUHCHcYwRhZDLED9nuYBlpJQNN5shqxa5s6K3thWzaPrR2SJfvDizGT3HLny3+ +iBzffOaPgD8+K7LiSxY2PJhuIg1/H9swC14IvIV2Pym2gkrM2vx05gzA4QKBgQCW +FmngEK4xVY7U6+Q5SYQcmSThVL18d3mYM4laHUNbE8NCtmpGPQmQzAYV98aH7e1T +XJDOkN1kh8n8V5bIKDXCMtL/ugiabD6fkLzVRoQVoqjtB/rZ4mWNRztS/oCI/WPO +qQSFMj7HCZGX1yoeO1ZyI2D2LC9fmGSOG+Me1Gb0KQKBgQCZazY6Wb7HPO50HnN3 +e3QrT9VE1PKLW6dWpokdYzq2ISnX8ZBeKvMBX+TpKASduNVXK5shsuNqjMAeXtVk +V90P2QkgswCoUlgiaxKby7jBqDIO9CsLt0erQ328WUsf9mgk18CmCc42EWBPuQv7 +WTykB3JVLPGKjKcZVI4PP91yAw== +-----END PRIVATE KEY----- diff --git a/src/mongo/crypto/hash_block.h b/src/mongo/crypto/hash_block.h index 8d82b4c4d9d..880b9a6832d 100644 --- a/src/mongo/crypto/hash_block.h +++ b/src/mongo/crypto/hash_block.h @@ -40,7 +40,9 @@ #include "mongo/base/status_with.h" #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/util/builder.h" #include "mongo/util/base64.h" +#include "mongo/util/hex.h" #include "mongo/util/secure_compare_memory.h" namespace mongo { @@ -122,6 +124,20 @@ public: return HashBlock(newHash); } + static StatusWith<HashBlock> fromHexStringNoThrow(StringData hex) { + if (!isValidHex(hex)) { + return {ErrorCodes::BadValue, "Hash input is not a hex string"}; + } + + BufBuilder buf; + mongo::fromHexString(hex, &buf); + return fromBuffer(reinterpret_cast<const uint8_t*>(buf.buf()), buf.len()); + } + + static HashBlock fromHexString(StringData hex) { + return uassertStatusOK(fromHexStringNoThrow(hex)); + } + static void computeHash(std::initializer_list<ConstDataRange> input, HashBlock* const output) { Traits::computeHash(input, &(output->_hash)); } @@ -254,6 +270,13 @@ public: StringData(reinterpret_cast<const char*>(_hash.data()), _hash.size())); } + /** + * Hex encoded hash block. + */ + std::string toHexString() const { + return toHex(_hash.data(), _hash.size()); + } + bool operator==(const HashBlock& other) const { return consttimeMemEqual(this->_hash.data(), other._hash.data(), kHashLength); } @@ -262,6 +285,10 @@ public: return !(*this == other); } + bool operator<(const HashBlock& other) const { + return this->_hash < other._hash; + } + bool operator==(const HashBlock::Secure& other) const { return consttimeMemEqual(this->_hash.data(), other.data(), kHashLength); } diff --git a/src/mongo/crypto/sha1_block.idl b/src/mongo/crypto/sha1_block.idl index d6054d9e3f6..59ddbbeca61 100644 --- a/src/mongo/crypto/sha1_block.idl +++ b/src/mongo/crypto/sha1_block.idl @@ -44,3 +44,10 @@ types: cpp_type: mongo::SHA1Block serializer: "mongo::SHA1Block::toCDR" deserializer: "mongo::SHA1Block::fromBinData" + + sha1BlockHex: + bson_serialization_type: string + description: "A fixed size hex string representing a SHA1 computation" + cpp_type: mongo::SHA1Block + serializer: "mongo::SHA1Block::toHexString" + deserializer: "mongo::SHA1Block::fromHexString" diff --git a/src/mongo/crypto/sha256_block.idl b/src/mongo/crypto/sha256_block.idl index 734983f286a..a5b4a1cef41 100644 --- a/src/mongo/crypto/sha256_block.idl +++ b/src/mongo/crypto/sha256_block.idl @@ -44,3 +44,10 @@ types: cpp_type: mongo::SHA256Block serializer: "mongo::SHA256Block::toCDR" deserializer: "mongo::SHA256Block::fromBinData" + + sha256BlockHex: + bson_serialization_type: string + description: "A fixed size hex string representing a SHA256 computation" + cpp_type: mongo::SHA256Block + serializer: "mongo::SHA256Block::toHexString" + deserializer: "mongo::SHA256Block::fromHexString" diff --git a/src/mongo/crypto/sha512_block.idl b/src/mongo/crypto/sha512_block.idl index 46b35d13613..e7d3ec8925a 100644 --- a/src/mongo/crypto/sha512_block.idl +++ b/src/mongo/crypto/sha512_block.idl @@ -44,3 +44,10 @@ types: cpp_type: mongo::SHA512Block serializer: "mongo::SHA512Block::toCDR" deserializer: "mongo::SHA512Block::fromBinData" + + sha512BlockHex: + bson_serialization_type: string + description: "A fixed size hex string representing a SHA512 computation" + cpp_type: mongo::SHA512Block + serializer: "mongo::SHA512Block::toHexString" + deserializer: "mongo::SHA512Block::fromHexString" diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp index 99fc341ca3c..eda69e8b9a7 100644 --- a/src/mongo/util/net/ssl_manager_apple.cpp +++ b/src/mongo/util/net/ssl_manager_apple.cpp @@ -1425,6 +1425,8 @@ StatusWith<SSLPeerInfo> SSLManagerApple::parseAndValidatePeerCertificate( boost::optional<std::string> sniName, const std::string& remoteHost, const HostAndPort& hostForLogging) { + invariant(!sslGlobalParams.tlsCATrusts); + // Record TLS version stats auto tlsVersionStatus = mapTLSVersion(ssl); if (!tlsVersionStatus.isOK()) { diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index 5f2c4048bbc..95fb0c6a075 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -1888,6 +1888,67 @@ StatusWith<TLSVersion> mapTLSVersion(SSL* conn) { } } +namespace { +Status _validatePeerRoles(const stdx::unordered_set<RoleName>& embeddedRoles, SSL* conn) { + if (embeddedRoles.empty()) { + // Nothing offered, nothing to restrict. + return Status::OK(); + } + + if (!sslGlobalParams.tlsCATrusts) { + // Nothing restricted. + return Status::OK(); + } + + const auto& tlsCATrusts = sslGlobalParams.tlsCATrusts.get(); + if (tlsCATrusts.empty()) { + // Nothing permitted. + return {ErrorCodes::BadValue, + "tlsCATrusts parameter prohibits role based authorization via X509 certificates"}; + } + + auto stack = SSLgetVerifiedChain(conn); + if (!stack || !sk_X509_num(stack.get())) { + return {ErrorCodes::BadValue, "Unable to obtain certificate chain"}; + } + + auto root = sk_X509_value(stack.get(), sk_X509_num(stack.get()) - 1); + SHA256Block::HashType digest; + if (!X509_digest(root, EVP_sha256(), digest.data(), nullptr)) { + return {ErrorCodes::BadValue, "Unable to digest root certificate"}; + } + + SHA256Block sha256(digest); + auto it = tlsCATrusts.find(sha256); + if (it == tlsCATrusts.end()) { + return { + ErrorCodes::BadValue, + str::stream() << "CA: " << sha256.toHexString() + << " is not authorized to grant any roles due to tlsCATrusts parameter"}; + } + + auto allowedRoles = it->second; + // See TLSCATrustsSetParameter::set() for a description of tlsCATrusts format. + if (allowedRoles.count(RoleName("", ""))) { + // CA is authorized for all role assignments. + return Status::OK(); + } + + for (const auto& role : embeddedRoles) { + // Check for exact match or wildcard matches. + if (!allowedRoles.count(role) && !allowedRoles.count(RoleName(role.getRole(), "")) && + !allowedRoles.count(RoleName("", role.getDB()))) { + return {ErrorCodes::BadValue, + str::stream() << "CA: " << sha256.toHexString() + << " is not authorized to grant role " << role.toString() + << " due to tlsCATrusts parameter"}; + } + } + + return Status::OK(); +} +} // namespace + StatusWith<SSLPeerInfo> SSLManagerOpenSSL::parseAndValidatePeerCertificate( SSL* conn, boost::optional<std::string> sni, @@ -1952,6 +2013,10 @@ StatusWith<SSLPeerInfo> SSLManagerOpenSSL::parseAndValidatePeerCertificate( return swPeerCertificateRoles.getStatus(); } + if (auto status = _validatePeerRoles(swPeerCertificateRoles.getValue(), conn); !status.isOK()) { + return status; + } + // Server side. if (remoteHost.empty()) { const auto exprThreshold = tlsX509ExpirationWarningThresholdDays; diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp index 4106f1f8ba3..48e21eeae20 100644 --- a/src/mongo/util/net/ssl_manager_windows.cpp +++ b/src/mongo/util/net/ssl_manager_windows.cpp @@ -1864,6 +1864,8 @@ StatusWith<SSLPeerInfo> SSLManagerWindows::parseAndValidatePeerCertificate( boost::optional<std::string> sni, const std::string& remoteHost, const HostAndPort& hostForLogging) { + invariant(!sslGlobalParams.tlsCATrusts); + PCCERT_CONTEXT cert; auto tlsVersionStatus = mapTLSVersion(ssl); diff --git a/src/mongo/util/net/ssl_options.h b/src/mongo/util/net/ssl_options.h index cb820ba91e7..56faa4f9dde 100644 --- a/src/mongo/util/net/ssl_options.h +++ b/src/mongo/util/net/ssl_options.h @@ -29,12 +29,17 @@ #pragma once +#include <boost/optional.hpp> +#include <map> +#include <set> #include <string> #include <vector> #include "mongo/base/status.h" #include "mongo/base/status_with.h" #include "mongo/config.h" +#include "mongo/crypto/sha256_block.h" +#include "mongo/db/auth/role_name.h" namespace mongo { @@ -49,6 +54,8 @@ class Environment; } // namespace optionenvironment struct SSLParams { + using TLSCATrusts = std::map<SHA256Block, std::set<RoleName>>; + enum class Protocols { TLS1_0, TLS1_1, TLS1_2, TLS1_3 }; AtomicWord<int> sslMode; // --tlsMode - the TLS operation mode, see enum SSLModes std::string sslPEMTempDHParam; // --setParameter OpenSSLDiffieHellmanParameters=file : PEM file @@ -62,6 +69,8 @@ struct SSLParams { std::string sslCRLFile; // --tlsCRLFile std::string sslCipherConfig; // --tlsCipherConfig + boost::optional<TLSCATrusts> tlsCATrusts; // --setParameter tlsCATrusts + struct CertificateSelector { std::string subject; std::vector<uint8_t> thumbprint; diff --git a/src/mongo/util/net/ssl_parameters.cpp b/src/mongo/util/net/ssl_parameters.cpp index fd1f8d23c58..66d5fdc5721 100644 --- a/src/mongo/util/net/ssl_parameters.cpp +++ b/src/mongo/util/net/ssl_parameters.cpp @@ -33,6 +33,7 @@ #include "mongo/util/net/ssl_parameters.h" +#include "mongo/bson/json.h" #include "mongo/config.h" #include "mongo/db/auth/sasl_command_constants.h" #include "mongo/db/server_options.h" @@ -148,6 +149,97 @@ Status TLSModeServerParameter::setFromString(const std::string& strMode) { return Status::OK(); } +void TLSCATrustsSetParameter::append(OperationContext*, + BSONObjBuilder& b, + const std::string& name) { + if (!sslGlobalParams.tlsCATrusts) { + b.appendNull(name); + return; + } + + BSONArrayBuilder trusts; + + for (const auto& cait : sslGlobalParams.tlsCATrusts.get()) { + BSONArrayBuilder roles; + + for (const auto& rolename : cait.second) { + BSONObjBuilder role; + role.append("role", rolename.getRole()); + role.append("db", rolename.getDB()); + roles.append(role.obj()); + } + + BSONObjBuilder ca; + ca.append("sha256", cait.first.toHexString()); + ca.append("roles", roles.arr()); + + trusts.append(ca.obj()); + } + + b.append(name, trusts.arr()); +} + +/** + * tlsCATrusts takes the form of an array of documents describing + * a set of roles which a given certificate authority may grant. + * + * [ + * { + * "sha256": "0123456789abcdef...", // SHA256 digest of a CA, as hex. + * "roles": [ // Array of grantable RoleNames + * { role: "read", db: "foo" }, + * { role: "readWrite", "db: "bar" }, + * // etc... + * ], + * }, + * // { "sha256": "...", roles: [...]}, // Additional documents... + * ] + * + * If this list has been set, and a client connects with a certificate + * containing roles which it has not been authorized to grant, + * then the connection will be refused. + * + * Wilcard roles may be defined by omitting the role and/or db portions: + * + * { role: "", db: "foo" } // May grant any role on the 'foo' DB. + * { role: "read", db: "" } // May grant 'read' role on any DB. + * { role: "", db: "" } // May grant any role on any DB. + */ +Status TLSCATrustsSetParameter::set(const BSONElement& element) try { + if ((element.type() != Object) || !element.Obj().couldBeArray()) { + return {ErrorCodes::BadValue, "Value must be an array"}; + } + + SSLParams::TLSCATrusts trusts; + for (const auto& trustElement : BSONArray(element.Obj())) { + if (trustElement.type() != Object) { + return {ErrorCodes::BadValue, "Value must be an array of trust definitions"}; + } + + IDLParserErrorContext ctx("tlsCATrusts"); + auto trust = TLSCATrust::parse(ctx, trustElement.Obj()); + + if (trusts.find(trust.getSha256()) != trusts.end()) { + return {ErrorCodes::BadValue, + str::stream() << "Duplicate thumbprint: " << trust.getSha256().toString()}; + } + + const auto& roles = trust.getRoles(); + trusts[std::move(trust.getSha256())] = std::set<RoleName>(roles.begin(), roles.end()); + } + + sslGlobalParams.tlsCATrusts = std::move(trusts); + return Status::OK(); +} catch (...) { + return exceptionToStatus(); +} + +Status TLSCATrustsSetParameter::setFromString(const std::string& json) try { + return set(BSON("" << fromjson(json)).firstElement()); +} catch (...) { + return exceptionToStatus(); +} + } // namespace mongo mongo::Status mongo::validateOpensslCipherConfig(const std::string&) { diff --git a/src/mongo/util/net/ssl_parameters.idl b/src/mongo/util/net/ssl_parameters.idl index bad759341e1..842e592df87 100644 --- a/src/mongo/util/net/ssl_parameters.idl +++ b/src/mongo/util/net/ssl_parameters.idl @@ -32,6 +32,18 @@ global: - "mongo/util/net/ssl_options.h" - "mongo/util/net/ssl_parameters.h" +imports: + - "mongo/crypto/sha256_block.idl" + - "mongo/db/auth/auth_types.idl" + +structs: + TLSCATrust: + description: + strict: true + fields: + sha256: sha256BlockHex + roles: array<RoleName> + server_parameters: opensslDiffieHellmanParameters: description: "OpenSSL Diffie-Hellman parameters" @@ -95,3 +107,15 @@ server_parameters: default: 30 validator: gte: 0 + + tlsCATrusts: + description: >- + Specify by fingerprint the certificate authorities which are allowed to + accept role authorizations from an X509 certificate and the specific roles + they are allowed to impart. + set_at: startup + cpp_class: + name: TLSCATrustsSetParameter + override_set: true + condition: + preprocessor: 'defined(__linux__)' |