mirror of
https://github.com/bitwarden/server
synced 2025-12-15 07:43:54 +00:00
Remove X509ChainCustomization Feature (#6108)
* Remove X509ChainCustomization Feature * `dotnet format`
This commit is contained in:
@@ -1,96 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
|
|
||||||
internal sealed class PostConfigureX509ChainOptions : IPostConfigureOptions<X509ChainOptions>
|
|
||||||
{
|
|
||||||
const string CertificateSearchPattern = "*.crt";
|
|
||||||
|
|
||||||
private readonly ILogger<PostConfigureX509ChainOptions> _logger;
|
|
||||||
private readonly IHostEnvironment _hostEnvironment;
|
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
|
|
||||||
public PostConfigureX509ChainOptions(
|
|
||||||
ILogger<PostConfigureX509ChainOptions> logger,
|
|
||||||
IHostEnvironment hostEnvironment,
|
|
||||||
GlobalSettings globalSettings)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_hostEnvironment = hostEnvironment;
|
|
||||||
_globalSettings = globalSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PostConfigure(string? name, X509ChainOptions options)
|
|
||||||
{
|
|
||||||
// We don't register or request a named instance of these options,
|
|
||||||
// so don't customize it.
|
|
||||||
if (name != Options.DefaultName)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only allow this setting to be configured on self host.
|
|
||||||
if (!_globalSettings.SelfHosted)
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificatesDirectory = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.AdditionalCustomTrustCertificates != null)
|
|
||||||
{
|
|
||||||
// Additional certificates were added directly, this overwrites the need to
|
|
||||||
// read them from the directory.
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Additional custom trust certificates were added directly, skipping loading them from '{Directory}'",
|
|
||||||
options.AdditionalCustomTrustCertificatesDirectory
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(options.AdditionalCustomTrustCertificatesDirectory))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(options.AdditionalCustomTrustCertificatesDirectory))
|
|
||||||
{
|
|
||||||
// The default directory is volume mounted via the default Bitwarden setup process.
|
|
||||||
// If the directory doesn't exist it could indicate a error in configuration but this
|
|
||||||
// directory is never expected in a normal development environment so lower the log
|
|
||||||
// level in that case.
|
|
||||||
var logLevel = _hostEnvironment.IsDevelopment()
|
|
||||||
? LogLevel.Debug
|
|
||||||
: LogLevel.Warning;
|
|
||||||
_logger.Log(
|
|
||||||
logLevel,
|
|
||||||
"An additional custom trust certificate directory was given '{Directory}' but that directory does not exist.",
|
|
||||||
options.AdditionalCustomTrustCertificatesDirectory
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var certificates = new List<X509Certificate2>();
|
|
||||||
|
|
||||||
foreach (var certFile in Directory.EnumerateFiles(options.AdditionalCustomTrustCertificatesDirectory, CertificateSearchPattern))
|
|
||||||
{
|
|
||||||
certificates.Add(new X509Certificate2(certFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.AdditionalCustomTrustCertificatesDirectory != X509ChainOptions.DefaultAdditionalCustomTrustCertificatesDirectory && certificates.Count == 0)
|
|
||||||
{
|
|
||||||
// They have intentionally given us a non-default directory but there weren't certificates, that is odd.
|
|
||||||
_logger.LogWarning(
|
|
||||||
"No additional custom trust certificates were found in '{Directory}'",
|
|
||||||
options.AdditionalCustomTrustCertificatesDirectory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.AdditionalCustomTrustCertificates = certificates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for setting up the ability to provide customization to how X509 chain validation works in an <see cref="IServiceCollection"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static class X509ChainCustomizationServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Configures X509ChainPolicy customization through the root level <c>X509ChainOptions</c> configuration section
|
|
||||||
/// and configures the primary <see cref="HttpMessageHandler"/> to use custom certificate validation
|
|
||||||
/// when customized to do so.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
|
|
||||||
/// <returns>The <see cref="IServiceCollection"/> for additional chaining.</returns>
|
|
||||||
public static IServiceCollection AddX509ChainCustomization(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
|
||||||
|
|
||||||
services.AddOptions<X509ChainOptions>()
|
|
||||||
.BindConfiguration(nameof(X509ChainOptions));
|
|
||||||
|
|
||||||
// Use TryAddEnumerable to make sure `PostConfigureX509ChainOptions` isn't added multiple
|
|
||||||
// times even if this method is called multiple times.
|
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<X509ChainOptions>, PostConfigureX509ChainOptions>());
|
|
||||||
|
|
||||||
services.AddHttpClient()
|
|
||||||
.ConfigureHttpClientDefaults(builder =>
|
|
||||||
{
|
|
||||||
builder.ConfigurePrimaryHttpMessageHandler(sp =>
|
|
||||||
{
|
|
||||||
var x509ChainOptions = sp.GetRequiredService<IOptions<X509ChainOptions>>().Value;
|
|
||||||
|
|
||||||
var handler = new HttpClientHandler();
|
|
||||||
|
|
||||||
if (x509ChainOptions.TryGetCustomRemoteCertificateValidationCallback(out var callback))
|
|
||||||
{
|
|
||||||
handler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, errors) =>
|
|
||||||
{
|
|
||||||
return callback(certificate, chain, errors);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Net.Security;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
|
|
||||||
namespace Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows for customization of the <see cref="X509ChainPolicy"/> and access to a custom server certificate validator
|
|
||||||
/// if customization has been made.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class X509ChainOptions
|
|
||||||
{
|
|
||||||
// This is the directory that we historically used to allow certificates be added inside our container
|
|
||||||
// and then on start of the container we would move them to `/usr/local/share/ca-certificates/` and call
|
|
||||||
// `update-ca-certificates` but since that operation requires root we can't do it in a rootless container.
|
|
||||||
// Ref: https://github.com/bitwarden/server/blob/67d7d685a619a5fc413f8532dacb09681ee5c956/src/Api/entrypoint.sh#L38-L41
|
|
||||||
public const string DefaultAdditionalCustomTrustCertificatesDirectory = "/etc/bitwarden/ca-certificates/";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A directory where additional certificates should be read from and included in <see cref="X509ChainPolicy.CustomTrustStore"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Only certificates suffixed with <c>*.crt</c> will be read. If <see cref="AdditionalCustomTrustCertificates"/> is
|
|
||||||
/// set, then this directory will not be read from.
|
|
||||||
/// </remarks>
|
|
||||||
public string? AdditionalCustomTrustCertificatesDirectory { get; set; } = DefaultAdditionalCustomTrustCertificatesDirectory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A list of additional certificates that should be included in <see cref="X509ChainPolicy.CustomTrustStore"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// If this value is set manually, then <see cref="AdditionalCustomTrustCertificatesDirectory"/> will be ignored.
|
|
||||||
/// </remarks>
|
|
||||||
public List<X509Certificate2>? AdditionalCustomTrustCertificates { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to retrieve a custom remote certificate validation callback.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="callback"></param>
|
|
||||||
/// <returns>Returns <see langword="true"/> when we have custom remote certification that should be added,
|
|
||||||
/// <see langword="false"/> when no custom validation is needed and the default validation callback should
|
|
||||||
/// be used instead.
|
|
||||||
/// </returns>
|
|
||||||
[MemberNotNullWhen(true, nameof(AdditionalCustomTrustCertificates))]
|
|
||||||
public bool TryGetCustomRemoteCertificateValidationCallback(
|
|
||||||
[MaybeNullWhen(false)] out Func<X509Certificate2?, X509Chain?, SslPolicyErrors, bool> callback)
|
|
||||||
{
|
|
||||||
callback = null;
|
|
||||||
if (AdditionalCustomTrustCertificates == null || AdditionalCustomTrustCertificates.Count == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do this outside of the callback so that we aren't opening the root store every request.
|
|
||||||
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine, OpenFlags.ReadOnly);
|
|
||||||
var rootCertificates = store.Certificates;
|
|
||||||
|
|
||||||
// Ref: https://github.com/dotnet/runtime/issues/39835#issuecomment-663020581
|
|
||||||
callback = (certificate, chain, errors) =>
|
|
||||||
{
|
|
||||||
if (chain == null || certificate == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
|
||||||
|
|
||||||
// We want our additional certificates to be in addition to the machines root store.
|
|
||||||
chain.ChainPolicy.CustomTrustStore.AddRange(rootCertificates);
|
|
||||||
|
|
||||||
foreach (var additionalCertificate in AdditionalCustomTrustCertificates)
|
|
||||||
{
|
|
||||||
chain.ChainPolicy.CustomTrustStore.Add(additionalCertificate);
|
|
||||||
}
|
|
||||||
return chain.Build(certificate);
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
@@ -16,14 +13,12 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService
|
|||||||
{
|
{
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ILogger<MailKitSmtpMailDeliveryService> _logger;
|
private readonly ILogger<MailKitSmtpMailDeliveryService> _logger;
|
||||||
private readonly X509ChainOptions _x509ChainOptions;
|
|
||||||
private readonly string _replyDomain;
|
private readonly string _replyDomain;
|
||||||
private readonly string _replyEmail;
|
private readonly string _replyEmail;
|
||||||
|
|
||||||
public MailKitSmtpMailDeliveryService(
|
public MailKitSmtpMailDeliveryService(
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILogger<MailKitSmtpMailDeliveryService> logger,
|
ILogger<MailKitSmtpMailDeliveryService> logger)
|
||||||
IOptions<X509ChainOptions> x509ChainOptions)
|
|
||||||
{
|
{
|
||||||
if (globalSettings.Mail.Smtp?.Host == null)
|
if (globalSettings.Mail.Smtp?.Host == null)
|
||||||
{
|
{
|
||||||
@@ -44,7 +39,6 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService
|
|||||||
|
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_x509ChainOptions = x509ChainOptions.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmailAsync(Models.Mail.MailMessage message)
|
public async Task SendEmailAsync(Models.Mail.MailMessage message)
|
||||||
@@ -89,13 +83,6 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService
|
|||||||
{
|
{
|
||||||
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
|
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
|
||||||
}
|
}
|
||||||
else if (_x509ChainOptions.TryGetCustomRemoteCertificateValidationCallback(out var callback))
|
|
||||||
{
|
|
||||||
client.ServerCertificateValidationCallback = (sender, cert, chain, errors) =>
|
|
||||||
{
|
|
||||||
return callback(new X509Certificate2(cert), chain, errors);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_globalSettings.Mail.Smtp.StartTls && !_globalSettings.Mail.Smtp.Ssl &&
|
if (!_globalSettings.Mail.Smtp.StartTls && !_globalSettings.Mail.Smtp.Ssl &&
|
||||||
_globalSettings.Mail.Smtp.Port == 25)
|
_globalSettings.Mail.Smtp.Port == 25)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using Bit.Core.Models.Mail;
|
using Bit.Core.Models.Mail;
|
||||||
using Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Rnwood.SmtpServer;
|
using Rnwood.SmtpServer;
|
||||||
using Rnwood.SmtpServer.Extensions.Auth;
|
using Rnwood.SmtpServer.Extensions.Auth;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
@@ -104,8 +102,7 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
||||||
globalSettings,
|
globalSettings,
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
NullLogger<MailKitSmtpMailDeliveryService>.Instance
|
||||||
Options.Create(new X509ChainOptions())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<SslHandshakeException>(
|
await Assert.ThrowsAsync<SslHandshakeException>(
|
||||||
@@ -118,117 +115,6 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_Works()
|
|
||||||
{
|
|
||||||
// If an SMTP server is using a self signed cert we will in the future
|
|
||||||
// allow a custom location for certificates to be stored and the certitifactes
|
|
||||||
// stored there will also be trusted.
|
|
||||||
var port = RandomPort();
|
|
||||||
var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);
|
|
||||||
using var smtpServer = new SmtpServer(behavior);
|
|
||||||
smtpServer.Start();
|
|
||||||
|
|
||||||
var globalSettings = GetSettings(gs =>
|
|
||||||
{
|
|
||||||
gs.Mail.Smtp.Port = port;
|
|
||||||
gs.Mail.Smtp.Ssl = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
var x509ChainOptions = new X509ChainOptions
|
|
||||||
{
|
|
||||||
AdditionalCustomTrustCertificates =
|
|
||||||
[
|
|
||||||
_selfSignedCert,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
|
||||||
globalSettings,
|
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
|
||||||
Options.Create(x509ChainOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
|
||||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
cts.Token.Register(() => _ = tcs.TrySetCanceled());
|
|
||||||
|
|
||||||
behavior.MessageReceivedEventHandler += (sender, args) =>
|
|
||||||
{
|
|
||||||
if (args.Message.Recipients.Contains("test1@example.com"))
|
|
||||||
{
|
|
||||||
tcs.SetResult();
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
await mailKitDeliveryService.SendEmailAsync(new MailMessage
|
|
||||||
{
|
|
||||||
Subject = "Test",
|
|
||||||
ToEmails = ["test1@example.com"],
|
|
||||||
TextContent = "Hi",
|
|
||||||
}, cts.Token);
|
|
||||||
|
|
||||||
// Wait for email
|
|
||||||
await tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_WithUnrelatedCerts_Works()
|
|
||||||
{
|
|
||||||
// If an SMTP server is using a self signed cert we will in the future
|
|
||||||
// allow a custom location for certificates to be stored and the certitifactes
|
|
||||||
// stored there will also be trusted.
|
|
||||||
var port = RandomPort();
|
|
||||||
var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);
|
|
||||||
using var smtpServer = new SmtpServer(behavior);
|
|
||||||
smtpServer.Start();
|
|
||||||
|
|
||||||
var globalSettings = GetSettings(gs =>
|
|
||||||
{
|
|
||||||
gs.Mail.Smtp.Port = port;
|
|
||||||
gs.Mail.Smtp.Ssl = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
var x509ChainOptions = new X509ChainOptions
|
|
||||||
{
|
|
||||||
AdditionalCustomTrustCertificates =
|
|
||||||
[
|
|
||||||
_selfSignedCert,
|
|
||||||
CreateSelfSignedCert("example.com"),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
|
||||||
globalSettings,
|
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
|
||||||
Options.Create(x509ChainOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
|
||||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
cts.Token.Register(() => _ = tcs.TrySetCanceled());
|
|
||||||
|
|
||||||
behavior.MessageReceivedEventHandler += (sender, args) =>
|
|
||||||
{
|
|
||||||
if (args.Message.Recipients.Contains("test1@example.com"))
|
|
||||||
{
|
|
||||||
tcs.SetResult();
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
await mailKitDeliveryService.SendEmailAsync(new MailMessage
|
|
||||||
{
|
|
||||||
Subject = "Test",
|
|
||||||
ToEmails = ["test1@example.com"],
|
|
||||||
TextContent = "Hi",
|
|
||||||
}, cts.Token);
|
|
||||||
|
|
||||||
// Wait for email
|
|
||||||
await tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendEmailAsync_Succeeds_WhenCertIsSelfSigned_ServerIsTrusted()
|
public async Task SendEmailAsync_Succeeds_WhenCertIsSelfSigned_ServerIsTrusted()
|
||||||
{
|
{
|
||||||
@@ -249,8 +135,7 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
||||||
globalSettings,
|
globalSettings,
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
NullLogger<MailKitSmtpMailDeliveryService>.Instance
|
||||||
Options.Create(new X509ChainOptions())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource();
|
||||||
@@ -296,8 +181,7 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
||||||
globalSettings,
|
globalSettings,
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
NullLogger<MailKitSmtpMailDeliveryService>.Instance
|
||||||
Options.Create(new X509ChainOptions())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
@@ -332,8 +216,7 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
||||||
globalSettings,
|
globalSettings,
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
NullLogger<MailKitSmtpMailDeliveryService>.Instance
|
||||||
Options.Create(new X509ChainOptions())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource();
|
||||||
@@ -399,8 +282,7 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
|
|
||||||
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
|
||||||
globalSettings,
|
globalSettings,
|
||||||
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
|
NullLogger<MailKitSmtpMailDeliveryService>.Instance
|
||||||
Options.Create(new X509ChainOptions())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource();
|
||||||
|
|||||||
@@ -1,359 +0,0 @@
|
|||||||
using System.Security.Authentication;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Platform.X509ChainCustomization;
|
|
||||||
|
|
||||||
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) =>
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
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 services = CreateServices((gs, environment, config) =>
|
|
||||||
{
|
|
||||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName;
|
|
||||||
}, services =>
|
|
||||||
{
|
|
||||||
services.Configure<X509ChainOptions>(options =>
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
|
|
||||||
|
|
||||||
Assert.True(options.TryGetCustomRemoteCertificateValidationCallback(out var callback));
|
|
||||||
var cert = Assert.Single(options.AdditionalCustomTrustCertificates);
|
|
||||||
Assert.Equal("CN=example.com", cert.Subject);
|
|
||||||
|
|
||||||
var fakeLogCollector = services.GetFakeLogCollector();
|
|
||||||
|
|
||||||
Assert.Contains(fakeLogCollector.GetSnapshot(),
|
|
||||||
r => r.Message == $"Additional custom trust certificates were added directly, skipping loading them from '{tempDir}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NullCustomDirectory_SkipsTryingToLoad()
|
|
||||||
{
|
|
||||||
var services = CreateServices((gs, environment, config) =>
|
|
||||||
{
|
|
||||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
|
|
||||||
|
|
||||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("Development", LogLevel.Debug)]
|
|
||||||
[InlineData("Production", LogLevel.Warning)]
|
|
||||||
public void CustomDirectoryDoesNotExist_Logs(string environment, LogLevel logLevel)
|
|
||||||
{
|
|
||||||
var fakeDir = "/fake/dir/that/does/not/exist";
|
|
||||||
var services = CreateServices((gs, hostEnvironment, config) =>
|
|
||||||
{
|
|
||||||
hostEnvironment.EnvironmentName = environment;
|
|
||||||
|
|
||||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = fakeDir;
|
|
||||||
});
|
|
||||||
|
|
||||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
|
|
||||||
|
|
||||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _));
|
|
||||||
|
|
||||||
var fakeLogCollector = services.GetFakeLogCollector();
|
|
||||||
|
|
||||||
Assert.Contains(fakeLogCollector.GetSnapshot(),
|
|
||||||
r => r.Message == $"An additional custom trust certificate directory was given '{fakeDir}' but that directory does not exist."
|
|
||||||
&& r.Level == logLevel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task NamedOptions_NotConfiguredAsync()
|
|
||||||
{
|
|
||||||
// To help make sure this fails for the right reason we should add certs to the directory
|
|
||||||
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) =>
|
|
||||||
{
|
|
||||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName;
|
|
||||||
});
|
|
||||||
|
|
||||||
var options = services.GetRequiredService<IOptionsMonitor<X509ChainOptions>>();
|
|
||||||
|
|
||||||
var namedOptions = options.Get("SomeName");
|
|
||||||
|
|
||||||
Assert.Null(namedOptions.AdditionalCustomTrustCertificates);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CustomLocation_NoCertificates_Logs()
|
|
||||||
{
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("certs");
|
|
||||||
var services = CreateServices((gs, hostEnvironment, config) =>
|
|
||||||
{
|
|
||||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName;
|
|
||||||
});
|
|
||||||
|
|
||||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value;
|
|
||||||
|
|
||||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _));
|
|
||||||
|
|
||||||
var fakeLogCollector = services.GetFakeLogCollector();
|
|
||||||
|
|
||||||
Assert.Contains(fakeLogCollector.GetSnapshot(),
|
|
||||||
r => r.Message == $"No additional custom trust certificates were found in '{tempDir.FullName}'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateConfigured_Works()
|
|
||||||
{
|
|
||||||
var selfSignedCertificate = CreateSelfSignedCert("localhost");
|
|
||||||
await using var app = await CreateServerAsync(55555, options =>
|
|
||||||
{
|
|
||||||
options.ServerCertificate = selfSignedCertificate;
|
|
||||||
});
|
|
||||||
|
|
||||||
var services = CreateServices((gs, environment, config) => { }, services =>
|
|
||||||
{
|
|
||||||
services.Configure<X509ChainOptions>(options =>
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificates = [selfSignedCertificate];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient();
|
|
||||||
|
|
||||||
var response = await httpClient.GetStringAsync("https://localhost:55555");
|
|
||||||
Assert.Equal("Hi", response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateNotConfigured_Throws()
|
|
||||||
{
|
|
||||||
var selfSignedCertificate = CreateSelfSignedCert("localhost");
|
|
||||||
await using var app = await CreateServerAsync(55556, options =>
|
|
||||||
{
|
|
||||||
options.ServerCertificate = selfSignedCertificate;
|
|
||||||
});
|
|
||||||
|
|
||||||
var services = CreateServices((gs, environment, config) => { }, services =>
|
|
||||||
{
|
|
||||||
services.Configure<X509ChainOptions>(options =>
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient();
|
|
||||||
|
|
||||||
var requestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await httpClient.GetStringAsync("https://localhost:55556"));
|
|
||||||
Assert.NotNull(requestException.InnerException);
|
|
||||||
var authenticationException = Assert.IsAssignableFrom<AuthenticationException>(requestException.InnerException);
|
|
||||||
Assert.Equal("The remote certificate was rejected by the provided RemoteCertificateValidationCallback.", authenticationException.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateConfigured_WithExtraCert_Works()
|
|
||||||
{
|
|
||||||
var selfSignedCertificate = CreateSelfSignedCert("localhost");
|
|
||||||
await using var app = await CreateServerAsync(55557, options =>
|
|
||||||
{
|
|
||||||
options.ServerCertificate = selfSignedCertificate;
|
|
||||||
});
|
|
||||||
|
|
||||||
var services = CreateServices((gs, environment, config) => { }, services =>
|
|
||||||
{
|
|
||||||
services.Configure<X509ChainOptions>(options =>
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificates = [selfSignedCertificate, CreateSelfSignedCert("example.com")];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient();
|
|
||||||
|
|
||||||
var response = await httpClient.GetStringAsync("https://localhost:55557");
|
|
||||||
Assert.Equal("Hi", response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CallHttp_ReachingOutToServerTrustedThroughSystemCA()
|
|
||||||
{
|
|
||||||
var services = CreateServices((gs, environment, config) => { }, services =>
|
|
||||||
{
|
|
||||||
services.Configure<X509ChainOptions>(options =>
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificates = [];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient();
|
|
||||||
|
|
||||||
var response = await httpClient.GetAsync("https://example.com");
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CallHttpWithCustomTrustForSelfSigned_ReachingOutToServerTrustedThroughSystemCA()
|
|
||||||
{
|
|
||||||
var selfSignedCertificate = CreateSelfSignedCert("localhost");
|
|
||||||
var services = CreateServices((gs, environment, config) => { }, services =>
|
|
||||||
{
|
|
||||||
services.Configure<X509ChainOptions>(options =>
|
|
||||||
{
|
|
||||||
options.AdditionalCustomTrustCertificates = [selfSignedCertificate];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient();
|
|
||||||
|
|
||||||
var response = await httpClient.GetAsync("https://example.com");
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IAsyncDisposable> CreateServerAsync(int port, Action<HttpsConnectionAdapterOptions> configure)
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions());
|
|
||||||
builder.Services.AddRoutingCore();
|
|
||||||
builder.WebHost.UseKestrelCore()
|
|
||||||
.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.ListenLocalhost(port, listenOptions =>
|
|
||||||
{
|
|
||||||
listenOptions.UseHttps(httpsOptions =>
|
|
||||||
{
|
|
||||||
configure(httpsOptions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
app.MapGet("/", () => "Hi");
|
|
||||||
|
|
||||||
await app.StartAsync();
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
// A solid default for these tests as these settings aren't allowed to work in cloud.
|
|
||||||
SelfHosted = true,
|
|
||||||
};
|
|
||||||
var hostEnvironment = Substitute.For<IHostEnvironment>();
|
|
||||||
hostEnvironment.EnvironmentName = "Development";
|
|
||||||
var config = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
configure(globalSettings, hostEnvironment, config);
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddLogging(logging =>
|
|
||||||
{
|
|
||||||
logging.SetMinimumLevel(LogLevel.Debug);
|
|
||||||
logging.AddFakeLogging();
|
|
||||||
});
|
|
||||||
services.AddSingleton(globalSettings);
|
|
||||||
services.AddSingleton(hostEnvironment);
|
|
||||||
services.AddSingleton<IConfiguration>(
|
|
||||||
new ConfigurationBuilder()
|
|
||||||
.AddInMemoryCollection(config)
|
|
||||||
.Build()
|
|
||||||
);
|
|
||||||
|
|
||||||
services.AddX509ChainCustomization();
|
|
||||||
|
|
||||||
after?.Invoke(services);
|
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,9 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Models.Business;
|
using Bit.Core.Auth.Models.Business;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Platform.X509ChainCustomization;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -138,7 +136,7 @@ public class HandlebarsMailServiceTests
|
|||||||
SiteName = "Bitwarden",
|
SiteName = "Bitwarden",
|
||||||
};
|
};
|
||||||
|
|
||||||
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>(), Options.Create(new X509ChainOptions()));
|
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
|
||||||
|
|
||||||
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService());
|
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService());
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using Bit.Core.Platform.X509ChainCustomization;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -25,8 +23,7 @@ public class MailKitSmtpMailDeliveryServiceTests
|
|||||||
|
|
||||||
_sut = new MailKitSmtpMailDeliveryService(
|
_sut = new MailKitSmtpMailDeliveryService(
|
||||||
_globalSettings,
|
_globalSettings,
|
||||||
_logger,
|
_logger
|
||||||
Options.Create(new X509ChainOptions())
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user