mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05:00
[PM-19601] Introduce options for adding certificates to trust without root (#5609)
* Introduce options for adding certificates to the X509ChainPolicy.CustomTrustStore Co-authored-by: tangowithfoxtrot <tangowithfoxtrot@users.noreply.github.com> * Add comments * Fix places I am still calling it TLS options * Format * Format from root * Add more tests * Add HTTP Tests * Format * Switch to empty builder * Remove unneeded helper * Configure logging only once --------- Co-authored-by: tangowithfoxtrot <tangowithfoxtrot@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,96 @@
|
||||
#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;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
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;
|
||||
}
|
||||
}
|
73
src/Core/Platform/X509ChainCustomization/X509ChainOptions.cs
Normal file
73
src/Core/Platform/X509ChainCustomization/X509ChainOptions.cs
Normal file
@ -0,0 +1,73 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
foreach (var additionalCertificate in AdditionalCustomTrustCertificates)
|
||||
{
|
||||
chain.ChainPolicy.CustomTrustStore.Add(additionalCertificate);
|
||||
}
|
||||
return chain.Build(certificate);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
using Bit.Core.Settings;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Bit.Core.Platform.X509ChainCustomization;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using MailKit.Net.Smtp;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
@ -10,12 +13,14 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ILogger<MailKitSmtpMailDeliveryService> _logger;
|
||||
private readonly X509ChainOptions _x509ChainOptions;
|
||||
private readonly string _replyDomain;
|
||||
private readonly string _replyEmail;
|
||||
|
||||
public MailKitSmtpMailDeliveryService(
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<MailKitSmtpMailDeliveryService> logger)
|
||||
ILogger<MailKitSmtpMailDeliveryService> logger,
|
||||
IOptions<X509ChainOptions> x509ChainOptions)
|
||||
{
|
||||
if (globalSettings.Mail?.Smtp?.Host == null)
|
||||
{
|
||||
@ -31,6 +36,7 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService
|
||||
|
||||
_globalSettings = globalSettings;
|
||||
_logger = logger;
|
||||
_x509ChainOptions = x509ChainOptions.Value;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(Models.Mail.MailMessage message)
|
||||
@ -75,6 +81,13 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService
|
||||
{
|
||||
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 &&
|
||||
_globalSettings.Mail.Smtp.Port == 25)
|
||||
|
Reference in New Issue
Block a user