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:
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user