summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSara Golemon <sara.golemon@mongodb.com>2018-02-20 12:39:37 -0500
committerSara Golemon <sara.golemon@mongodb.com>2018-03-17 12:45:29 -0400
commit65191bbe5c00bb419a7466fb7db43e220035f776 (patch)
tree1f2c00082b35eafbcdaa1539ed680ac84949a640
parent9eea82ee2a0e37da90cbb549d55c5eac8aa69a55 (diff)
downloadmongo-65191bbe5c00bb419a7466fb7db43e220035f776.tar.gz
SERVER-22412 Implement a secure transport ASIO backend
-rw-r--r--SConstruct10
-rw-r--r--jstests/ssl/dh_params.js7
-rw-r--r--jstests/ssl/libs/ssl_helpers.js31
-rw-r--r--jstests/ssl/ssl_cert_password.js341
-rw-r--r--jstests/ssl/ssl_crl.js21
-rw-r--r--jstests/ssl/ssl_crl_revoked.js42
-rw-r--r--jstests/ssl/ssl_options.js82
-rw-r--r--jstests/sslSpecial/SERVER-26369.js14
-rw-r--r--src/mongo/config.h.in1
-rw-r--r--src/mongo/util/net/ssl/apple.hpp98
-rw-r--r--src/mongo/util/net/ssl/context.hpp4
-rw-r--r--src/mongo/util/net/ssl/context_apple.hpp82
-rw-r--r--src/mongo/util/net/ssl/detail/engine.hpp4
-rw-r--r--src/mongo/util/net/ssl/detail/engine_apple.hpp103
-rw-r--r--src/mongo/util/net/ssl/detail/impl/engine_apple.ipp347
-rw-r--r--src/mongo/util/net/ssl/detail/stream_core.hpp4
-rw-r--r--src/mongo/util/net/ssl/impl/error.ipp10
-rw-r--r--src/mongo/util/net/ssl/impl/src.hpp5
-rw-r--r--src/mongo/util/net/ssl/stream.hpp3
-rw-r--r--src/mongo/util/net/ssl_manager.cpp29
-rw-r--r--src/mongo/util/net/ssl_manager.h4
-rw-r--r--src/mongo/util/net/ssl_manager_apple.cpp1171
-rw-r--r--src/mongo/util/net/ssl_manager_openssl.cpp19
-rw-r--r--src/mongo/util/version.cpp6
24 files changed, 2173 insertions, 265 deletions
diff --git a/SConstruct b/SConstruct
index fbbf8f433bb..fcd14983ed4 100644
--- a/SConstruct
+++ b/SConstruct
@@ -2901,11 +2901,13 @@ def doConfigure(myenv):
conf.env.Append( MONGO_CRYPTO=["windows"] )
elif conf.env.TargetOSIs('darwin', 'macOS'):
+ ssl_provider = 'apple'
+ env.SetConfigHeaderDefine("MONGO_CONFIG_SSL_PROVIDER", "SSL_PROVIDER_APPLE")
conf.env.Append( MONGO_CRYPTO=["apple"] )
- if has_option("ssl"):
- # TODO: Replace SSL implementation as well.
- # For now, let openssl fill that role.
- checkOpenSSL(conf)
+ conf.env.AppendUnique(FRAMEWORKS=[
+ 'CoreFoundation',
+ 'Security',
+ ])
if ssl_provider == 'openssl':
if has_option("ssl"):
diff --git a/jstests/ssl/dh_params.js b/jstests/ssl/dh_params.js
index 2da7867aff9..852fc8c311b 100644
--- a/jstests/ssl/dh_params.js
+++ b/jstests/ssl/dh_params.js
@@ -1,9 +1,8 @@
-(function() {
+load("jstests/ssl/libs/ssl_helpers.js");
+requireSSLProvider('openssl', function() {
"use strict";
- load("jstests/ssl/libs/ssl_helpers.js");
-
// Verify that requireSSL with Diffie-Hellman parameters allows ssl connections
print("=== Testing that DHParams files can be loaded ===");
replShouldSucceed("dhparam-dhparam", dhparamSSL, dhparamSSL);
-}());
+});
diff --git a/jstests/ssl/libs/ssl_helpers.js b/jstests/ssl/libs/ssl_helpers.js
index 23703683aa4..05d0a5b9333 100644
--- a/jstests/ssl/libs/ssl_helpers.js
+++ b/jstests/ssl/libs/ssl_helpers.js
@@ -176,3 +176,34 @@ function mixedShardTest(options1, options2, shouldSucceed) {
}
}
}
+
+function determineSSLProvider() {
+ 'use strict';
+ const info = getBuildInfo();
+ const ssl = (info.openssl === undefined) ? '' : info.openssl.running;
+ if (/OpenSSL/.test(ssl)) {
+ return 'openssl';
+ } else if (/Apple/.test(ssl)) {
+ return 'apple';
+ } else if (/Windows/.test(ssl)) {
+ return 'windows';
+ } else {
+ return null;
+ }
+}
+
+function requireSSLProvider(required, fn) {
+ 'use strict';
+ if ((typeof required) === 'string') {
+ required = [required];
+ }
+
+ const provider = determineSSLProvider();
+ if (!required.includes(provider)) {
+ print("*****************************************************");
+ print("Skipping " + tojson(required) + " test because SSL provider is " + provider);
+ print("*****************************************************");
+ return;
+ }
+ fn();
+}
diff --git a/jstests/ssl/ssl_cert_password.js b/jstests/ssl/ssl_cert_password.js
index 2595510ff4a..356c0603c84 100644
--- a/jstests/ssl/ssl_cert_password.js
+++ b/jstests/ssl/ssl_cert_password.js
@@ -4,172 +4,179 @@
// does not return error statuses to indicate an error.
// This test requires ssl support in mongo-tools
// @tags: [requires_ssl_mongo_tools]
-var baseName = "jstests_ssl_ssl_cert_password";
-var dbpath = MongoRunner.dataPath + baseName;
-var external_scratch_dir = MongoRunner.dataPath + baseName + "/external/";
-resetDbpath(dbpath);
-mkdir(external_scratch_dir);
-
-// Password is correct
-var md = MongoRunner.runMongod({
- nopreallocj: "",
- dbpath: dbpath,
- sslMode: "requireSSL",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty"
-});
-// MongoRunner.runMongod connects a Mongo shell, so if we get here, the test is successful.
-
-// Password incorrect; error logged is:
-// error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
-var exit_code = runMongoProgram("mongo",
- "--port",
- md.port,
- "--ssl",
- "--sslAllowInvalidCertificates",
- "--sslCAFile",
- "jstests/libs/ca.pem",
- "--sslPEMKeyFile",
- "jstests/libs/password_protected.pem",
- "--sslPEMKeyPassword",
- "barf");
-
-// 1 is the exit code for failure
-assert(exit_code == 1);
-
-// Test that mongodump and mongorestore support ssl
-c = md.getDB("dumprestore_ssl").getCollection("foo");
-assert.eq(0, c.count(), "dumprestore_ssl.foo collection is not initially empty");
-c.save({a: 22});
-assert.eq(1, c.count(), "failed to insert document into dumprestore_ssl.foo collection");
-
-exit_code = MongoRunner.runMongoTool("mongodump", {
- out: external_scratch_dir,
- port: md.port,
- ssl: "",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslCAFile: "jstests/libs/ca.pem",
- sslPEMKeyPassword: "qwerty",
-});
-
-assert.eq(exit_code, 0, "Failed to start mongodump with ssl");
-
-c.drop();
-assert.eq(0, c.count(), "dumprestore_ssl.foo collection is not empty after drop");
-
-exit_code = MongoRunner.runMongoTool("mongorestore", {
- dir: external_scratch_dir,
- port: md.port,
- ssl: "",
- sslCAFile: "jstests/libs/ca.pem",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty",
-});
-assert.eq(exit_code, 0, "Failed to start mongorestore with ssl");
-
-assert.soon("c.findOne()", "no data after sleep. Expected a document after calling mongorestore");
-assert.eq(1,
- c.count(),
- "did not find expected document in dumprestore_ssl.foo collection after mongorestore");
-assert.eq(22, c.findOne().a, "did not find correct value in document after mongorestore");
-
-// Test that mongoimport and mongoexport support ssl
-var exportimport_ssl_dbname = "exportimport_ssl";
-c = md.getDB(exportimport_ssl_dbname).getCollection("foo");
-assert.eq(0, c.count(), "exportimport_ssl.foo collection is not initially empty");
-c.save({a: 22});
-assert.eq(1, c.count(), "failed to insert document into exportimport_ssl.foo collection");
-
-var exportimport_file = "data.json";
-
-exit_code = MongoRunner.runMongoTool("mongoexport", {
- out: external_scratch_dir + exportimport_file,
- db: exportimport_ssl_dbname,
- collection: "foo",
- port: md.port,
- ssl: "",
- sslCAFile: "jstests/libs/ca.pem",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty",
+load('jstests/ssl/libs/ssl_helpers.js');
+requireSSLProvider('openssl', function() {
+ var baseName = "jstests_ssl_ssl_cert_password";
+ var dbpath = MongoRunner.dataPath + baseName;
+ var external_scratch_dir = MongoRunner.dataPath + baseName + "/external/";
+ resetDbpath(dbpath);
+ mkdir(external_scratch_dir);
+
+ // Password is correct
+ var md = MongoRunner.runMongod({
+ nopreallocj: "",
+ dbpath: dbpath,
+ sslMode: "requireSSL",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslPEMKeyPassword: "qwerty"
+ });
+ // MongoRunner.runMongod connects a Mongo shell, so if we get here, the test is successful.
+
+ // Password incorrect; error logged is:
+ // error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
+ var exit_code = runMongoProgram("mongo",
+ "--port",
+ md.port,
+ "--ssl",
+ "--sslAllowInvalidCertificates",
+ "--sslCAFile",
+ "jstests/libs/ca.pem",
+ "--sslPEMKeyFile",
+ "jstests/libs/password_protected.pem",
+ "--sslPEMKeyPassword",
+ "barf");
+
+ // 1 is the exit code for failure
+ assert(exit_code == 1);
+
+ // Test that mongodump and mongorestore support ssl
+ c = md.getDB("dumprestore_ssl").getCollection("foo");
+ assert.eq(0, c.count(), "dumprestore_ssl.foo collection is not initially empty");
+ c.save({a: 22});
+ assert.eq(1, c.count(), "failed to insert document into dumprestore_ssl.foo collection");
+
+ exit_code = MongoRunner.runMongoTool("mongodump", {
+ out: external_scratch_dir,
+ port: md.port,
+ ssl: "",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslPEMKeyPassword: "qwerty",
+ });
+
+ assert.eq(exit_code, 0, "Failed to start mongodump with ssl");
+
+ c.drop();
+ assert.eq(0, c.count(), "dumprestore_ssl.foo collection is not empty after drop");
+
+ exit_code = MongoRunner.runMongoTool("mongorestore", {
+ dir: external_scratch_dir,
+ port: md.port,
+ ssl: "",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslPEMKeyPassword: "qwerty",
+ });
+
+ assert.eq(exit_code, 0, "Failed to start mongorestore with ssl");
+
+ assert.soon("c.findOne()",
+ "no data after sleep. Expected a document after calling mongorestore");
+ assert.eq(
+ 1,
+ c.count(),
+ "did not find expected document in dumprestore_ssl.foo collection after mongorestore");
+ assert.eq(22, c.findOne().a, "did not find correct value in document after mongorestore");
+
+ // Test that mongoimport and mongoexport support ssl
+ var exportimport_ssl_dbname = "exportimport_ssl";
+ c = md.getDB(exportimport_ssl_dbname).getCollection("foo");
+ assert.eq(0, c.count(), "exportimport_ssl.foo collection is not initially empty");
+ c.save({a: 22});
+ assert.eq(1, c.count(), "failed to insert document into exportimport_ssl.foo collection");
+
+ var exportimport_file = "data.json";
+
+ exit_code = MongoRunner.runMongoTool("mongoexport", {
+ out: external_scratch_dir + exportimport_file,
+ db: exportimport_ssl_dbname,
+ collection: "foo",
+ port: md.port,
+ ssl: "",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslPEMKeyPassword: "qwerty",
+ });
+
+ assert.eq(exit_code, 0, "Failed to start mongoexport with ssl");
+
+ c.drop();
+ assert.eq(0, c.count(), "afterdrop", "-d", exportimport_ssl_dbname, "-c", "foo");
+
+ exit_code = MongoRunner.runMongoTool("mongoimport", {
+ file: external_scratch_dir + exportimport_file,
+ db: exportimport_ssl_dbname,
+ collection: "foo",
+ port: md.port,
+ ssl: "",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslPEMKeyPassword: "qwerty",
+ });
+
+ assert.eq(exit_code, 0, "Failed to start mongoimport with ssl");
+
+ assert.soon("c.findOne()",
+ "no data after sleep. Expected a document after calling mongoimport");
+ assert.eq(1,
+ c.count(),
+ "did not find expected document in dumprestore_ssl.foo collection after mongoimport");
+ assert.eq(22, c.findOne().a, "did not find correct value in document after mongoimport");
+
+ // Test that mongofiles supports ssl
+ var mongofiles_ssl_dbname = "mongofiles_ssl";
+ mongofiles_db = md.getDB(mongofiles_ssl_dbname);
+
+ source_filename = 'jstests/ssl/ssl_cert_password.js';
+ filename = 'ssl_cert_password.js';
+
+ exit_code = MongoRunner.runMongoTool("mongofiles",
+ {
+ db: mongofiles_ssl_dbname,
+ port: md.port,
+ ssl: "",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslPEMKeyPassword: "qwerty",
+ },
+ "put",
+ source_filename);
+
+ assert.eq(exit_code, 0, "Failed to start mongofiles with ssl");
+
+ md5 = md5sumFile(source_filename);
+
+ file_obj = mongofiles_db.fs.files.findOne();
+ assert(file_obj, "failed to find file object in mongofiles_ssl db using gridfs");
+ md5_stored = file_obj.md5;
+ md5_computed = mongofiles_db.runCommand({filemd5: file_obj._id}).md5;
+ assert.eq(md5, md5_stored, "md5 incorrect for file");
+ assert.eq(md5, md5_computed, "md5 computed incorrectly by server");
+
+ exit_code = MongoRunner.runMongoTool("mongofiles",
+ {
+ db: mongofiles_ssl_dbname,
+ local: external_scratch_dir + filename,
+ port: md.port,
+ ssl: "",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslPEMKeyPassword: "qwerty",
+ },
+ "get",
+ source_filename);
+
+ assert.eq(exit_code, 0, "Failed to start mongofiles with ssl");
+
+ md5 = md5sumFile(external_scratch_dir + filename);
+ assert.eq(md5, md5_stored, "hash of stored file does not match the expected value");
+
+ if (!_isWindows()) {
+ // Stop the server
+ var exitCode = MongoRunner.stopMongod(md);
+ assert(exitCode == 0);
+ } else {
+ MongoRunner.stopMongod(md);
+ }
});
-
-assert.eq(exit_code, 0, "Failed to start mongoexport with ssl");
-
-c.drop();
-assert.eq(0, c.count(), "afterdrop", "-d", exportimport_ssl_dbname, "-c", "foo");
-
-exit_code = MongoRunner.runMongoTool("mongoimport", {
- file: external_scratch_dir + exportimport_file,
- db: exportimport_ssl_dbname,
- collection: "foo",
- port: md.port,
- ssl: "",
- sslCAFile: "jstests/libs/ca.pem",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty",
-});
-
-assert.eq(exit_code, 0, "Failed to start mongoimport with ssl");
-
-assert.soon("c.findOne()", "no data after sleep. Expected a document after calling mongoimport");
-assert.eq(1,
- c.count(),
- "did not find expected document in dumprestore_ssl.foo collection after mongoimport");
-assert.eq(22, c.findOne().a, "did not find correct value in document after mongoimport");
-
-// Test that mongofiles supports ssl
-var mongofiles_ssl_dbname = "mongofiles_ssl";
-mongofiles_db = md.getDB(mongofiles_ssl_dbname);
-
-source_filename = 'jstests/ssl/ssl_cert_password.js';
-filename = 'ssl_cert_password.js';
-
-exit_code = MongoRunner.runMongoTool("mongofiles",
- {
- db: mongofiles_ssl_dbname,
- port: md.port,
- ssl: "",
- sslCAFile: "jstests/libs/ca.pem",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty",
- },
- "put",
- source_filename);
-
-assert.eq(exit_code, 0, "Failed to start mongofiles with ssl");
-
-md5 = md5sumFile(source_filename);
-
-file_obj = mongofiles_db.fs.files.findOne();
-assert(file_obj, "failed to find file object in mongofiles_ssl db using gridfs");
-md5_stored = file_obj.md5;
-md5_computed = mongofiles_db.runCommand({filemd5: file_obj._id}).md5;
-assert.eq(md5, md5_stored, "md5 incorrect for file");
-assert.eq(md5, md5_computed, "md5 computed incorrectly by server");
-
-exit_code = MongoRunner.runMongoTool("mongofiles",
- {
- db: mongofiles_ssl_dbname,
- local: external_scratch_dir + filename,
- port: md.port,
- ssl: "",
- sslCAFile: "jstests/libs/ca.pem",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty",
- },
- "get",
- source_filename);
-
-assert.eq(exit_code, 0, "Failed to start mongofiles with ssl");
-
-md5 = md5sumFile(external_scratch_dir + filename);
-assert.eq(md5, md5_stored, "hash of stored file does not match the expected value");
-
-if (!_isWindows()) {
- // Stop the server
- var exitCode = MongoRunner.stopMongod(md);
- assert(exitCode == 0);
-} else {
- MongoRunner.stopMongod(md);
-}
diff --git a/jstests/ssl/ssl_crl.js b/jstests/ssl/ssl_crl.js
index 9f70ba91b2b..a01a2726f06 100644
--- a/jstests/ssl/ssl_crl.js
+++ b/jstests/ssl/ssl_crl.js
@@ -4,16 +4,19 @@
// Note: crl_expired.pem is a CRL with no revoked certificates, but is an expired CRL.
// crl.pem is a CRL with no revoked certificates.
-load("jstests/libs/ssl_test.js");
+load('jstests/ssl/libs/ssl_helpers.js');
+requireSSLProvider(['openssl', 'windows'], function() {
+ load("jstests/libs/ssl_test.js");
-var testUnrevoked = new SSLTest(
- // Server option overrides
- {sslMode: "requireSSL", sslCRLFile: "jstests/libs/crl.pem"});
+ var testUnrevoked = new SSLTest(
+ // Server option overrides
+ {sslMode: "requireSSL", sslCRLFile: "jstests/libs/crl.pem"});
-assert(testUnrevoked.connectWorked());
+ assert(testUnrevoked.connectWorked());
-var testRevoked = new SSLTest(
- // Server option overrides
- {sslMode: "requireSSL", sslCRLFile: "jstests/libs/crl_expired.pem"});
+ var testRevoked = new SSLTest(
+ // Server option overrides
+ {sslMode: "requireSSL", sslCRLFile: "jstests/libs/crl_expired.pem"});
-assert(!testRevoked.connectWorked());
+ assert(!testRevoked.connectWorked());
+});
diff --git a/jstests/ssl/ssl_crl_revoked.js b/jstests/ssl/ssl_crl_revoked.js
index 1fe4f558f2a..1dab6b0ae70 100644
--- a/jstests/ssl/ssl_crl_revoked.js
+++ b/jstests/ssl/ssl_crl_revoked.js
@@ -2,24 +2,28 @@
// Note: crl_client_revoked.pem is a CRL with the client.pem certificate listed as revoked.
// This test should test that the user cannot connect with client.pem certificate.
-var md = MongoRunner.runMongod({
- sslMode: "requireSSL",
- sslPEMKeyFile: "jstests/libs/server.pem",
- sslCAFile: "jstests/libs/ca.pem",
- sslCRLFile: "jstests/libs/crl_client_revoked.pem"
-});
+load('jstests/ssl/libs/ssl_helpers.js');
+
+requireSSLProvider(['openssl', 'windows'], function() {
+ var md = MongoRunner.runMongod({
+ sslMode: "requireSSL",
+ sslPEMKeyFile: "jstests/libs/server.pem",
+ sslCAFile: "jstests/libs/ca.pem",
+ sslCRLFile: "jstests/libs/crl_client_revoked.pem"
+ });
-var mongo = runMongoProgram("mongo",
- "--port",
- md.port,
- "--ssl",
- "--sslAllowInvalidCertificates",
- "--sslPEMKeyFile",
- "jstests/libs/client_revoked.pem",
- "--eval",
- ";");
+ var mongo = runMongoProgram("mongo",
+ "--port",
+ md.port,
+ "--ssl",
+ "--sslAllowInvalidCertificates",
+ "--sslPEMKeyFile",
+ "jstests/libs/client_revoked.pem",
+ "--eval",
+ ";");
-// 1 is the exit code for the shell failing to connect, which is what we want
-// for a successful test.
-assert(mongo == 1);
-MongoRunner.stopMongod(md); \ No newline at end of file
+ // 1 is the exit code for the shell failing to connect, which is what we want
+ // for a successful test.
+ assert(mongo == 1);
+ MongoRunner.stopMongod(md);
+});
diff --git a/jstests/ssl/ssl_options.js b/jstests/ssl/ssl_options.js
index d1c0c04adcd..9a85142804a 100644
--- a/jstests/ssl/ssl_options.js
+++ b/jstests/ssl/ssl_options.js
@@ -1,41 +1,49 @@
-var baseName = "jstests_ssl_ssl_options";
-
-jsTest.log("Testing censorship of ssl options");
-
-var mongodConfig = {
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslMode: "requireSSL",
- sslPEMKeyPassword: "qwerty",
- sslClusterPassword: "qwerty",
- sslCAFile: "jstests/libs/ca.pem"
-};
-var mongodSource = MongoRunner.runMongod(mongodConfig);
-
-var getCmdLineOptsResult = mongodSource.adminCommand("getCmdLineOpts");
-
-var i;
-var isPassword = false;
-for (i = 0; i < getCmdLineOptsResult.argv.length; i++) {
- if (isPassword) {
- assert.eq(getCmdLineOptsResult.argv[i],
- "<password>",
- "Password not properly censored: " + tojson(getCmdLineOptsResult));
- isPassword = false;
- continue;
- }
+// Test redaction of passwords in command line SSL option parsing.
+
+load('jstests/ssl/libs/ssl_helpers.js');
+requireSSLProvider('openssl', function() {
+ 'use strict';
+
+ const baseName = "jstests_ssl_ssl_options";
+
+ jsTest.log("Testing censorship of ssl options");
- if (getCmdLineOptsResult.argv[i] === "--sslPEMKeyPassword" ||
- getCmdLineOptsResult.argv[i] === "--sslClusterPassword") {
- isPassword = true;
+ const mongodConfig = {
+ sslPEMKeyFile: "jstests/libs/password_protected.pem",
+ sslMode: "requireSSL",
+ sslPEMKeyPassword: "qwerty",
+ sslClusterPassword: "qwerty",
+ sslCAFile: "jstests/libs/ca.pem"
+ };
+ const mongodSource = MongoRunner.runMongod(mongodConfig);
+
+ const getCmdLineOptsResult = mongodSource.adminCommand("getCmdLineOpts");
+
+ let i;
+ let isPassword = false;
+ for (i = 0; i < getCmdLineOptsResult.argv.length; i++) {
+ if (isPassword) {
+ assert.eq(getCmdLineOptsResult.argv[i],
+ "<password>",
+ "Password not properly censored: " + tojson(getCmdLineOptsResult));
+ isPassword = false;
+ continue;
+ }
+
+ if (getCmdLineOptsResult.argv[i] === "--sslPEMKeyPassword" ||
+ getCmdLineOptsResult.argv[i] === "--sslClusterPassword") {
+ isPassword = true;
+ }
}
-}
-assert.eq(getCmdLineOptsResult.parsed.net.ssl.PEMKeyPassword,
- "<password>",
- "Password not properly censored: " + tojson(getCmdLineOptsResult));
-assert.eq(getCmdLineOptsResult.parsed.net.ssl.clusterPassword,
- "<password>",
- "Password not properly censored: " + tojson(getCmdLineOptsResult));
-MongoRunner.stopMongod(mongodSource);
+ assert.eq(getCmdLineOptsResult.parsed.net.ssl.PEMKeyPassword,
+ "<password>",
+ "Password not properly censored: " + tojson(getCmdLineOptsResult));
+ assert.eq(getCmdLineOptsResult.parsed.net.ssl.clusterPassword,
+ "<password>",
+ "Password not properly censored: " + tojson(getCmdLineOptsResult));
+
+ MongoRunner.stopMongod(mongodSource);
-print(baseName + " succeeded.");
+ print(baseName + " succeeded.");
+});
diff --git a/jstests/sslSpecial/SERVER-26369.js b/jstests/sslSpecial/SERVER-26369.js
index 065f646c3fc..e2852189822 100644
--- a/jstests/sslSpecial/SERVER-26369.js
+++ b/jstests/sslSpecial/SERVER-26369.js
@@ -6,15 +6,19 @@ TestData.skipCheckingUUIDsConsistentAcrossCluster = true;
load("jstests/ssl/libs/ssl_helpers.js");
- var st = new ShardingTest({shards: {rs0: {nodes: 1}}});
-
- st.rs0.restart(0, {
+ const st = new ShardingTest({shards: {rs0: {nodes: 1}}});
+ let opts = {
sslMode: "allowSSL",
- sslPEMKeyFile: "jstests/libs/password_protected.pem",
- sslPEMKeyPassword: "qwerty",
+ sslPEMKeyFile: "jstests/libs/client.pem",
sslCAFile: "jstests/libs/ca.pem",
shardsvr: ''
+ };
+ requireSSLProvider('openssl', function() {
+ // Only the OpenSSL provider supports encrypted PKCS#8
+ opts.sslPEMKeyFile = "jstests/libs/password_protected.pem";
+ opts.sslPEMKeyPassword = "qwerty";
});
+ st.rs0.restart(0, opts);
st.stop();
})();
diff --git a/src/mongo/config.h.in b/src/mongo/config.h.in
index 5808477e11f..39a6541ce73 100644
--- a/src/mongo/config.h.in
+++ b/src/mongo/config.h.in
@@ -31,6 +31,7 @@
// List of possible SSL providers
#define SSL_PROVIDER_OPENSSL 1
#define SSL_PROVIDER_WINDOWS 2
+#define SSL_PROVIDER_APPLE 3
// Define to target byte order (1234 vs 4321)
@mongo_config_byte_order@
diff --git a/src/mongo/util/net/ssl/apple.hpp b/src/mongo/util/net/ssl/apple.hpp
new file mode 100644
index 00000000000..e38e2b46c97
--- /dev/null
+++ b/src/mongo/util/net/ssl/apple.hpp
@@ -0,0 +1,98 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#if MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <Security/Security.h>
+#include <memory>
+#include <string>
+
+namespace asio {
+namespace ssl {
+namespace apple {
+
+namespace {
+template <typename T>
+struct CFReleaser {
+ void operator()(T ptr) {
+ if (ptr) {
+ ::CFRelease(ptr);
+ }
+ }
+};
+} // namespace
+
+/**
+ * CoreFoundation types are internally refcounted using CFRetain/CFRelease.
+ * Values received from a method using the word "Copy" typically follow "The Copy Rule"
+ * which requires that the caller explicitly invoke CFRelease on the obtained value.
+ * Values received from a method using the word "Get" typically follow "The Get Rule"
+ * which requires that the caller DOES NOT attempt to release any references,
+ * though it may invoke CFRetain to hold on to the object for longer.
+ *
+ * Use of the CFUniquePtr type assumes that a value was wither obtained from a "Copy"
+ * method, or that it has been explicitly retained.
+ */
+template <typename T>
+using CFUniquePtr = std::unique_ptr<typename std::remove_pointer<T>::type, CFReleaser<T>>;
+
+/**
+ * Equivalent of OpenSSL's SSL_CTX type.
+ * Allows loading SecIdentity and SecCertificate chains
+ * separate from an SSLContext instance.
+ *
+ * Unlike OpenSSL, Secure Transport sets protocol range on
+ * each connection instance separately, so just stash them aside
+ * in the same place for now.
+ */
+struct Context {
+ Context() = default;
+ explicit Context(::SSLProtocol p) : protoMin(p), protoMax(p) {}
+ Context& operator=(const Context& src) {
+ protoMin = src.protoMin;
+ protoMax = src.protoMax;
+ if (src.certs) {
+ ::CFRetain(src.certs.get());
+ }
+ certs.reset(src.certs.get());
+ return *this;
+ }
+
+ ::SSLProtocol protoMin = kTLSProtocol1;
+ ::SSLProtocol protoMax = kTLSProtocol12;
+ CFUniquePtr<::CFArrayRef> certs;
+};
+
+} // namespace apple
+} // namespace ssl
+} // namespace asio
+
+#endif
diff --git a/src/mongo/util/net/ssl/context.hpp b/src/mongo/util/net/ssl/context.hpp
index 278530f5916..2a23b8454c8 100644
--- a/src/mongo/util/net/ssl/context.hpp
+++ b/src/mongo/util/net/ssl/context.hpp
@@ -34,6 +34,10 @@
#include "mongo/util/net/ssl/context_openssl.hpp"
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+
+#include "mongo/util/net/ssl/context_apple.hpp"
+
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl/context_apple.hpp b/src/mongo/util/net/ssl/context_apple.hpp
new file mode 100644
index 00000000000..3fd7f7eabde
--- /dev/null
+++ b/src/mongo/util/net/ssl/context_apple.hpp
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include "asio/detail/config.hpp"
+#include "asio/detail/noncopyable.hpp"
+#include "mongo/util/net/ssl/apple.hpp"
+#include "mongo/util/net/ssl/context_base.hpp"
+
+#include "asio/detail/push_options.hpp"
+
+namespace asio {
+namespace ssl {
+
+class context : public context_base, private noncopyable {
+public:
+ using native_handle_type = apple::Context*;
+
+ ASIO_DECL explicit context(method m) {
+ _context.protoMin = _mapProto(m);
+ _context.protoMax = _context.protoMax;
+ }
+
+ ASIO_DECL context(context&& other) = default;
+ ASIO_DECL context& operator=(context&& other) = default;
+
+ ASIO_DECL native_handle_type native_handle() {
+ return &_context;
+ }
+
+private:
+ static ::SSLProtocol _mapProto(method m) {
+ switch (m) {
+ case context::tlsv1:
+ case context::tlsv1_client:
+ case context::tlsv1_server:
+ return ::kTLSProtocol1;
+ case context::tlsv11:
+ case context::tlsv11_client:
+ case context::tlsv11_server:
+ return ::kTLSProtocol11;
+ case context::tlsv12:
+ case context::tlsv12_client:
+ case context::tlsv12_server:
+ default:
+ return ::kTLSProtocol12;
+ }
+ }
+
+ apple::Context _context;
+};
+
+} // namespace ssl
+} // namespace asio
+
+#include "asio/detail/pop_options.hpp"
diff --git a/src/mongo/util/net/ssl/detail/engine.hpp b/src/mongo/util/net/ssl/detail/engine.hpp
index 80fc21c5daf..9beb6724b7e 100644
--- a/src/mongo/util/net/ssl/detail/engine.hpp
+++ b/src/mongo/util/net/ssl/detail/engine.hpp
@@ -34,6 +34,10 @@
#include "mongo/util/net/ssl/detail/engine_openssl.hpp"
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+
+#include "mongo/util/net/ssl/detail/engine_apple.hpp"
+
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl/detail/engine_apple.hpp b/src/mongo/util/net/ssl/detail/engine_apple.hpp
new file mode 100644
index 00000000000..69b027ceaa2
--- /dev/null
+++ b/src/mongo/util/net/ssl/detail/engine_apple.hpp
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <deque>
+
+#include "asio/buffer.hpp"
+#include "asio/detail/config.hpp"
+#include "mongo/util/net/ssl/apple.hpp"
+#include "mongo/util/net/ssl/context_apple.hpp"
+#include "mongo/util/net/ssl/stream_base.hpp"
+
+#include "asio/detail/push_options.hpp"
+
+namespace asio {
+namespace ssl {
+namespace detail {
+
+class engine {
+public:
+ using native_handle_type = ::SSLContextRef;
+ enum want {
+ want_input_and_retry = -2,
+ want_output_and_retry = -1,
+ want_nothing = 0,
+ want_output = 1
+ };
+
+ ASIO_DECL explicit engine(context::native_handle_type context);
+
+ ASIO_DECL native_handle_type native_handle() {
+ return _ssl.get();
+ }
+
+ ASIO_DECL want handshake(stream_base::handshake_type type, asio::error_code& ec);
+
+ ASIO_DECL want shutdown(asio::error_code& ec);
+
+ ASIO_DECL want write(const asio::const_buffer& data,
+ asio::error_code& ec,
+ std::size_t& bytes_transferred);
+
+ ASIO_DECL want read(const asio::mutable_buffer& data,
+ asio::error_code& ec,
+ std::size_t& bytes_transferred);
+
+ ASIO_DECL asio::mutable_buffer get_output(const asio::mutable_buffer& data);
+
+ ASIO_DECL asio::const_buffer put_input(const asio::const_buffer& data);
+
+ ASIO_DECL const asio::error_code& map_error_code(asio::error_code& ec) const;
+
+private:
+ engine(const engine&) = delete;
+ engine& operator=(const engine&) = delete;
+ bool _initSSL(stream_base::handshake_type type, asio::error_code& ec);
+ static ::OSStatus read_func(::SSLConnectionRef ctx, void* data, size_t* data_len);
+ static ::OSStatus write_func(::SSLConnectionRef ctx, const void* data, size_t* data_len);
+ want wouldBlock() const;
+
+ apple::CFUniquePtr<native_handle_type> _ssl;
+ apple::CFUniquePtr<::CFArrayRef> _certs;
+ apple::CFUniquePtr<::CFArrayRef> _ca;
+ ::SSLProtocol _protoMin, _protoMax;
+ std::deque<char> _inbuf;
+ std::deque<char> _outbuf;
+};
+
+} // namespace detail
+} // namespace ssl
+} // namespace asio
+
+#include "asio/detail/pop_options.hpp"
+
+#if defined(ASIO_HEADER_ONLY)
+#include "mongo/util/net/ssl/detail/impl/engine_apple.ipp"
+#endif // defined(ASIO_HEADER_ONLY)
diff --git a/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp b/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp
new file mode 100644
index 00000000000..25abd1a70bb
--- /dev/null
+++ b/src/mongo/util/net/ssl/detail/impl/engine_apple.ipp
@@ -0,0 +1,347 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork
+
+#include "asio/detail/config.hpp"
+
+#include "asio/detail/push_options.hpp"
+#include "asio/detail/throw_error.hpp"
+#include "asio/error.hpp"
+
+#include "mongo/util/log.h"
+#include "mongo/util/mongoutils/str.h"
+#include "mongo/util/net/ssl/apple.hpp"
+#include "mongo/util/net/ssl/detail/engine.hpp"
+#include "mongo/util/net/ssl/error.hpp"
+
+namespace asio {
+namespace ssl {
+namespace detail {
+
+namespace {
+
+std::ostringstream& operator<<(std::ostringstream& ss, ::OSStatus status) {
+ apple::CFUniquePtr<::CFStringRef> errstr(::SecCopyErrorMessageString(status, nullptr));
+ if (!errstr) {
+ ss << "Unknown Error: " << static_cast<int>(status);
+ return ss;
+ }
+ const auto len = ::CFStringGetMaximumSizeForEncoding(::CFStringGetLength(errstr.get()),
+ ::kCFStringEncodingUTF8);
+ std::string ret;
+ ret.resize(len + 1);
+ if (!::CFStringGetCString(errstr.get(), &ret[0], len, ::kCFStringEncodingUTF8)) {
+ ss << "Unknown Error: " << static_cast<int>(status);
+ return ss;
+ }
+
+ ret.resize(strlen(ret.c_str()));
+ ss << ret;
+ return ss;
+}
+
+const class osstatus_category : public error_category {
+public:
+ const char* name() const noexcept final {
+ return "Secure.Transport";
+ }
+
+ std::string message(int value) const noexcept final {
+ const auto status = static_cast<::OSStatus>(value);
+ return mongo::str::stream() << "Secure.Transport: " << status;
+ }
+} OSStatus_category;
+
+asio::error_code errorCode(::OSStatus status) {
+ return asio::error_code(static_cast<int>(status), OSStatus_category);
+}
+
+/**
+ * Verify that an SSL session is ready for I/O (state: Connected).
+ * In all other states, asio should be speaking to the socket directly.
+ */
+bool verifyConnected(::SSLContextRef ssl, asio::error_code* ec) {
+ auto state = ::kSSLAborted;
+ auto status = ::SSLGetSessionState(ssl, &state);
+ if (status != ::errSecSuccess) {
+ // Unable to determine session state.
+ *ec = errorCode(status);
+ return false;
+ }
+ switch (state) {
+ case ::kSSLIdle:
+ *ec = asio::error::not_connected;
+ return false;
+ case ::kSSLHandshake:
+ *ec = asio::error::in_progress;
+ return false;
+ case ::kSSLConnected:
+ return true;
+ case ::kSSLClosed:
+ *ec = asio::error::shut_down;
+ return false;
+ case ::kSSLAborted:
+ *ec = asio::error::connection_aborted;
+ return false;
+ default:
+ // Undefined state, call it an internal error.
+ *ec = errorCode(::errSSLInternal);
+ return false;
+ }
+}
+
+} // namespace
+
+engine::engine(context::native_handle_type context) {
+ if (context) {
+ if (context->certs) {
+ ::CFRetain(context->certs.get());
+ _certs.reset(context->certs.get());
+ }
+ _protoMin = context->protoMin;
+ _protoMax = context->protoMax;
+ } else {
+ apple::Context def;
+ _protoMin = def.protoMin;
+ _protoMax = def.protoMax;
+ }
+}
+
+bool engine::_initSSL(stream_base::handshake_type type, asio::error_code& ec) {
+ if (_ssl) {
+ return true;
+ }
+
+ const auto side = (type == stream_base::client) ? ::kSSLClientSide : ::kSSLServerSide;
+ _ssl.reset(::SSLCreateContext(nullptr, side, ::kSSLStreamType));
+ if (!_ssl) {
+ mongo::error() << "Failed allocating SSLContext";
+ ec = errorCode(::errSSLInternal);
+ return false;
+ }
+
+ auto status = ::SSLSetConnection(_ssl.get(), static_cast<void*>(this));
+
+ // TODO: ::SSLSetPeerDomainName()
+
+ if (_certs && (status == ::errSecSuccess)) {
+ status = ::SSLSetCertificate(_ssl.get(), _certs.get());
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetPeerID(_ssl.get(), _ssl.get(), sizeof(native_handle_type));
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetIOFuncs(_ssl.get(), read_func, write_func);
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetProtocolVersionMin(_ssl.get(), _protoMin);
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetProtocolVersionMax(_ssl.get(), _protoMax);
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetClientSideAuthenticate(_ssl.get(), ::kTryAuthenticate);
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetSessionOption(_ssl.get(), ::kSSLSessionOptionBreakOnServerAuth, true);
+ }
+
+ if (status == ::errSecSuccess) {
+ status = ::SSLSetSessionOption(_ssl.get(), ::kSSLSessionOptionBreakOnClientAuth, true);
+ }
+
+ if (status != ::errSecSuccess) {
+ _ssl.reset(nullptr);
+ ec = errorCode(status);
+ return false;
+ }
+
+ return true;
+}
+
+engine::want engine::handshake(stream_base::handshake_type type, asio::error_code& ec) {
+ if (!_initSSL(type, ec)) {
+ // Error happened, ec has been set.
+ return want::want_nothing;
+ }
+
+ // We use BreakOnClientAuth and BreakOnServerAuth above to
+ // convince the OS not to validate the certs for us.
+ // In practice, we'll be validating the peer in ssl_manager_apple.cpp later.
+ // As a side effect, we have to call SSLHandshake up to three times.
+ // Breaking once for client auth, then for server auth, and finally on completion.
+ ::OSStatus status;
+ do {
+ status = ::SSLHandshake(_ssl.get());
+ } while ((status == ::errSSLServerAuthCompleted) || (status == ::errSSLClientAuthCompleted));
+
+ if (status == ::errSSLWouldBlock) {
+ return wouldBlock();
+ }
+
+ if (status != ::errSecSuccess) {
+ _ssl.reset(nullptr);
+ ec = errorCode(status);
+ return want::want_nothing;
+ }
+
+ return _outbuf.size() ? want::want_output : want::want_nothing;
+}
+
+engine::want engine::shutdown(asio::error_code& ec) {
+ if (_ssl) {
+ const auto status = ::SSLClose(_ssl.get());
+ if (status == ::errSSLWouldBlock) {
+ return wouldBlock();
+ }
+ if (status == ::errSecSuccess) {
+ _ssl.reset(nullptr);
+ } else {
+ ec = errorCode(status);
+ }
+ } else {
+ mongo::error() << "SSL connection already shut down";
+ ec = errorCode(::errSSLInternal);
+ }
+ return want::want_nothing;
+}
+
+const asio::error_code& engine::map_error_code(asio::error_code& ec) const {
+ if (ec != asio::error::eof) {
+ return ec;
+ }
+
+ if (_inbuf.size() || _outbuf.size()) {
+ ec = asio::ssl::error::stream_truncated;
+ return ec;
+ }
+
+ invariant(_ssl);
+ auto state = ::kSSLAborted;
+ const auto status = ::SSLGetSessionState(_ssl.get(), &state);
+ if (status != ::errSecSuccess) {
+ ec = errorCode(status);
+ return ec;
+ }
+
+ if (state == ::kSSLConnected) {
+ ec = asio::ssl::error::stream_truncated;
+ return ec;
+ }
+
+ return ec;
+}
+
+engine::want engine::write(const asio::const_buffer& data,
+ asio::error_code& ec,
+ std::size_t& bytes_transferred) {
+ if (!verifyConnected(_ssl.get(), &ec)) {
+ return want::want_nothing;
+ }
+ const auto status = ::SSLWrite(_ssl.get(), data.data(), data.size(), &bytes_transferred);
+ if (status == ::errSSLWouldBlock) {
+ return (bytes_transferred < data.size()) ? want::want_output_and_retry : want::want_nothing;
+ }
+ if (status != ::errSecSuccess) {
+ ec = errorCode(status);
+ }
+ return _outbuf.size() ? want::want_output : want::want_nothing;
+}
+
+asio::mutable_buffer engine::get_output(const asio::mutable_buffer& data) {
+ const auto len = std::min<size_t>(data.size(), _outbuf.size());
+ if (len > 0) {
+ auto* p = const_cast<char*>(static_cast<const char*>(data.data()));
+ std::copy(_outbuf.begin(), _outbuf.begin() + len, p);
+ _outbuf.erase(_outbuf.begin(), _outbuf.begin() + len);
+ }
+
+ return asio::mutable_buffer(data.data(), len);
+}
+
+::OSStatus engine::write_func(::SSLConnectionRef ctx, const void* data, size_t* data_len) {
+ auto* this_ = const_cast<engine*>(static_cast<const engine*>(ctx));
+ const auto* p = static_cast<const char*>(data);
+ this_->_outbuf.insert(this_->_outbuf.end(), p, p + *data_len);
+ return ::errSecSuccess;
+}
+
+engine::want engine::read(const asio::mutable_buffer& data,
+ asio::error_code& ec,
+ std::size_t& bytes_transferred) {
+ if (!verifyConnected(_ssl.get(), &ec)) {
+ return want::want_nothing;
+ }
+ const auto status = ::SSLRead(_ssl.get(), data.data(), data.size(), &bytes_transferred);
+ if ((status != ::errSSLWouldBlock) && (status != ::errSecSuccess)) {
+ ec = errorCode(status);
+ }
+ return bytes_transferred ? want::want_nothing : wouldBlock();
+}
+
+asio::const_buffer engine::put_input(const asio::const_buffer& data) {
+ const auto* p = static_cast<const char*>(data.data());
+ _inbuf.insert(_inbuf.end(), p, p + data.size());
+ return asio::buffer(data + data.size());
+}
+
+::OSStatus engine::read_func(::SSLConnectionRef ctx, void* data, size_t* data_len) {
+ ::OSStatus ret = ::errSecSuccess;
+ auto* this_ = const_cast<engine*>(static_cast<const engine*>(ctx));
+ if (*data_len > this_->_inbuf.size()) {
+ // If we're able to 100% satisfy the read request Secure Transport made,
+ // then we should ultimately signal that the read is incomplete.
+ ret = ::errSSLWouldBlock;
+ }
+ *data_len = std::min<size_t>(*data_len, this_->_inbuf.size());
+ if (*data_len > 0) {
+ std::copy(
+ this_->_inbuf.begin(), this_->_inbuf.begin() + *data_len, static_cast<char*>(data));
+ this_->_inbuf.erase(this_->_inbuf.begin(), this_->_inbuf.begin() + *data_len);
+ }
+ return ret;
+}
+
+engine::want engine::wouldBlock() const {
+ return _outbuf.empty() ? want::want_input_and_retry : want::want_output_and_retry;
+}
+
+#include "asio/detail/pop_options.hpp"
+
+} // namespace detail
+} // namespace ssl
+} // namespace asio
diff --git a/src/mongo/util/net/ssl/detail/stream_core.hpp b/src/mongo/util/net/ssl/detail/stream_core.hpp
index 21841fec2b9..1740a5bda29 100644
--- a/src/mongo/util/net/ssl/detail/stream_core.hpp
+++ b/src/mongo/util/net/ssl/detail/stream_core.hpp
@@ -22,7 +22,9 @@
#else // defined(ASIO_HAS_BOOST_DATE_TIME)
#include "asio/steady_timer.hpp"
#endif // defined(ASIO_HAS_BOOST_DATE_TIME)
+
#include "asio/buffer.hpp"
+#include "mongo/util/net/ssl/apple.hpp"
#include "mongo/util/net/ssl/detail/engine.hpp"
#include "asio/detail/push_options.hpp"
@@ -40,6 +42,8 @@ struct stream_core {
stream_core(SCHANNEL_CRED* context, asio::io_context& io_context)
#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_OPENSSL
stream_core(SSL_CTX* context, asio::io_context& io_context)
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+ stream_core(apple::Context* context, asio::io_context& io_context)
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl/impl/error.ipp b/src/mongo/util/net/ssl/impl/error.ipp
index 60b8f0da177..e2c6bea061e 100644
--- a/src/mongo/util/net/ssl/impl/error.ipp
+++ b/src/mongo/util/net/ssl/impl/error.ipp
@@ -15,8 +15,12 @@
#pragma once
#endif // defined(_MSC_VER) && (_MSC_VER >= 1200)
+#include <string>
+
+#include "asio/detail/assert.hpp"
#include "asio/detail/config.hpp"
#include "mongo/util/errno_util.h"
+#include "mongo/util/net/ssl/apple.hpp"
#include "mongo/util/net/ssl/error.hpp"
#include "asio/detail/push_options.hpp"
@@ -40,6 +44,12 @@ public:
const char* s = ::ERR_reason_error_string(value);
return s ? s : "asio.ssl error";
}
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+ std::string message(int value) const {
+ // engine_apple produces osstatus_errorcategory messages.
+ ASIO_ASSERT(false);
+ return "asio.ssl error";
+ }
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl/impl/src.hpp b/src/mongo/util/net/ssl/impl/src.hpp
index f5bb866efee..5e300c2147e 100644
--- a/src/mongo/util/net/ssl/impl/src.hpp
+++ b/src/mongo/util/net/ssl/impl/src.hpp
@@ -33,6 +33,11 @@
#include "mongo/util/net/ssl/impl/context_openssl.ipp"
#include "mongo/util/net/ssl/impl/error.ipp"
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+
+#include "mongo/util/net/ssl/detail/impl/engine_apple.ipp"
+#include "mongo/util/net/ssl/impl/error.ipp"
+
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl/stream.hpp b/src/mongo/util/net/ssl/stream.hpp
index d44bdc4bd08..df933c0fe55 100644
--- a/src/mongo/util/net/ssl/stream.hpp
+++ b/src/mongo/util/net/ssl/stream.hpp
@@ -22,6 +22,7 @@
#include "asio/detail/handler_type_requirements.hpp"
#include "asio/detail/noncopyable.hpp"
#include "asio/detail/type_traits.hpp"
+#include "mongo/util/net/ssl/apple.hpp"
#include "mongo/util/net/ssl/context.hpp"
#include "mongo/util/net/ssl/detail/buffered_handshake_op.hpp"
#include "mongo/util/net/ssl/detail/handshake_op.hpp"
@@ -67,6 +68,8 @@ public:
typedef PCtxtHandle native_handle_type;
#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_OPENSSL
typedef SSL* native_handle_type;
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+ typedef ::SSLContextRef native_handle_type;
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp
index 82207251d6e..1e84d75242a 100644
--- a/src/mongo/util/net/ssl_manager.cpp
+++ b/src/mongo/util/net/ssl_manager.cpp
@@ -65,6 +65,14 @@ ExportedServerParameter<std::string, ServerParameterType::kStartupOnly>
setDiffieHellmanParameterPEMFile(ServerParameterSet::getGlobal(),
"opensslDiffieHellmanParameters",
&sslGlobalParams.sslPEMTempDHParam);
+
+std::string removeFQDNRoot(std::string name) {
+ if (name.back() == '.') {
+ name.pop_back();
+ }
+ return name;
+};
+
} // namespace
SSLPeerInfo& SSLPeerInfo::forSession(const transport::SessionHandle& session) {
@@ -463,3 +471,24 @@ StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(ConstDataRange cdrExten
#endif
} // namespace mongo
+
+#ifdef MONGO_CONFIG_SSL
+// TODO SERVER-11601 Use NFC Unicode canonicalization
+bool mongo::hostNameMatchForX509Certificates(std::string nameToMatch, std::string certHostName) {
+ nameToMatch = removeFQDNRoot(std::move(nameToMatch));
+ certHostName = removeFQDNRoot(std::move(certHostName));
+
+ if (certHostName.size() < 2) {
+ return false;
+ }
+
+ // match wildcard DNS names
+ if (certHostName[0] == '*' && certHostName[1] == '.') {
+ // allow name.example.com if the cert is *.example.com, '*' does not match '.'
+ const char* subName = strchr(nameToMatch.c_str(), '.');
+ return subName && !strcasecmp(certHostName.c_str() + 1, subName);
+ } else {
+ return !strcasecmp(nameToMatch.c_str(), certHostName.c_str());
+ }
+}
+#endif
diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h
index 9e0edccb649..fc171f8612c 100644
--- a/src/mongo/util/net/ssl_manager.h
+++ b/src/mongo/util/net/ssl_manager.h
@@ -40,6 +40,7 @@
#include "mongo/bson/bsonobj.h"
#include "mongo/util/decorable.h"
#include "mongo/util/net/sock.h"
+#include "mongo/util/net/ssl/apple.hpp"
#include "mongo/util/net/ssl_types.h"
#include "mongo/util/time_support.h"
@@ -67,6 +68,9 @@ typedef SSL* SSLConnectionType;
#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_WINDOWS
typedef SCHANNEL_CRED* SSLContextType;
typedef PCtxtHandle SSLConnectionType;
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+typedef asio::ssl::apple::Context* SSLContextType;
+typedef SSLContextRef SSLConnectionType;
#else
#error "Unknown SSL Provider"
#endif
diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp
new file mode 100644
index 00000000000..bca51fa9a40
--- /dev/null
+++ b/src/mongo/util/net/ssl_manager_apple.cpp
@@ -0,0 +1,1171 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork
+
+#include "mongo/platform/basic.h"
+
+#include <boost/optional/optional.hpp>
+#include <fstream>
+#include <stdlib.h>
+
+#include "mongo/base/checked_cast.h"
+#include "mongo/base/init.h"
+#include "mongo/base/initializer_context.h"
+#include "mongo/base/status.h"
+#include "mongo/base/status_with.h"
+#include "mongo/platform/random.h"
+#include "mongo/stdx/memory.h"
+#include "mongo/util/base64.h"
+#include "mongo/util/concurrency/mutex.h"
+#include "mongo/util/log.h"
+#include "mongo/util/net/private/ssl_expiration.h"
+#include "mongo/util/net/socket_exception.h"
+#include "mongo/util/net/ssl/apple.hpp"
+#include "mongo/util/net/ssl_manager.h"
+#include "mongo/util/net/ssl_options.h"
+
+using asio::ssl::apple::CFUniquePtr;
+
+/* This API appears in the Security framework even though
+ * the SecIdentity.h header doesn't reference it.
+ *
+ * Use it explicitly for turning Cert/Key pairs into Identities
+ * because it's way cheaper than going via keychain and has the
+ * handy property of not causing difficult to diagnose heisenbugs.
+ */
+extern "C" SecIdentityRef SecIdentityCreate(CFAllocatorRef, SecCertificateRef, SecKeyRef);
+
+namespace mongo {
+
+namespace {
+
+// CFAbsoluteTime and X.509 is relative to Jan 1 2001 00:00:00 GMT
+// Unix Epoch (and thereby Date_t) is relative to Jan 1, 1970 00:00:00 GMT
+static const ::CFAbsoluteTime k20010101_000000_GMT = 978307200;
+
+StatusWith<std::string> toString(::CFStringRef str) {
+ const auto len =
+ ::CFStringGetMaximumSizeForEncoding(::CFStringGetLength(str), ::kCFStringEncodingUTF8);
+ std::string ret;
+ ret.resize(len + 1);
+ if (!::CFStringGetCString(str, &ret[0], len, ::kCFStringEncodingUTF8)) {
+ return Status(ErrorCodes::InternalError, "Unable to convert CoreFoundation string");
+ }
+ ret.resize(strlen(ret.c_str()));
+ return ret;
+}
+
+// Never actually errors, but use StatusWith to be
+// consistent with other toString() signatures.
+StatusWith<std::string> toString(::CFDataRef data) {
+ const auto len = ::CFDataGetLength(data);
+ const auto* p = (const char*)::CFDataGetBytePtr(data);
+ return std::string(p, len);
+}
+
+StatusWith<std::string> toString(::OSStatus status) {
+ CFUniquePtr<::CFStringRef> errstr(::SecCopyErrorMessageString(status, nullptr));
+ if (!errstr) {
+ return Status(ErrorCodes::InternalError, "Unable to convert OSStatus");
+ }
+ auto ret = toString(errstr.get());
+ return ret;
+}
+
+// CFTypeRef is actually just `void*`.
+// So while we could be polymorphic with the other toString() methods,
+// it's basically asking for a hard to diagnose type error.
+StatusWith<std::string> stringFromCFType(::CFTypeRef val) {
+ const auto type = val ? ::CFGetTypeID(val) : ((CFTypeID)-1);
+ if (type == ::CFStringGetTypeID()) {
+ return toString(static_cast<::CFStringRef>(val));
+ } else if (type == ::CFDataGetTypeID()) {
+ return toString(static_cast<::CFDataRef>(val));
+ } else {
+ return Status(ErrorCodes::BadValue, "Value is not translatable to string");
+ }
+}
+
+std::ostringstream& operator<<(std::ostringstream& ss, ::CFStringRef str) {
+ auto swStr = toString(str);
+ if (swStr.isOK()) {
+ ss << swStr.getValue();
+ } else {
+ ss << "Unknown error";
+ }
+ return ss;
+}
+
+std::ostringstream& operator<<(std::ostringstream& ss, ::OSStatus status) {
+ static_assert(std::is_signed<int>::value == std::is_signed<::OSStatus>::value,
+ "Must cast status to same signedness");
+ static_assert(sizeof(int) >= sizeof(::OSStatus), "Must cast status to same or wider type");
+ auto swStr = toString(status);
+ if (swStr.isOK()) {
+ ss << swStr.getValue();
+ } else {
+ ss << "Unknown error: " << static_cast<int>(status);
+ }
+ return ss;
+}
+
+std::ostringstream& operator<<(std::ostringstream& ss, ::CFErrorRef error) {
+ std::string comma;
+
+ CFUniquePtr<::CFStringRef> desc(::CFErrorCopyDescription(error));
+ if (desc) {
+ ss << comma << desc.get();
+ comma = ", ";
+ }
+
+ CFUniquePtr<::CFStringRef> reason(::CFErrorCopyFailureReason(error));
+ if (reason) {
+ ss << comma << reason.get();
+ comma = ", ";
+ }
+
+ CFUniquePtr<::CFStringRef> suggest(::CFErrorCopyRecoverySuggestion(error));
+ if (suggest) {
+ ss << comma << suggest.get();
+ comma = ", ";
+ }
+
+ auto code = ::CFErrorGetCode(error);
+ if (code) {
+ ss << comma << "Code: " << code;
+ }
+
+ return ss;
+}
+
+void uassertOSStatusOK(::OSStatus status,
+ ErrorCodes::Error code = ErrorCodes::InvalidSSLConfiguration) {
+ if (status == ::errSecSuccess) {
+ return;
+ }
+ auto swMsg = toString(status);
+ if (!swMsg.isOK()) {
+ uasserted(code, str::stream() << "Unknown SSL error" << static_cast<int>(status));
+ }
+ uasserted(code, swMsg.getValue());
+}
+
+void uassertOSStatusOK(::OSStatus status, SocketErrorKind kind) {
+ if (status == ::errSecSuccess) {
+ return;
+ }
+ auto swMsg = toString(status);
+ if (!swMsg.isOK()) {
+ throwSocketError(kind, str::stream() << "Unknown SSL error" << static_cast<int>(status));
+ }
+ throwSocketError(kind, swMsg.getValue());
+}
+
+StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(::SecCertificateRef peer) {
+ // TODO: ASN.1 parsing
+ return stdx::unordered_set<RoleName>();
+}
+
+bool isUnixDomainSocket(const std::string& hostname) {
+ return end(hostname) != std::find(begin(hostname), end(hostname), '/');
+}
+
+::OSStatus posixErrno(int err) {
+ switch (err) {
+ case EAGAIN:
+ return ::errSSLWouldBlock;
+ case ENOENT:
+ return ::errSSLClosedGraceful;
+ case ECONNRESET:
+ return ::errSSLClosedAbort;
+ default:
+ return ::errSSLInternal;
+ }
+}
+
+namespace detail {
+template <typename T>
+struct CFTypeMap {};
+template <>
+struct CFTypeMap<::CFStringRef> {
+ static constexpr StringData typeName = "string"_sd;
+ static ::CFTypeID type() {
+ return ::CFStringGetTypeID();
+ }
+};
+constexpr StringData CFTypeMap<::CFStringRef>::typeName;
+template <>
+struct CFTypeMap<::CFNumberRef> {
+ static constexpr StringData typeName = "number"_sd;
+ static ::CFTypeID type() {
+ return ::CFNumberGetTypeID();
+ }
+};
+constexpr StringData CFTypeMap<::CFNumberRef>::typeName;
+template <>
+struct CFTypeMap<::CFArrayRef> {
+ static constexpr StringData typeName = "array"_sd;
+ static ::CFTypeID type() {
+ return ::CFArrayGetTypeID();
+ }
+};
+constexpr StringData CFTypeMap<::CFArrayRef>::typeName;
+template <>
+struct CFTypeMap<::CFDictionaryRef> {
+ static constexpr StringData typeName = "dictionary"_sd;
+ static ::CFTypeID type() {
+ return ::CFDictionaryGetTypeID();
+ }
+};
+constexpr StringData CFTypeMap<::CFDictionaryRef>::typeName;
+} // namespace detail
+
+template <typename T>
+StatusWith<T> extractDictionaryValue(::CFDictionaryRef dict, ::CFStringRef key) {
+ const auto badValue = [key](StringData msg) -> Status {
+ auto swKey = toString(key);
+ if (!swKey.isOK()) {
+ return {ErrorCodes::InvalidSSLConfiguration, msg};
+ }
+ return {ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << msg << " for key'" << swKey.getValue() << "'"};
+ };
+
+ auto val = ::CFDictionaryGetValue(dict, key);
+ if (!val) {
+ return badValue("Missing value");
+ }
+ if (::CFGetTypeID(val) != detail::CFTypeMap<T>::type()) {
+ return badValue(str::stream() << "Value is not a " << detail::CFTypeMap<T>::typeName);
+ }
+ return reinterpret_cast<T>(val);
+}
+
+StatusWith<std::string> mapSubjectLabel(::CFStringRef label) {
+ if (!::CFStringCompare(label, ::kSecOIDCommonName, 0)) {
+ return {"CN"};
+ } else if (!::CFStringCompare(label, ::kSecOIDCountryName, 0)) {
+ return {"C"};
+ } else if (!::CFStringCompare(label, ::kSecOIDStateProvinceName, 0)) {
+ return {"ST"};
+ } else if (!::CFStringCompare(label, ::kSecOIDLocalityName, 0)) {
+ return {"L"};
+ } else if (!::CFStringCompare(label, ::kSecOIDOrganizationName, 0)) {
+ return {"O"};
+ } else if (!::CFStringCompare(label, ::kSecOIDOrganizationalUnitName, 0)) {
+ return {"OU"};
+ } else if (!::CFStringCompare(label, ::kSecOIDStreetAddress, 0)) {
+ return {"STREET"};
+ } else if (!::CFStringCompare(label, CFSTR("0.9.2342.19200300.100.1.25"), 0)) {
+ return {"DC"};
+ } else if (!::CFStringCompare(label, CFSTR("0.9.2342.19200300.100.1.1"), 0)) {
+ return {"UID"};
+ }
+ // RFC 2253 specifies #hexstring encoding for unknown OIDs,
+ // however for backward compatibility purposes, we omit these.
+ return {ErrorCodes::InvalidSSLConfiguration, str::stream() << "Unknown OID: " << label};
+}
+
+// Encode a raw DER subject sequence into a human readable subject name (RFC 2253).
+StatusWith<std::string> extractSubjectName(::CFDictionaryRef dict) {
+ auto swSubject = extractDictionaryValue<::CFDictionaryRef>(dict, ::kSecOIDX509V1SubjectName);
+ if (!swSubject.isOK()) {
+ return swSubject.getStatus();
+ }
+
+ auto swElems =
+ extractDictionaryValue<::CFArrayRef>(swSubject.getValue(), ::kSecPropertyKeyValue);
+ if (!swElems.isOK()) {
+ return swElems.getStatus();
+ }
+
+ auto elems = swElems.getValue();
+ const auto nElems = ::CFArrayGetCount(elems);
+ StringBuilder ret;
+ for (auto i = nElems; i; --i) {
+ auto elem = reinterpret_cast<::CFDictionaryRef>(::CFArrayGetValueAtIndex(elems, i - 1));
+ invariant(elem);
+ if (::CFGetTypeID(elem) != ::CFDictionaryGetTypeID()) {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ "Subject name element is not a dictionary"};
+ }
+
+ auto swLabel = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyLabel);
+ if (!swLabel.isOK()) {
+ return swLabel.getStatus();
+ }
+ auto swLabelStr = mapSubjectLabel(swLabel.getValue());
+ if (!swLabelStr.isOK()) {
+ return swLabelStr.getStatus();
+ }
+
+ auto swValue = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyValue);
+ if (!swValue.isOK()) {
+ return swValue.getStatus();
+ }
+ auto swValueStr = toString(swValue.getValue());
+ if (!swValueStr.isOK()) {
+ return swValueStr.getStatus();
+ }
+
+ ret << swLabelStr.getValue() << '=' << swValueStr.getValue();
+ if (i > 1) {
+ ret << ",";
+ }
+ }
+ return ret.str();
+}
+
+StatusWith<mongo::Date_t> extractValidityDate(::CFDictionaryRef dict,
+ ::CFStringRef oid,
+ StringData name) {
+ auto swVal = extractDictionaryValue<::CFDictionaryRef>(dict, oid);
+ if (!swVal.isOK()) {
+ return swVal.getStatus();
+ }
+
+ auto swNum = extractDictionaryValue<::CFNumberRef>(swVal.getValue(), ::kSecPropertyKeyValue);
+ if (!swNum.isOK()) {
+ return swNum.getStatus();
+ }
+
+ int64_t dateval = 0;
+ if (!::CFNumberGetValue(swNum.getValue(), ::kCFNumberSInt64Type, &dateval)) {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << "Certificate contains invalid " << name
+ << ": OID contains invalid numeric value"};
+ }
+
+ return Date_t::fromMillisSinceEpoch((k20010101_000000_GMT + dateval) * 1000);
+}
+
+StatusWith<std::vector<std::string>> extractSubjectAlternateNames(::CFDictionaryRef dict) {
+ const auto badValue = [](StringData msg) -> Status {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << "Certificate contains invalid SAN: " << msg};
+ };
+ auto swSANDict = extractDictionaryValue<::CFDictionaryRef>(dict, ::kSecOIDSubjectAltName);
+ if (!swSANDict.isOK()) {
+ return swSANDict.getStatus();
+ }
+
+ auto sanDict = swSANDict.getValue();
+ auto swList = extractDictionaryValue<::CFArrayRef>(sanDict, ::kSecPropertyKeyValue);
+ if (!swList.isOK()) {
+ return swList.getStatus();
+ }
+
+ std::vector<std::string> ret;
+ auto list = swList.getValue();
+ const auto count = ::CFArrayGetCount(list);
+ for (::CFIndex i = 0; i < count; ++i) {
+ auto elemval = ::CFArrayGetValueAtIndex(list, i);
+ invariant(elemval);
+ if (::CFGetTypeID(elemval) != ::CFDictionaryGetTypeID()) {
+ return badValue("Invalid list element");
+ }
+
+ auto elem = reinterpret_cast<::CFDictionaryRef>(elemval);
+ auto swLabel = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyLabel);
+ if (!swLabel.isOK()) {
+ return swLabel.getStatus();
+ }
+ if (::CFStringCompare(swLabel.getValue(), CFSTR("DNS Name"), ::kCFCompareCaseInsensitive) !=
+ ::kCFCompareEqualTo) {
+ // Skip other elements, e.g. 'Critical'
+ continue;
+ }
+ auto swName = extractDictionaryValue<::CFStringRef>(elem, ::kSecPropertyKeyValue);
+ if (!swName.isOK()) {
+ return swName.getStatus();
+ }
+ auto swNameStr = toString(swName.getValue());
+ if (!swNameStr.isOK()) {
+ return swNameStr.getStatus();
+ }
+ ret.push_back(swNameStr.getValue());
+ }
+ return ret;
+}
+
+enum LoadPEMMode {
+ kLoadPEMBindIdentities = true,
+ kLoadPEMStripKeys = false,
+};
+/**
+ * Load a PEM encoded file from disk.
+ * This file may contain multiple PEM blocks (e.g. a private key and one or more certificates).
+ * Because SecItemImport loads all items as-is, we must manually attempt to pair up
+ * corresponding Certificates and Keys.
+ * This is done using a temporary Keychain and looping through the results.
+ *
+ * Depending on the value passed for <mode> any SecKey instances present will be
+ * either discarded, or combined with matching SecCertificates to make SecIdentities.
+ * Unbound certificates will remain in the CFArray as-is.
+ */
+StatusWith<CFUniquePtr<::CFArrayRef>> loadPEM(const std::string& keyfilepath,
+ const std::string& passphrase = "",
+ const LoadPEMMode mode = kLoadPEMBindIdentities) {
+ const auto retFail = [&keyfilepath, &passphrase](const std::string& msg = "") {
+ return Status(ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << "Unable to load PEM from '" << keyfilepath << "'"
+ << (passphrase.empty() ? "" : " with passphrase: ")
+ << msg);
+ };
+
+ std::ifstream pemFile(keyfilepath, std::ios::binary);
+ if (!pemFile.is_open()) {
+ return retFail("Failed opening file");
+ }
+
+ std::vector<uint8_t> pemdata((std::istreambuf_iterator<char>(pemFile)),
+ std::istreambuf_iterator<char>());
+ CFUniquePtr<CFDataRef> cfdata(::CFDataCreate(nullptr, pemdata.data(), pemdata.size()));
+ invariant(cfdata);
+ pemdata.clear();
+
+ CFUniquePtr<CFDataRef> cfpass;
+ if (!passphrase.empty()) {
+ cfpass.reset(::CFDataCreate(
+ nullptr, reinterpret_cast<const uint8_t*>(passphrase.c_str()), passphrase.size()));
+ }
+ ::SecItemImportExportKeyParameters params = {
+ SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION, 0, cfpass.get(),
+ };
+
+ CFUniquePtr<CFStringRef> cfkeyfile(
+ ::CFStringCreateWithCString(nullptr, keyfilepath.c_str(), ::kCFStringEncodingUTF8));
+ auto format = ::kSecFormatUnknown;
+ auto type = ::kSecItemTypeUnknown;
+ ::CFArrayRef certs = nullptr;
+ auto status = ::SecItemImport(cfdata.get(),
+ cfkeyfile.get(),
+ &format,
+ &type,
+ ::kSecItemPemArmour,
+ &params,
+ nullptr,
+ &certs);
+ CFUniquePtr<::CFArrayRef> cfcerts(certs);
+ if ((status == ::errSecUnknownFormat) && !passphrase.empty()) {
+ // The Security framework in OSX doesn't support PKCS#8 encrypted keys
+ // using modern encryption algorithms. Give the user a hint about that.
+ return Status(ErrorCodes::InvalidSSLConfiguration,
+ "Unable to import PEM key file, possibly due to presence of PKCS#8 encrypted "
+ "key. Consider using a certificate selector or PKCS#12 instead");
+ }
+ if (status != ::errSecSuccess) {
+ return retFail(str::stream() << "Failing importing certificate(s): " << status);
+ }
+
+ auto count = ::CFArrayGetCount(cfcerts.get());
+ if ((count > 0) && (mode == kLoadPEMBindIdentities)) {
+ // Turn Certificate/Key pairs into identities.
+ CFUniquePtr<::CFMutableArrayRef> bind(
+ ::CFArrayCreateMutable(nullptr, count, &kCFTypeArrayCallBacks));
+ for (::CFIndex i = 0; i < count; ++i) {
+ auto elem = ::CFArrayGetValueAtIndex(cfcerts.get(), i);
+ invariant(elem);
+ const auto type = ::CFGetTypeID(elem);
+ if (type == ::SecIdentityGetTypeID()) {
+ // Our import had a proper identity in it, ready to go.
+ ::CFArrayAppendValue(bind.get(), elem);
+ continue;
+ }
+ if (type != ::SecCertificateGetTypeID()) {
+ continue;
+ }
+
+ // Attempt to match the certificate to a private key in the aggregate we just imported.
+ CFUniquePtr<::SecIdentityRef> cfid;
+ for (::CFIndex j = 0; j < count; ++j) {
+ auto key = ::CFArrayGetValueAtIndex(cfcerts.get(), j);
+ invariant(key);
+ if (::CFGetTypeID(key) != ::SecKeyGetTypeID()) {
+ continue;
+ }
+ auto id =
+ ::SecIdentityCreate(nullptr,
+ static_cast<::SecCertificateRef>(const_cast<void*>(elem)),
+ static_cast<::SecKeyRef>(const_cast<void*>(key)));
+ if (id) {
+ cfid.reset(id);
+ break;
+ }
+ }
+ if (cfid) {
+ ::CFArrayAppendValue(bind.get(), cfid.get());
+ } else {
+ ::CFArrayAppendValue(bind.get(), elem);
+ }
+ }
+ // Reencapsulate to allow the inner type to change.
+ cfcerts.reset(bind.release());
+ count = ::CFArrayGetCount(cfcerts.get());
+ }
+
+ if ((count > 0) && (mode == kLoadPEMStripKeys)) {
+ // Strip unpaired keys and identities.
+ CFUniquePtr<::CFMutableArrayRef> strip(
+ ::CFArrayCreateMutable(nullptr, count, &kCFTypeArrayCallBacks));
+ for (::CFIndex i = 0; i < count; ++i) {
+ auto elem = ::CFArrayGetValueAtIndex(cfcerts.get(), i);
+ if (!elem) {
+ continue;
+ }
+ const auto type = ::CFGetTypeID(elem);
+ if (type == ::SecCertificateGetTypeID()) {
+ // Preserve Certificates.
+ ::CFArrayAppendValue(strip.get(), elem);
+ continue;
+ }
+ if (type != ::SecIdentityGetTypeID()) {
+ continue;
+ }
+
+ // Extract public certificate from Identity.
+ ::SecCertificateRef cert = nullptr;
+ const auto status = ::SecIdentityCopyCertificate(
+ static_cast<::SecIdentityRef>(const_cast<void*>(elem)), &cert);
+ CFUniquePtr<::SecCertificateRef> cfcert(cert);
+ if (status != ::errSecSuccess) {
+ return {ErrorCodes::InternalError,
+ str::stream() << "Unable to extract certificate from identity: " << status};
+ }
+ ::CFArrayAppendValue(strip.get(), cfcert.get());
+ }
+ // Reencapsulate to allow the inner type to change.
+ cfcerts.reset(strip.release());
+ count = ::CFArrayGetCount(cfcerts.get());
+ }
+
+ if (count <= 0) {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << "PEM file '" << keyfilepath << "' has no certificates"};
+ }
+
+ // Rewrap the return to be the non-mutable type.
+ return std::move(cfcerts);
+}
+
+StatusWith<std::string> loadAndValidatePEM(const std::string& key,
+ const std::string& pass,
+ Date_t* expire = nullptr) {
+ auto swCerts = loadPEM(key, pass);
+ if (!swCerts.isOK()) {
+ return swCerts.getStatus();
+ }
+
+ auto certs = std::move(swCerts.getValue());
+ if (::CFArrayGetCount(certs.get()) <= 0) {
+ return {ErrorCodes::InvalidSSLConfiguration, "No certificates in certificate list"};
+ }
+
+ auto root = ::CFArrayGetValueAtIndex(certs.get(), 0);
+ if (!root || (::CFGetTypeID(root) != ::SecIdentityGetTypeID())) {
+ return {ErrorCodes::InvalidSSLConfiguration, "Root certificate not an identity pair"};
+ }
+
+ ::SecCertificateRef idcert = nullptr;
+ auto status = ::SecIdentityCopyCertificate(
+ static_cast<::SecIdentityRef>(const_cast<void*>(root)), &idcert);
+ if (status != ::errSecSuccess) {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << "Unable to get certificate from identity: " << status};
+ }
+ CFUniquePtr<::SecCertificateRef> cert(idcert);
+
+ // Fetch expiry range and full subject name.
+ CFUniquePtr<::CFMutableArrayRef> oids(
+ ::CFArrayCreateMutable(nullptr, expire ? 3 : 1, &::kCFTypeArrayCallBacks));
+ ::CFArrayAppendValue(oids.get(), ::kSecOIDX509V1SubjectName);
+ if (expire) {
+ ::CFArrayAppendValue(oids.get(), ::kSecOIDX509V1ValidityNotBefore);
+ ::CFArrayAppendValue(oids.get(), ::kSecOIDX509V1ValidityNotAfter);
+ }
+
+ ::CFErrorRef cferror = nullptr;
+ CFUniquePtr<::CFDictionaryRef> cfdict(
+ ::SecCertificateCopyValues(cert.get(), oids.get(), &cferror));
+ if (cferror) {
+ CFUniquePtr<::CFErrorRef> deleter(cferror);
+ return {ErrorCodes::InvalidSSLConfiguration,
+ str::stream() << "Unable to determine certificate validity: " << cferror};
+ }
+
+ auto swSubjectName = extractSubjectName(cfdict.get());
+ if (!swSubjectName.isOK()) {
+ return swSubjectName.getStatus();
+ }
+ auto subject = swSubjectName.getValue();
+
+ if (!expire) {
+ return subject;
+ }
+
+ // Marshal expiration.
+ auto swValidFrom =
+ extractValidityDate(cfdict.get(), ::kSecOIDX509V1ValidityNotBefore, "valid-from");
+ auto swValidUntil =
+ extractValidityDate(cfdict.get(), ::kSecOIDX509V1ValidityNotAfter, "valid-until");
+ if (!swValidFrom.isOK() || !swValidUntil.isOK()) {
+ return swValidUntil.getStatus();
+ }
+
+ *expire = swValidUntil.getValue();
+ const auto now = Date_t::now();
+ if ((now < swValidFrom.getValue()) || (*expire < now)) {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ "The provided SSL certificate is expired or not yet valid"};
+ }
+
+ return subject;
+};
+
+} // namespace
+
+/////////////////////////////////////////////////////////////////////////////
+// SSLConnection
+namespace {
+
+class SSLConnectionApple : public SSLConnectionInterface {
+public:
+ SSLConnectionApple(asio::ssl::apple::Context* ctx,
+ Socket* socket,
+ ::SSLProtocolSide side,
+ std::string hostname = "",
+ std::vector<uint8_t> init = {})
+ : _sock(socket), _init(std::move(init)) {
+ _ssl.reset(::SSLCreateContext(nullptr, side, ::kSSLStreamType));
+ uassert(ErrorCodes::InternalError, "Failed creating SSL context", _ssl);
+
+ auto certs = ctx->certs.get();
+ if (certs) {
+ uassertOSStatusOK(::SSLSetCertificate(_ssl.get(), certs));
+ }
+
+ uassertOSStatusOK(::SSLSetConnection(_ssl.get(), static_cast<void*>(this)));
+ uassertOSStatusOK(::SSLSetPeerID(_ssl.get(), _ssl.get(), sizeof(_ssl)));
+ uassertOSStatusOK(::SSLSetIOFuncs(_ssl.get(), read_func, write_func));
+ uassertOSStatusOK(::SSLSetProtocolVersionMin(_ssl.get(), ctx->protoMin));
+ uassertOSStatusOK(::SSLSetProtocolVersionMax(_ssl.get(), ctx->protoMax));
+
+ if (!hostname.empty()) {
+ uassertOSStatusOK(
+ ::SSLSetPeerDomainName(_ssl.get(), hostname.c_str(), hostname.size()));
+ }
+
+ // SSLHandshake will return errSSLServerAuthCompleted and let us do our own verify.
+ // We'll pretend to have done that, and let our caller invoke verifyPeer later.
+ uassertOSStatusOK(::SSLSetClientSideAuthenticate(_ssl.get(), ::kTryAuthenticate));
+ uassertOSStatusOK(
+ ::SSLSetSessionOption(_ssl.get(), ::kSSLSessionOptionBreakOnServerAuth, true));
+ uassertOSStatusOK(
+ ::SSLSetSessionOption(_ssl.get(), ::kSSLSessionOptionBreakOnClientAuth, true));
+
+ ::OSStatus status;
+ do {
+ status = ::SSLHandshake(_ssl.get());
+ } while ((status == ::errSSLServerAuthCompleted) ||
+ (status == ::errSSLClientAuthCompleted));
+ uassertOSStatusOK(status, ErrorCodes::SSLHandshakeFailed);
+ }
+
+ std::string getSNIServerName() const final {
+ size_t len = 0;
+ auto status = ::SSLCopyRequestedPeerNameLength(_ssl.get(), &len);
+ if (status != ::errSecSuccess) {
+ return "";
+ }
+ std::string ret;
+ ret.resize(len + 1);
+ status = ::SSLCopyRequestedPeerName(_ssl.get(), &ret[0], &len);
+ if (status != ::errSecSuccess) {
+ return "";
+ }
+ ret.resize(len);
+ return ret;
+ }
+
+ ::SSLContextRef get() const {
+ return const_cast<::SSLContextRef>(_ssl.get());
+ }
+
+private:
+ static ::OSStatus write_func(::SSLConnectionRef ctx, const void* data, size_t* data_len) {
+ const auto* conn = reinterpret_cast<const SSLConnectionApple*>(ctx);
+ size_t len = *data_len;
+ *data_len = 0;
+ while (len > 0) {
+ auto wrote =
+ ::write(conn->_sock->rawFD(), static_cast<const char*>(data) + *data_len, len);
+ if (wrote > 0) {
+ *data_len += wrote;
+ len -= wrote;
+ continue;
+ }
+ return posixErrno(errno);
+ }
+ return ::errSecSuccess;
+ }
+
+ static ::OSStatus read_func(::SSLConnectionRef ctx, void* data, size_t* data_len) {
+ auto* conn =
+ const_cast<SSLConnectionApple*>(reinterpret_cast<const SSLConnectionApple*>(ctx));
+ auto* dest = static_cast<char*>(data);
+ size_t len = *data_len;
+ *data_len = 0;
+ while (len > 0) {
+ if (conn->_init.size()) {
+ // Consume any initial bytes first.
+ auto& init = conn->_init;
+ const auto mvlen = std::max(len, init.size());
+ std::copy(init.begin(), init.begin() + mvlen, dest + *data_len);
+ init.erase(init.begin(), init.begin() + mvlen);
+ *data_len += mvlen;
+ len -= mvlen;
+ continue;
+ }
+
+ // Then go to the network.
+ auto didread = ::read(conn->_sock->rawFD(), dest + *data_len, len);
+ if (didread > 0) {
+ *data_len += didread;
+ len -= didread;
+ continue;
+ }
+ return posixErrno(errno);
+ }
+ return ::errSecSuccess;
+ }
+
+ Socket* _sock;
+
+ // When in server mode, _init contains any bytes read prior to
+ // starting the SSL handshake process.
+ // Once exhausted, this is never refilled.
+ std::vector<uint8_t> _init;
+
+ CFUniquePtr<::SSLContextRef> _ssl;
+};
+
+} // namespace
+
+/////////////////////////////////////////////////////////////////////////////
+// SSLManager
+namespace {
+
+class SSLManagerApple : public SSLManagerInterface {
+public:
+ explicit SSLManagerApple(const SSLParams& params, bool isServer);
+
+ Status initSSLContext(asio::ssl::apple::Context* context,
+ const SSLParams& params,
+ ConnectionDirection direction) final;
+
+ SSLConnectionInterface* connect(Socket* socket) final;
+ SSLConnectionInterface* accept(Socket* socket, const char* initialBytes, int len) final;
+
+ SSLPeerInfo parseAndValidatePeerCertificateDeprecated(const SSLConnectionInterface* conn,
+ const std::string& remoteHost) final;
+
+ StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate(
+ ::SSLContextRef conn, const std::string& remoteHost) final;
+
+ const SSLConfiguration& getSSLConfiguration() const final {
+ return _sslConfiguration;
+ }
+
+ int SSL_read(SSLConnectionInterface* conn, void* buf, int num) final;
+ int SSL_write(SSLConnectionInterface* conn, const void* buf, int num) final;
+ int SSL_shutdown(SSLConnectionInterface* conn) final;
+
+private:
+ Status _validatePEMs(const SSLParams& params, bool isServer);
+
+ bool _weakValidation;
+ bool _allowInvalidCertificates;
+ bool _allowInvalidHostnames;
+ asio::ssl::apple::Context _clientCtx;
+ asio::ssl::apple::Context _serverCtx;
+ CFUniquePtr<::CFArrayRef> _ca;
+ SSLConfiguration _sslConfiguration;
+};
+
+SSLManagerApple::SSLManagerApple(const SSLParams& params, bool isServer)
+ : _weakValidation(params.sslWeakCertificateValidation),
+ _allowInvalidCertificates(params.sslAllowInvalidCertificates),
+ _allowInvalidHostnames(params.sslAllowInvalidHostnames) {
+ uassertStatusOK(initSSLContext(&_clientCtx, params, ConnectionDirection::kOutgoing));
+ if (isServer) {
+ uassertStatusOK(initSSLContext(&_serverCtx, params, ConnectionDirection::kIncoming));
+ }
+ uassertStatusOK(_validatePEMs(params, isServer));
+
+ if (!params.sslCAFile.empty()) {
+ auto ca = uassertStatusOK(loadPEM(params.sslCAFile, "", kLoadPEMStripKeys));
+ _ca = std::move(ca);
+ _sslConfiguration.hasCA = _ca && ::CFArrayGetCount(_ca.get());
+ }
+}
+
+Status SSLManagerApple::_validatePEMs(const SSLParams& params, bool isServer) {
+ // pick the certificate for use in outgoing connections,
+ std::string clientPEM, clientPassword;
+ if (!isServer || params.sslClusterFile.empty()) {
+ // We are either a client, or a server without a cluster key,
+ // so use the PEM key file, if specified.
+ clientPEM = params.sslPEMKeyFile;
+ clientPassword = params.sslPEMKeyPassword;
+ } else {
+ // We are a server with a cluster key, so use the cluster key file
+ clientPEM = params.sslClusterFile;
+ clientPassword = params.sslClusterPassword;
+ }
+ if (!clientPEM.empty()) {
+ auto swSubject = loadAndValidatePEM(clientPEM, clientPassword);
+ if (!swSubject.isOK()) {
+ return swSubject.getStatus();
+ }
+ _sslConfiguration.clientSubjectName = swSubject.getValue();
+ }
+
+ if (isServer) {
+ auto swSubject = loadAndValidatePEM(params.sslPEMKeyFile,
+ params.sslPEMKeyPassword,
+ &_sslConfiguration.serverCertificateExpirationDate);
+ if (!swSubject.isOK()) {
+ return swSubject.getStatus();
+ }
+ _sslConfiguration.serverSubjectName = swSubject.getValue();
+
+ static auto task =
+ CertificateExpirationMonitor(_sslConfiguration.serverCertificateExpirationDate);
+ }
+
+ return Status::OK();
+}
+
+StatusWith<std::pair<::SSLProtocol, ::SSLProtocol>> parseProtocolRange(const SSLParams& params) {
+ // Map disabled protocols to range.
+ bool tls10 = true, tls11 = true, tls12 = true;
+ for (const SSLParams::Protocols& protocol : params.sslDisabledProtocols) {
+ if (protocol == SSLParams::Protocols::TLS1_0) {
+ tls10 = false;
+ } else if (protocol == SSLParams::Protocols::TLS1_1) {
+ tls11 = false;
+ } else if (protocol == SSLParams::Protocols::TLS1_2) {
+ tls12 = false;
+ } else {
+ return {ErrorCodes::InvalidSSLConfiguration, "Unknown disabled TLS protocol version"};
+ }
+ }
+ // Throw out the invalid cases.
+ if (tls10 && !tls11 && tls12) {
+ return {ErrorCodes::InvalidSSLConfiguration,
+ "Can not disable TLS 1.1 while leaving 1.0 and 1.2 enabled"};
+ }
+ if (!tls10 && !tls11 && !tls12) {
+ return {ErrorCodes::InvalidSSLConfiguration, "All valid TLS modes disabled"};
+ }
+
+ auto protoMin = tls10 ? ::kTLSProtocol1 : tls11 ? ::kTLSProtocol11 : ::kTLSProtocol12;
+ auto protoMax = tls12 ? ::kTLSProtocol12 : tls11 ? ::kTLSProtocol11 : ::kTLSProtocol1;
+
+ return std::pair<::SSLProtocol, ::SSLProtocol>(protoMin, protoMax);
+}
+
+Status SSLManagerApple::initSSLContext(asio::ssl::apple::Context* context,
+ const SSLParams& params,
+ ConnectionDirection direction) {
+ // Protocol Version.
+ const auto swProto = parseProtocolRange(params);
+ if (!swProto.isOK()) {
+ return swProto.getStatus();
+ }
+ const auto proto = swProto.getValue();
+ context->protoMin = proto.first;
+ context->protoMax = proto.second;
+
+ // Certificate.
+ if (direction == ConnectionDirection::kOutgoing && !params.sslClusterFile.empty()) {
+ auto swCertificates = loadPEM(params.sslClusterFile, params.sslClusterPassword);
+ if (!swCertificates.isOK()) {
+ return swCertificates.getStatus();
+ }
+ context->certs = std::move(swCertificates.getValue());
+ } else if (!params.sslPEMKeyFile.empty()) {
+ auto swCertificates = loadPEM(params.sslPEMKeyFile, params.sslPEMKeyPassword);
+ if (!swCertificates.isOK()) {
+ return swCertificates.getStatus();
+ }
+ context->certs = std::move(swCertificates.getValue());
+ }
+
+ return Status::OK();
+}
+
+SSLConnectionInterface* SSLManagerApple::connect(Socket* socket) {
+ return new SSLConnectionApple(
+ &_clientCtx, socket, ::kSSLClientSide, socket->remoteAddr().hostOrIp());
+}
+
+SSLConnectionInterface* SSLManagerApple::accept(Socket* socket, const char* initialBytes, int len) {
+ std::vector<uint8_t> init;
+ const auto* p = reinterpret_cast<const uint8_t*>(initialBytes);
+ init.insert(init.end(), p, p + len);
+ return new SSLConnectionApple(&_serverCtx, socket, ::kSSLServerSide, "", init);
+}
+
+SSLPeerInfo SSLManagerApple::parseAndValidatePeerCertificateDeprecated(
+ const SSLConnectionInterface* conn, const std::string& remoteHost) {
+ auto ssl = checked_cast<const SSLConnectionApple*>(conn)->get();
+
+ auto swPeerSubjectName = parseAndValidatePeerCertificate(ssl, remoteHost);
+ // We can't use uassertStatusOK here because we need to throw a NetworkException.
+ if (!swPeerSubjectName.isOK()) {
+ throwSocketError(SocketErrorKind::CONNECT_ERROR, swPeerSubjectName.getStatus().reason());
+ }
+ return swPeerSubjectName.getValue().get_value_or(SSLPeerInfo());
+}
+
+StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCertificate(
+ ::SSLContextRef ssl, const std::string& remoteHost) {
+
+ /* While we always have a system CA via the Keychain,
+ * we'll pretend not to in terms of validation if the server
+ * was started using a PEM file (legacy mode).
+ *
+ * When a certificate selector is used, we'll override hasCA to true
+ * so that the validation path runs anyway.
+ */
+ if (!_sslConfiguration.hasCA && isSSLServer) {
+ return {boost::none};
+ }
+
+ const auto badCert = [](StringData msg,
+ bool warn = false) -> StatusWith<boost::optional<SSLPeerInfo>> {
+ constexpr StringData prefix = "SSL peer certificate validation failed: "_sd;
+ if (warn) {
+ warning() << prefix << msg;
+ return {boost::none};
+ } else {
+ std::string m = str::stream() << prefix << msg << "; connection rejected";
+ error() << m;
+ return Status(ErrorCodes::SSLHandshakeFailed, m);
+ }
+ };
+
+ ::SecTrustRef trust = nullptr;
+ const auto status = ::SSLCopyPeerTrust(ssl, &trust);
+ CFUniquePtr<::SecTrustRef> cftrust(trust);
+ if ((status != ::errSecSuccess) || (!cftrust)) {
+ return badCert(str::stream() << "Unable to retreive SSL trust from peer: " << status,
+ _weakValidation);
+ }
+
+ if (_ca) {
+ auto status = ::SecTrustSetAnchorCertificates(cftrust.get(), _ca.get());
+ if (status == ::errSecSuccess) {
+ status = ::SecTrustSetAnchorCertificatesOnly(cftrust.get(), true);
+ }
+ if (status != ::errSecSuccess) {
+ return badCert(str::stream() << "Unable to bind CA to trust chain: " << status,
+ _weakValidation);
+ }
+ }
+
+ auto result = ::kSecTrustResultInvalid;
+ uassertOSStatusOK(::SecTrustEvaluate(cftrust.get(), &result), ErrorCodes::SSLHandshakeFailed);
+ if ((result != ::kSecTrustResultProceed) && (result != ::kSecTrustResultUnspecified)) {
+ if (result == ::kSecTrustResultDeny) {
+ return badCert("Certificate trust denied", _allowInvalidCertificates);
+ } else if (result == ::kSecTrustResultRecoverableTrustFailure) {
+ return badCert("Certificate trust failed (recoverably)", _allowInvalidCertificates);
+ } else if (result == ::kSecTrustResultFatalTrustFailure) {
+ return badCert("Certificate trust failure", _allowInvalidCertificates);
+ } else {
+ static_assert(std::is_signed<uint>::value ==
+ std::is_signed<::SecTrustResultType>::value,
+ "Must cast status to same signedness");
+ static_assert(sizeof(uint) >= sizeof(::SecTrustResultType),
+ "Must cast result to same or wider type");
+ return badCert(str::stream() << "Unknown cause: " << static_cast<uint>(result),
+ _allowInvalidCertificates);
+ }
+ }
+
+ auto cert = ::SecTrustGetCertificateAtIndex(cftrust.get(), 0);
+ if (!cert) {
+ return badCert("no SSL certificate provided by peer", _weakValidation);
+ }
+
+ CFUniquePtr<::CFMutableArrayRef> oids(
+ ::CFArrayCreateMutable(nullptr, 2, &::kCFTypeArrayCallBacks));
+ ::CFArrayAppendValue(oids.get(), ::kSecOIDX509V1SubjectName);
+ ::CFArrayAppendValue(oids.get(), ::kSecOIDSubjectAltName);
+
+ ::CFErrorRef err = nullptr;
+ CFUniquePtr<::CFDictionaryRef> cfdict(::SecCertificateCopyValues(cert, oids.get(), &err));
+ CFUniquePtr<::CFErrorRef> cferror(err);
+ if (cferror) {
+ return badCert(str::stream() << cferror.get(), _weakValidation);
+ }
+
+ // Extract SubjectName into a human readable string.
+ auto swPeerSubjectName = extractSubjectName(cfdict.get());
+ if (!swPeerSubjectName.isOK()) {
+ return swPeerSubjectName.getStatus();
+ }
+ const auto peerSubjectName = std::move(swPeerSubjectName.getValue());
+ LOG(2) << "Accepted TLS connection from peer: " << peerSubjectName;
+
+ if (remoteHost.empty()) {
+ // If this is an SSL server context (on a mongod/mongos)
+ // parse any client roles out of the client certificate.
+ auto swPeerCertificateRoles = parsePeerRoles(cert);
+ if (!swPeerCertificateRoles.isOK()) {
+ return swPeerCertificateRoles.getStatus();
+ }
+ return boost::make_optional(
+ SSLPeerInfo(peerSubjectName, std::move(swPeerCertificateRoles.getValue())));
+ }
+
+ // If this is an SSL client context (on a MongoDB server or client)
+ // perform hostname validation of the remote server
+ bool sanMatch = false;
+ bool cnMatch = false;
+ StringBuilder certErr;
+ certErr << "The server certificate does not match the host name. "
+ << "Hostname: " << remoteHost << " does not match ";
+
+ // Attempt to retreive "Subject Alternative Name"
+ std::vector<std::string> sans;
+ auto swSANs = extractSubjectAlternateNames(cfdict.get());
+ if (swSANs.isOK()) {
+ sans = std::move(swSANs.getValue());
+ }
+
+ if (!sans.empty()) {
+ certErr << "SAN(s): ";
+ for (auto& san : sans) {
+ if (hostNameMatchForX509Certificates(remoteHost, san)) {
+ sanMatch = true;
+ break;
+ }
+ certErr << san << " ";
+ }
+
+ } else if (peerSubjectName.find("CN=") != std::string::npos) {
+ const auto cnBegin = peerSubjectName.find("CN=") + 3;
+ auto cnEnd = peerSubjectName.find(",", cnBegin);
+ if (cnEnd == std::string::npos) {
+ cnEnd = peerSubjectName.size();
+ }
+ const auto commonName = peerSubjectName.substr(cnBegin, cnEnd - cnBegin);
+ cnMatch = hostNameMatchForX509Certificates(remoteHost, commonName);
+ certErr << "CN: " << commonName;
+
+ } else {
+ certErr << "No Common Name (CN) or Subject Alternate Names (SAN) found";
+ }
+
+ if (!sanMatch && !cnMatch) {
+ const auto msg = certErr.str();
+ if (_allowInvalidCertificates || _allowInvalidHostnames || isUnixDomainSocket(remoteHost)) {
+ warning() << msg;
+ } else {
+ error() << msg;
+ return Status(ErrorCodes::SSLHandshakeFailed, msg);
+ }
+ }
+
+ return boost::make_optional(SSLPeerInfo(peerSubjectName, stdx::unordered_set<RoleName>()));
+}
+
+int SSLManagerApple::SSL_read(SSLConnectionInterface* conn, void* buf, int num) {
+ auto ssl = checked_cast<SSLConnectionApple*>(conn)->get();
+ size_t read = 0;
+ uassertOSStatusOK(::SSLRead(ssl, static_cast<uint8_t*>(buf), num, &read),
+ SocketErrorKind::RECV_ERROR);
+ return read;
+}
+
+int SSLManagerApple::SSL_write(SSLConnectionInterface* conn, const void* buf, int num) {
+ auto ssl = checked_cast<SSLConnectionApple*>(conn)->get();
+ size_t written = 0;
+ uassertOSStatusOK(::SSLWrite(ssl, static_cast<const uint8_t*>(buf), num, &written),
+ SocketErrorKind::SEND_ERROR);
+ return written;
+}
+
+int SSLManagerApple::SSL_shutdown(SSLConnectionInterface* conn) {
+ auto ssl = checked_cast<SSLConnectionApple*>(conn)->get();
+ const auto status = ::SSLClose(ssl);
+ if (status == ::errSSLWouldBlock) {
+ return 0;
+ }
+ uassertOSStatusOK(status, ErrorCodes::SocketException);
+ return 1;
+}
+
+} // namespace
+
+/////////////////////////////////////////////////////////////////////////////
+
+// Global variable indicating if this is a server or a client instance
+bool isSSLServer = false;
+
+namespace {
+SimpleMutex sslManagerMtx;
+SSLManagerInterface* theSSLManager = nullptr;
+} // namespace
+
+std::unique_ptr<SSLManagerInterface> SSLManagerInterface::create(const SSLParams& params,
+ bool isServer) {
+ return stdx::make_unique<SSLManagerApple>(params, isServer);
+}
+
+MONGO_INITIALIZER(SSLManager)(InitializerContext*) {
+ stdx::lock_guard<SimpleMutex> lck(sslManagerMtx);
+ if (!isSSLServer || (sslGlobalParams.sslMode.load() != SSLParams::SSLMode_disabled)) {
+ theSSLManager = new SSLManagerApple(sslGlobalParams, isSSLServer);
+ }
+ return Status::OK();
+}
+
+} // namespace mongo
+
+mongo::SSLManagerInterface* mongo::getSSLManager() {
+ stdx::lock_guard<SimpleMutex> lck(sslManagerMtx);
+ if (theSSLManager) {
+ return theSSLManager;
+ }
+ return nullptr;
+}
diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp
index 98db6a830fc..372c38b4975 100644
--- a/src/mongo/util/net/ssl_manager_openssl.cpp
+++ b/src/mongo/util/net/ssl_manager_openssl.cpp
@@ -1419,22 +1419,3 @@ void SSLManagerOpenSSL::_handleSSLError(int code, int ret) {
throwSocketError(SocketErrorKind::CONNECT_ERROR, "");
}
} // namespace mongo
-
-// TODO SERVER-11601 Use NFC Unicode canonicalization
-bool mongo::hostNameMatchForX509Certificates(std::string nameToMatch, std::string certHostName) {
- nameToMatch = removeFQDNRoot(std::move(nameToMatch));
- certHostName = removeFQDNRoot(std::move(certHostName));
-
- if (certHostName.size() < 2) {
- return false;
- }
-
- // match wildcard DNS names
- if (certHostName[0] == '*' && certHostName[1] == '.') {
- // allow name.example.com if the cert is *.example.com, '*' does not match '.'
- const char* subName = strchr(nameToMatch.c_str(), '.');
- return subName && !strcasecmp(certHostName.c_str() + 1, subName);
- } else {
- return !strcasecmp(nameToMatch.c_str(), certHostName.c_str());
- }
-}
diff --git a/src/mongo/util/version.cpp b/src/mongo/util/version.cpp
index 9536caf1516..3df27505eec 100644
--- a/src/mongo/util/version.cpp
+++ b/src/mongo/util/version.cpp
@@ -150,7 +150,11 @@ void VersionInfoInterface::appendBuildInfo(BSONObjBuilder* result) const {
#if MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_OPENSSL
opensslInfo << "running" << openSSLVersion() << "compiled" << OPENSSL_VERSION_TEXT;
#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_WINDOWS
- opensslInfo << "Windows SChannel";
+ opensslInfo << "running"
+ << "Windows SChannel";
+#elif MONGO_CONFIG_SSL_PROVIDER == SSL_PROVIDER_APPLE
+ opensslInfo << "running"
+ << "Apple Secure Transport";
#else
#error "Unknown SSL Provider"
#endif // MONGO_CONFIG_SSL_PROVIDER