HTTPS certificate validation options

New: Enable HTTPS certificate validation by default
New: Option to disable certificate validation for all or only local addresses
pull/1689/head
Mark McDowall 6 years ago committed by Qstick
parent 50ac0c8235
commit 37137b0c10

@ -10,6 +10,18 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
];
const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
{ key: 'disabled', value: 'Disabled' }
];
class SecuritySettings extends Component {
//
@ -57,15 +69,10 @@ class SecuritySettings extends Component {
authenticationMethod,
username,
password,
apiKey
apiKey,
certificateValidation
} = settings;
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
];
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
return (
@ -146,6 +153,19 @@ class SecuritySettings extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>Certificate Validation</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="certificateValidation"
values={certificateValidationOptions}
helpText="Change how strict HTTPS certification validation is"
onChange={onInputChange}
{...certificateValidation}
/>
</FormGroup>
<ConfirmModal
isOpen={this.state.isConfirmApiKeyResetModalOpen}
kind={kinds.DANGER}

@ -1,6 +1,7 @@
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Core.Update;
using Lidarr.Http.REST;
@ -34,6 +35,7 @@ namespace Lidarr.Api.V1.Config
public string ProxyPassword { get; set; }
public string ProxyBypassFilter { get; set; }
public bool ProxyBypassLocalAddresses { get; set; }
public CertificateValidationType CertificateValidation { get; set; }
public string BackupFolder { get; set; }
public int BackupInterval { get; set; }
public int BackupRetention { get; set; }
@ -72,6 +74,7 @@ namespace Lidarr.Api.V1.Config
ProxyPassword = configService.ProxyPassword,
ProxyBypassFilter = configService.ProxyBypassFilter,
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
CertificateValidation = configService.CertificateValidation,
BackupFolder = configService.BackupFolder,
BackupInterval = configService.BackupInterval,
BackupRetention = configService.BackupRetention

@ -0,0 +1,30 @@
using System.Net;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests
{
[TestFixture]
public class IPAddressExtensionsFixture
{
[TestCase("::1")]
[TestCase("10.64.5.1")]
[TestCase("127.0.0.1")]
[TestCase("172.16.0.1")]
[TestCase("192.168.5.1")]
public void should_return_true_for_local_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeTrue();
}
[TestCase("1.2.3.4")]
[TestCase("172.55.0.1")]
[TestCase("192.55.0.1")]
public void should_return_false_for_public_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
}
}
}

@ -0,0 +1,29 @@
using System.Net;
namespace NzbDrone.Common.Extensions
{
public static class IPAddressExtensions
{
public static bool IsLocalAddress(this IPAddress ipAddress)
{
if (ipAddress.ToString() == "::1")
{
return true;
}
byte[] bytes = ipAddress.GetAddressBytes();
switch (bytes[0])
{
case 10:
case 127:
return true;
case 172:
return bytes[1] < 32 && bytes[1] >= 16;
case 192:
return bytes[1] == 168;
default:
return false;
}
}
}
}

@ -9,7 +9,6 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Security;
namespace NzbDrone.Common.Http.Dispatchers
{
@ -83,11 +82,6 @@ namespace NzbDrone.Common.Http.Dispatchers
}
catch (WebException e)
{
if (e.Status == WebExceptionStatus.SecureChannelFailure && OsInfo.IsWindows)
{
SecurityProtocolPolicy.DisableTls12();
}
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)

@ -1,66 +0,0 @@
using System;
using System.Net;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Security
{
public static class SecurityProtocolPolicy
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(SecurityProtocolPolicy));
private const SecurityProtocolType Tls11 = (SecurityProtocolType)768;
private const SecurityProtocolType Tls12 = (SecurityProtocolType)3072;
public static void Register()
{
if (OsInfo.IsNotWindows)
{
// This was never meant to be used on mono, and will cause issues with mono 5 and higher if btls is enabled.
return;
}
try
{
// TODO: In v3 we should drop support for SSL3 because its very insecure. Only leaving it enabled because some people might rely on it.
var protocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls;
if (Enum.IsDefined(typeof(SecurityProtocolType), Tls11))
{
protocol |= Tls11;
}
// Enabling Tls1.2 invalidates certificates using md5, so we disable Tls12 on the fly if that happens.
if (Enum.IsDefined(typeof(SecurityProtocolType), Tls12))
{
protocol |= Tls12;
}
ServicePointManager.SecurityProtocol = protocol;
}
catch (Exception ex)
{
Logger.Debug(ex, "Failed to set TLS security protocol.");
}
}
public static void DisableTls12()
{
try
{
var protocol = ServicePointManager.SecurityProtocol;
if (protocol.HasFlag(Tls12))
{
Logger.Warn("Disabled Tls1.2 due to remote certificate error.");
ServicePointManager.SecurityProtocol = protocol & ~Tls12;
}
}
catch (Exception ex)
{
Logger.Debug(ex, "Failed to disable TLS 1.2 security protocol.");
}
}
}
}

