diff --git a/build.cake b/build.cake index 1fcd96a6554..2ad45206ef6 100644 --- a/build.cake +++ b/build.cake @@ -254,6 +254,15 @@ Task("TestCsfleWithGcpKms") action: (BuildConfig buildConfig, Path testProject) => RunTests(buildConfig, testProject, filter: "Category=\"CsfleGCPKMS\"")); +Task("TestX509") + .IsDependentOn("Build") + .DoesForEach( + items: GetFiles("./**/MongoDB.Driver.Tests.csproj"), + action: (BuildConfig buildConfig, Path testProject) => + RunTests(buildConfig, testProject, filter: "Category=\"X509\"")); + +Task("TestX509Net60").IsDependentOn("TestX509"); + Task("Package") .IsDependentOn("PackageNugetPackages"); diff --git a/evergreen/convert-client-cert-to-pkcs12.sh b/evergreen/convert-client-cert-to-pkcs12.sh index ee8a5939df0..b5523c86d49 100755 --- a/evergreen/convert-client-cert-to-pkcs12.sh +++ b/evergreen/convert-client-cert-to-pkcs12.sh @@ -3,29 +3,39 @@ set -o errexit # Exit the script with an error if any of the commands fail # Environment variables used as input: -# CLIENT_PEM Path to mongo -orchestration's client.pem: must be set. -# MONGO_X509_CLIENT_P12 Filename for client certificate in p12 format +# CLIENT_PEM Path to mongo client.pem: must be set +# P12_FILENAME Filename for client certificate in p12 format +# P12_PASSWORD Password for client certificate in p12 format +# P12_FRIENDLY_NAME Friendly name for client certificate in p12 format +# OUT_CLIENT_PATH_VAR Name of the output variable containing the path of the p12 certificate +# OUT_CLIENT_PASSWORD_VAR Name of the output variable containing the password for the p12 certificate # # Environment variables produced as output: -# MONGODB_X509_CLIENT_P12_PATH Absolute path to client certificate in p12 format -# MONGO_X509_CLIENT_CERTIFICATE_PASSWORD Password for client certificate +# ${OUT_CLIENT_PATH_VAR} Absolute path to client certificate in p12 format (OUT_CLIENT_PATH_VAR contains the actual variable being exported) +# ${OUT_CLIENT_PASSWORD_VAR} Password for client certificate (OUT_CLIENT_PASSWORD_VAR contains the actual variable being exported) -CLIENT_PEM=${CLIENT_PEM:-nil} -MONGO_X509_CLIENT_P12=${MONGO_X509_CLIENT_P12:-client.p12} -MONGO_X509_CLIENT_CERTIFICATE_PASSWORD=${MONGO_X509_CLIENT_CERTIFICATE_PASSWORD:-Picard-Alpha-Alpha-3-0-5} +# Input environment variables and default values +: "${CLIENT_PEM:=nil}" +: "${P12_FRIENDLY_NAME:="Drivers Client Certificate"}" +: "${P12_FILENAME:="client.p12"}" +: "${P12_PASSWORD:="Picard-Alpha-Alpha-3-0-5"}" +: "${OUT_CLIENT_PATH_VAR:="MONGO_X509_CLIENT_CERTIFICATE_PATH"}" +: "${OUT_CLIENT_PASSWORD_VAR:="MONGO_X509_CLIENT_CERTIFICATE_PASSWORD"}" if [[ "$CLIENT_PEM" == "nil" ]]; then + echo "Error: CLIENT_PEM must be set." exit 1 fi openssl pkcs12 -export -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 -in "${CLIENT_PEM}" \ - -out "${MONGO_X509_CLIENT_P12}" \ - -name "Drivers Client Certificate" \ - -password "pass:${MONGO_X509_CLIENT_CERTIFICATE_PASSWORD}" + -out "${P12_FILENAME}" \ + -name "${P12_FRIENDLY_NAME}" \ + -password "pass:${P12_PASSWORD}" +# Determine path using realpath (compatible across macOS, Linux, and Windows) if [[ "$OS" =~ MAC|Mac|mac ]]; then - # this function is not available on mac OS + # Functionality to mimic `realpath` on macOS function realpath() { OURPWD=$PWD cd "$(dirname "$1")" @@ -39,11 +49,17 @@ if [[ "$OS" =~ MAC|Mac|mac ]]; then echo "$REALPATH" } fi -MONGO_X509_CLIENT_CERTIFICATE_PATH=$(realpath "${MONGO_X509_CLIENT_P12}") + +CERT_PATH=$(realpath "${P12_FILENAME}") if [[ "$OS" =~ Windows|windows ]]; then - MONGO_X509_CLIENT_CERTIFICATE_PATH=$(cygpath -w "${MONGO_X509_CLIENT_CERTIFICATE_PATH}") + CERT_PATH=$(cygpath -w "${CERT_PATH}") fi -export MONGO_X509_CLIENT_CERTIFICATE_PATH -export MONGO_X509_CLIENT_CERTIFICATE_PASSWORD +# Output environment variables +export "${OUT_CLIENT_PASSWORD_VAR}"="${P12_PASSWORD}" +export "${OUT_CLIENT_PATH_VAR}"="${CERT_PATH}" + +echo "Exported variables:" +echo "${OUT_CLIENT_PASSWORD_VAR}=${!OUT_CLIENT_PASSWORD_VAR}" +echo "${OUT_CLIENT_PATH_VAR}=${!OUT_CLIENT_PATH_VAR}" \ No newline at end of file diff --git a/evergreen/evergreen.yml b/evergreen/evergreen.yml index f6f961402ce..bf9dbbff883 100644 --- a/evergreen/evergreen.yml +++ b/evergreen/evergreen.yml @@ -1029,6 +1029,46 @@ functions: bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/run-driver-test.sh bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/teardown-pod.sh + setup-x509-tests: + - command: shell.exec + params: + shell: "bash" + include_expansions_in_env: + - "AWS_ACCESS_KEY_ID" + - "AWS_SECRET_ACCESS_KEY" + - "AWS_SESSION_TOKEN" + script: | + ${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh drivers/atlas_connect + source secrets-export.sh + + echo $ATLAS_X509_DEV_CERT_BASE64 | base64 --decode > ${DRIVERS_TOOLS}/CLIENT_CERT.pem + echo $ATLAS_X509_DEV_CERT_NOUSER_BASE64 | base64 --decode > ${DRIVERS_TOOLS}/CLIENT_NO_USER_CERT.pem + + run-x509-tests: + - command: shell.exec + type: test + params: + shell: "bash" + working_dir: mongo-csharp-driver + script: | + source ../secrets-export.sh + ${PREPARE_SHELL} + OS=${OS} \ + evergreen/add-ca-certs.sh + P12_FRIENDLY_NAME="Drivers No-User Client Certificate" \ + P12_FILENAME="client_no_user.p12" \ + OUT_CLIENT_PASSWORD_VAR="MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PASSWORD" \ + OUT_CLIENT_PATH_VAR="MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PATH" \ + CLIENT_PEM=${DRIVERS_TOOLS}/CLIENT_NO_USER_CERT.pem \ + source evergreen/convert-client-cert-to-pkcs12.sh + MONGODB_URI="$ATLAS_X509_DEV" \ + CLIENT_PEM=${DRIVERS_TOOLS}/CLIENT_CERT.pem \ + TOPOLOGY=${TOPOLOGY} \ + OS=${OS} \ + FRAMEWORK=${FRAMEWORK} \ + TARGET="TestX509" \ + evergreen/run-tests.sh + pre: - func: fetch-source - func: prepare-resources @@ -1720,6 +1760,14 @@ tasks: ${PREPARE_SHELL} CRYPT_SHARED_LIB_PATH="${CRYPT_SHARED_LIB_PATH}" DRIVER_VERSION="${PACKAGE_VERSION}" MONGODB_VERSION="${VERSION}" bash ./evergreen/run-tests.sh + - name: x509-auth-tests + commands: + - func: assume-ec2-role + - func: setup-x509-tests + - func: run-x509-tests + vars: + FRAMEWORK: net60 + axes: - id: version display_name: MongoDB Version @@ -2417,6 +2465,12 @@ buildvariants: - name: test-gssapi-netstandard21 - name: test-gssapi-net60 +- matrix_name: "x509-tests" + matrix_spec: { os: ["ubuntu-2004", "macos-14", "windows-64"], ssl: ["ssl"], version: ["latest"], topology: ["standalone"] } + display_name: "X509 tests ${version} ${os}" + tasks: + - name: x509-auth-tests + # Load balancer tests - matrix_name: load-balancer-tests matrix_spec: { version: ["5.0", "6.0", "7.0", "8.0", "rapid", "latest"], auth: "noauth", ssl: "nossl", topology: "sharded-cluster", os: "ubuntu-2004" } diff --git a/tests/MongoDB.Driver.Tests/X509Tests.cs b/tests/MongoDB.Driver.Tests/X509Tests.cs new file mode 100644 index 00000000000..a948204ae6f --- /dev/null +++ b/tests/MongoDB.Driver.Tests/X509Tests.cs @@ -0,0 +1,125 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Security.Cryptography.X509Certificates; +using FluentAssertions; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.TestHelpers.XunitExtensions; +using Xunit; + +namespace MongoDB.Driver.Tests; + +[Trait("Category", "Integration")] +[Trait("Category", "X509")] +public class X509Tests +{ + const string MONGODB_X509_CLIENT_CERTIFICATE_PATH = "MONGO_X509_CLIENT_CERTIFICATE_PATH"; + const string MONGODB_X509_CLIENT_CERTIFICATE_PASSWORD = "MONGO_X509_CLIENT_CERTIFICATE_PASSWORD"; + + const string MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PATH = "MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PATH"; + const string MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PASSWORD = "MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PASSWORD"; + + [Fact] + public void Authentication_succeeds_with_MONGODB_X509_mechanism() + { + var clientCertificate = GetClientCertificate(CertificateType.MONGO_X509); + + var settings = DriverTestConfiguration.GetClientSettings(); + settings.SslSettings.ClientCertificates = [clientCertificate]; + + AssertAuthenticationSucceeds(settings); + } + + [Fact] + public void Authentication_fails_with_MONGODB_X509_mechanism_when_username_is_wrong() + { + var clientCertificate = GetClientCertificate(CertificateType.MONGO_X509); + + var settings = DriverTestConfiguration.GetClientSettings(); + settings.Credential = MongoCredential.CreateMongoX509Credential("wrong_username"); + settings.SslSettings.ClientCertificates = [clientCertificate]; + + AssertAuthenticationFails(settings); + } + + [Fact] + public void Authentication_fails_with_MONGODB_X509_mechanism_when_user_is_not_in_database() + { + var noUserClientCertificate = GetClientCertificate(CertificateType.MONGO_X509_CLIENT_NO_USER); + + var settings = DriverTestConfiguration.GetClientSettings(); + settings.SslSettings.ClientCertificates = [noUserClientCertificate]; + + AssertAuthenticationFails(settings, "Could not find user"); + } + + private void AssertAuthenticationSucceeds(MongoClientSettings settings) + { + using var client = DriverTestConfiguration.CreateMongoClient(settings); + _ = client.ListDatabaseNames().ToList(); + } + + private void AssertAuthenticationFails(MongoClientSettings settings, string innerExceptionMessage = null) + { + using var client = DriverTestConfiguration.CreateMongoClient(settings); + var exception = Record.Exception(() => client.ListDatabaseNames().ToList()); + exception.Should().BeOfType(); + + if (innerExceptionMessage != null) + { + var innerException = exception.InnerException; + innerException.Should().BeOfType(); + innerException.Message.Should().Contain(innerExceptionMessage); + } + } + + private enum CertificateType + { + MONGO_X509, + MONGO_X509_CLIENT_NO_USER + } + + private X509Certificate2 GetClientCertificate(CertificateType certificateType) + { + RequireServer.Check().Tls(required: true); + + string pathVariable = null; + string passwordVariable = null; + + switch (certificateType) + { + case CertificateType.MONGO_X509: + pathVariable = MONGODB_X509_CLIENT_CERTIFICATE_PATH; + passwordVariable = MONGODB_X509_CLIENT_CERTIFICATE_PASSWORD; + break; + case CertificateType.MONGO_X509_CLIENT_NO_USER: + pathVariable = MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PATH; + passwordVariable = MONGO_X509_CLIENT_NO_USER_CERTIFICATE_PASSWORD; + break; + default: + throw new ArgumentException("Wrong certificate type specified.", nameof(certificateType)); + } + + RequireEnvironment.Check() + .EnvironmentVariable(pathVariable, isDefined: true) + .EnvironmentVariable(passwordVariable, isDefined: true); + + var path = Environment.GetEnvironmentVariable(pathVariable); + var password = Environment.GetEnvironmentVariable(passwordVariable); + + return new X509Certificate2(path, password); + } +} \ No newline at end of file