1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-19 00:21:35 -05:00

Introduce options for adding certificates to the X509ChainPolicy.CustomTrustStore

Co-authored-by: tangowithfoxtrot <tangowithfoxtrot@users.noreply.github.com>
This commit is contained in:
Justin Baur
2025-04-04 11:58:15 -04:00
parent 39ac93326d
commit 190328c0cf
8 changed files with 412 additions and 21 deletions

View File

@ -3,9 +3,11 @@ using System.Security.Cryptography.X509Certificates;
using Bit.Core.Models.Mail;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Platform.TlsCustomization;
using MailKit.Security;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Rnwood.SmtpServer;
using Rnwood.SmtpServer.Extensions.Auth;
using Xunit.Abstractions;
@ -100,7 +102,8 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509CertificateCustomizationOptions())
);
await Assert.ThrowsAsync<SslHandshakeException>(
@ -113,7 +116,7 @@ public class MailKitSmtpMailDeliveryServiceTests
);
}
[Fact(Skip = "Upcoming feature")]
[Fact]
public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_Works()
{
// If an SMTP server is using a self signed cert we will in the future
@ -130,12 +133,18 @@ public class MailKitSmtpMailDeliveryServiceTests
gs.Mail.Smtp.Ssl = true;
});
// TODO: Setup custom location and save self signed cert there.
// await SaveCertAsync("./my-location", _selfSignedCert);
var tlsOptions = new X509CertificateCustomizationOptions
{
AdditionalCustomTrustCertificates =
[
_selfSignedCert,
],
};
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(tlsOptions)
);
var tcs = new TaskCompletionSource();
@ -162,7 +171,7 @@ public class MailKitSmtpMailDeliveryServiceTests
await tcs.Task;
}
[Fact(Skip = "Upcoming feature")]
[Fact]
public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_WithUnrelatedCerts_Works()
{
// If an SMTP server is using a self signed cert we will in the future
@ -179,15 +188,19 @@ public class MailKitSmtpMailDeliveryServiceTests
gs.Mail.Smtp.Ssl = true;
});
// TODO: Setup custom location and save self signed cert there
// along with another self signed cert that is not related to
// the SMTP server.
// await SaveCertAsync("./my-location", _selfSignedCert);
// await SaveCertAsync("./my-location", CreateSelfSignedCert("example.com"));
var tlsOptions = new X509CertificateCustomizationOptions
{
AdditionalCustomTrustCertificates =
[
_selfSignedCert,
CreateSelfSignedCert("example.com"),
],
};
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(tlsOptions)
);
var tcs = new TaskCompletionSource();
@ -234,7 +247,8 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509CertificateCustomizationOptions())
);
var tcs = new TaskCompletionSource();
@ -280,7 +294,8 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509CertificateCustomizationOptions())
);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
@ -315,7 +330,8 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509CertificateCustomizationOptions())
);
var tcs = new TaskCompletionSource();
@ -381,7 +397,8 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509CertificateCustomizationOptions())
);
var tcs = new TaskCompletionSource();

View File

@ -0,0 +1,137 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Bit.Core.Platform.X509ChainCustomization;
using Bit.Core.Settings;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.TlsCustomization;
public class X509ChainCustomizationServiceCollectionExtensionsTests
{
private static X509Certificate2 CreateSelfSignedCert(string commonName)
{
using var rsa = RSA.Create(2048);
var certRequest = new CertificateRequest($"CN={commonName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return certRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
}
[Fact]
public async Task OptionsPatternReturnsCachedValue()
{
var tempDir = Directory.CreateTempSubdirectory("certs");
var tempCert = Path.Combine(tempDir.FullName, "test.crt");
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert));
var services = CreateServices((gs, environment, config) =>
{
gs.SelfHosted = true;
environment.EnvironmentName = "Development";
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName;
});
// Create options once
var firstOptions = services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
Assert.NotNull(firstOptions.AdditionalCustomTrustCertificates);
var cert = Assert.Single(firstOptions.AdditionalCustomTrustCertificates);
Assert.Equal("CN=localhost", cert.Subject);
// Since the second resolution should have cached values, deleting the file during operation
// should have no impact.
File.Delete(tempCert);
// This is expected to be a cached version and doesn't actually need to go and read the file system
var secondOptions = services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
Assert.Same(firstOptions, secondOptions);
// This is the same reference as the first one so it shouldn't be different but just in case.
Assert.NotNull(secondOptions.AdditionalCustomTrustCertificates);
Assert.Single(secondOptions.AdditionalCustomTrustCertificates);
}
[Fact]
public async Task DoesNotProvideCustomCallbackOnCloud()
{
var tempDir = Directory.CreateTempSubdirectory("certs");
var tempCert = Path.Combine(tempDir.FullName, "test.crt");
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert));
var options = CreateOptions((gs, environment, config) =>
{
gs.SelfHosted = false;
environment.EnvironmentName = "Development";
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName;
});
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _));
}
[Fact]
public async Task ManuallyAddingOptionsTakesPrecedence()
{
var tempDir = Directory.CreateTempSubdirectory("certs");
var tempCert = Path.Combine(tempDir.FullName, "test.crt");
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert));
var options = CreateOptions((gs, environment, config) =>
{
gs.SelfHosted = false;
environment.EnvironmentName = "Development";
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName;
}, services =>
{
services.Configure<X509ChainOptions>(options =>
{
options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")];
});
});
Assert.True(options.TryGetCustomRemoteCertificateValidationCallback(out var callback));
var cert = Assert.Single(options.AdditionalCustomTrustCertificates);
Assert.Equal("CN=example.com", cert.Subject);
}
private static X509ChainOptions CreateOptions(Action<GlobalSettings, IHostEnvironment, Dictionary<string, string>> configure, Action<IServiceCollection>? after = null)
{
var services = CreateServices(configure, after);
return services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
}
private static IServiceProvider CreateServices(Action<GlobalSettings, IHostEnvironment, Dictionary<string, string>> configure, Action<IServiceCollection>? after = null)
{
var globalSettings = new GlobalSettings();
var hostEnvironment = Substitute.For<IHostEnvironment>();
var config = new Dictionary<string, string>();
configure(globalSettings, hostEnvironment, config);
var services = new ServiceCollection();
services.AddSingleton(globalSettings);
services.AddSingleton(hostEnvironment);
services.AddSingleton<IConfiguration>(
new ConfigurationBuilder()
.AddInMemoryCollection(config)
.Build()
);
services.AddX509ChainCustomization();
after?.Invoke(services);
return services.BuildServiceProvider();
}
}

View File

@ -4,9 +4,11 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Business;
using Bit.Core.Entities;
using Bit.Core.Platform.X509ChainCustomization;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
@ -136,7 +138,7 @@ public class HandlebarsMailServiceTests
SiteName = "Bitwarden",
};
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>(), Options.Create(new X509ChainOptions()));
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService());

View File

@ -1,6 +1,8 @@
using Bit.Core.Services;
using Bit.Core.Platform.X509ChainCustomization;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
@ -23,7 +25,8 @@ public class MailKitSmtpMailDeliveryServiceTests
_sut = new MailKitSmtpMailDeliveryService(
_globalSettings,
_logger
_logger,
Options.Create(new X509ChainOptions())
);
}