@ -1,44 +0,0 @@
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Security
{
public static class X509CertificateValidationPolicy
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(X509CertificateValidationPolicy));
public static void Register()
{
ServicePointManager.ServerCertificateValidationCallback = ShouldByPassValidationError;
}
private static bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var request = sender as HttpWebRequest;
if (request == null)
{
return true;
}
var req = sender as HttpWebRequest;
var cert2 = certificate as X509Certificate2;
if (cert2 != null && req != null && cert2.SignatureAlgorithm.FriendlyName == "md5RSA")
{
Logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", req.RequestUri.Authority);
}
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
Logger.Debug("Certificate validation for {0} failed. {1}", request.Address, sslPolicyErrors);
return true;
}
}
}

@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Security;
namespace NzbDrone.Core.Configuration
{
@ -409,6 +410,9 @@ namespace NzbDrone.Core.Configuration
public int BackupRetention => GetValueInt("BackupRetention", 28);
public CertificateValidationType CertificateValidation =>
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
private string GetValue(string key)
{
return GetValue(key, string.Empty);

@ -2,6 +2,7 @@ using System.Collections.Generic;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Security;
namespace NzbDrone.Core.Configuration
{
@ -97,5 +98,6 @@ namespace NzbDrone.Core.Configuration
int BackupInterval { get; }
int BackupRetention { get; }
CertificateValidationType CertificateValidation { get; }
}
}

@ -4,7 +4,7 @@ using System.Text;
namespace NzbDrone.Core
{
public static class Security
public static class Hashing
{
public static string SHA256Hash(this string input)
{

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Security
{
public enum CertificateValidationType
{
Enabled = 0,
DisabledForLocalAddresses = 1,
Disabled = 2
}
}

@ -0,0 +1,72 @@
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Security
{
public interface IX509CertificateValidationPolicy
{
void Register();
}
public class X509CertificateValidationPolicy : IX509CertificateValidationPolicy
{
private readonly IConfigService _configService;
private readonly Logger _logger;
public X509CertificateValidationPolicy(IConfigService configService, Logger logger)
{
_configService = configService;
_logger = logger;
}
public void Register()
{
ServicePointManager.ServerCertificateValidationCallback = ShouldByPassValidationError;
}
private bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var request = sender as HttpWebRequest;
if (request == null)
{
return true;
}
var req = sender as HttpWebRequest;
var cert2 = certificate as X509Certificate2;
if (cert2 != null && req != null && cert2.SignatureAlgorithm.FriendlyName == "md5RSA")
{
_logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", req.RequestUri.Authority);
}
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
var host = Dns.GetHostEntry(req.Host);
var certificateValidation = _configService.CertificateValidation;
if (certificateValidation == CertificateValidationType.Disabled)
{
return true;
}
if (certificateValidation == CertificateValidationType.DisabledForLocalAddresses && host.AddressList.All(i => i.IsLocalAddress()))
{
return true;
}
_logger.Error("Certificate validation for {0} failed. {1}", request.Address, sslPolicyErrors);
return false;
}
}
}

@ -8,9 +8,9 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes;
using NzbDrone.Common.Security;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Security;
namespace NzbDrone.Host
{
@ -23,9 +23,6 @@ namespace NzbDrone.Host
{
try
{
SecurityProtocolPolicy.Register();
X509CertificateValidationPolicy.Register();
Logger.Info("Starting Lidarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version);
if (!PlatformValidation.IsValidate(userAlert))
@ -43,6 +40,7 @@ namespace NzbDrone.Host
var appMode = GetApplicationMode(startupContext);
Start(appMode, startupContext);
_container.Resolve<IX509CertificateValidationPolicy>().Register();
if (startCallback != null)
{

@ -7,7 +7,6 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes;
using NzbDrone.Common.Security;
using NzbDrone.Update.UpdateEngine;
namespace NzbDrone.Update
@ -30,9 +29,6 @@ namespace NzbDrone.Update
{
try
{
SecurityProtocolPolicy.Register();
X509CertificateValidationPolicy.Register();
var startupContext = new StartupContext(args);
NzbDroneLogger.Register(startupContext, true, true);

Loading…
Cancel
Save