diff --git a/src/NzbDrone.Common/Http/SslCertificateLoader.cs b/src/NzbDrone.Common/Http/SslCertificateLoader.cs new file mode 100644 index 0000000000..06daedd237 --- /dev/null +++ b/src/NzbDrone.Common/Http/SslCertificateLoader.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace NzbDrone.Common.Http +{ + public class SslCertificateLoadException : Exception + { + public SslCertificateLoadException(string message) + : base(message) + { + } + } + + public static class SslCertificateLoader + { + public static SslStreamCertificateContext LoadCertificateContext(string certPath, string certPassword) + { + var certificateCollection = new X509Certificate2Collection(); + + certificateCollection.Import(certPath, certPassword, X509KeyStorageFlags.DefaultKeySet); + + var leafCert = certificateCollection.FirstOrDefault(c => c.HasPrivateKey); + if (leafCert == null) + { + throw new SslCertificateLoadException( + $"The SSL certificate file {certPath} does not contain a certificate with an associated private key"); + } + + return SslStreamCertificateContext.Create(leafCert, certificateCollection, offline: true); + } + } +} diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 1d9309748b..b205f5cf19 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Data.SQLite; using System.IO; +using System.Net.Security; using System.Reflection; using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using System.Text; using DryIoc; using DryIoc.Microsoft.DependencyInjection; @@ -19,6 +19,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Options; @@ -192,7 +193,13 @@ public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContex { options.ConfigureHttpsDefaults(configureOptions => { - configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslCertPassword); + var sslContext = ValidateSslCertificate(sslCertPath, sslCertPassword); + + configureOptions.ServerCertificate = sslContext.TargetCertificate; + configureOptions.OnAuthenticate = (context, authOptions) => + { + authOptions.ServerCertificateContext = sslContext; + }; }); } }); @@ -272,13 +279,13 @@ private static string BuildUrl(string scheme, string bindAddress, int port) return $"{scheme}://{bindAddress}:{port}"; } - private static X509Certificate2 ValidateSslCertificate(string cert, string password) + private static SslStreamCertificateContext ValidateSslCertificate(string cert, string password) { - X509Certificate2 certificate; + SslStreamCertificateContext certificateContext; try { - certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet); + certificateContext = SslCertificateLoader.LoadCertificateContext(cert, password); } catch (CryptographicException ex) { @@ -291,7 +298,7 @@ private static X509Certificate2 ValidateSslCertificate(string cert, string passw throw new RadarrStartupException(ex); } - return certificate; + return certificateContext; } } } diff --git a/src/Radarr.Api.V3/Config/CertificateValidator.cs b/src/Radarr.Api.V3/Config/CertificateValidator.cs index 4f1878d8ee..77c59b91df 100644 --- a/src/Radarr.Api.V3/Config/CertificateValidator.cs +++ b/src/Radarr.Api.V3/Config/CertificateValidator.cs @@ -1,8 +1,8 @@ -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; +using System; using FluentValidation; using FluentValidation.Validators; using NLog; +using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; namespace Radarr.Api.V3.Config @@ -35,11 +35,11 @@ protected override bool IsValid(PropertyValidatorContext context) try { - new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + SslCertificateLoader.LoadCertificateContext(resource.SslCertPath, resource.SslCertPassword); return true; } - catch (CryptographicException ex) + catch (Exception ex) { Logger.Debug(ex, "Invalid SSL certificate file or password. {0}", ex.Message);