From 5de31f51f55b47fecb3efd164a52d3f7af7c416f Mon Sep 17 00:00:00 2001 From: Tamer Wahba Date: Sat, 2 May 2026 17:23:54 -0400 Subject: [PATCH] refactor ssl certificate loading into common helper --- .../Http/SslCertificateLoader.cs | 34 +++++++++++++++++++ src/NzbDrone.Host/Bootstrap.cs | 19 +++-------- .../Config/CertificateValidator.cs | 26 ++++---------- 3 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 src/NzbDrone.Common/Http/SslCertificateLoader.cs 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 2c48ea3543..b205f5cf19 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -2,11 +2,9 @@ using System.Collections.Generic; using System.Data.SQLite; using System.IO; -using System.Linq; 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; @@ -21,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; @@ -282,11 +281,11 @@ private static string BuildUrl(string scheme, string bindAddress, int port) private static SslStreamCertificateContext ValidateSslCertificate(string cert, string password) { - var certificateCollection = new X509Certificate2Collection(); + SslStreamCertificateContext certificateContext; try { - certificateCollection.Import(cert, password, X509KeyStorageFlags.DefaultKeySet); + certificateContext = SslCertificateLoader.LoadCertificateContext(cert, password); } catch (CryptographicException ex) { @@ -299,17 +298,7 @@ private static SslStreamCertificateContext ValidateSslCertificate(string cert, s throw new RadarrStartupException(ex); } - var leafCert = certificateCollection.FirstOrDefault(c => c.HasPrivateKey); - - if (leafCert == null) - { - throw new RadarrStartupException( - $"The SSL certificate file {cert} does not contain a certificate with an associated private key"); - } - - certificateCollection.Remove(leafCert); - - return SslStreamCertificateContext.Create(leafCert, certificateCollection, offline: true); + return certificateContext; } } } diff --git a/src/Radarr.Api.V3/Config/CertificateValidator.cs b/src/Radarr.Api.V3/Config/CertificateValidator.cs index 9161deb77c..77c59b91df 100644 --- a/src/Radarr.Api.V3/Config/CertificateValidator.cs +++ b/src/Radarr.Api.V3/Config/CertificateValidator.cs @@ -1,9 +1,8 @@ -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; +using System; using FluentValidation; using FluentValidation.Validators; using NLog; -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; namespace Radarr.Api.V3.Config @@ -34,13 +33,13 @@ protected override bool IsValid(PropertyValidatorContext context) return true; } - var certificateCollection = new X509Certificate2Collection(); - try { - certificateCollection.Import(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); @@ -48,19 +47,6 @@ protected override bool IsValid(PropertyValidatorContext context) return false; } - - if (certificateCollection.None(c => c.HasPrivateKey)) - { - var message = $"The SSL certificate file {resource.SslCertPath} does not contain a certificate with an associated private key"; - - Logger.Debug($"{message}"); - - context.MessageFormatter.AppendArgument("message", message); - - return false; - } - - return true; } } }