summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSara Golemon <sara.golemon@mongodb.com>2020-01-13 20:38:27 +0000
committerA. Jesse Jiryu Davis <jesse@mongodb.com>2020-01-27 15:40:40 -0500
commitd346a2beb3aaa9338dd351f3b085431b21a9e93f (patch)
tree7d2dcb8a6250d06de434890399e1281fc173792b
parent0271734bb2382630fa7008bf2fd9132560181bb5 (diff)
downloadmongo-d346a2beb3aaa9338dd351f3b085431b21a9e93f.tar.gz
SERVER-44435 Allow selective whitelisting of X509 based role authorizations
create mode 100644 jstests/ssl/tlsCATrusts.js create mode 100644 jstests/ssl/x509/trusted-client-testdb-roles.pem
-rw-r--r--jstests/ssl/tlsCATrusts.js189
-rw-r--r--jstests/ssl/x509/certs.yml10
-rw-r--r--jstests/ssl/x509/trusted-client-testdb-roles.pem55
-rw-r--r--src/mongo/crypto/hash_block.h27
-rw-r--r--src/mongo/crypto/sha1_block.idl7
-rw-r--r--src/mongo/crypto/sha256_block.idl7
-rw-r--r--src/mongo/crypto/sha512_block.idl7
-rw-r--r--src/mongo/util/net/ssl_manager_apple.cpp2
-rw-r--r--src/mongo/util/net/ssl_manager_openssl.cpp65
-rw-r--r--src/mongo/util/net/ssl_manager_windows.cpp2
-rw-r--r--src/mongo/util/net/ssl_options.h9
-rw-r--r--src/mongo/util/net/ssl_parameters.cpp92
-rw-r--r--src/mongo/util/net/ssl_parameters.idl24
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__)'