From 12f53513d0bd7a48782264ffa500dc293cb2591c Mon Sep 17 00:00:00 2001 From: "Marcelo C." Date: Fri, 14 Mar 2025 12:09:01 -0300 Subject: [PATCH] Re-work of SSL Validation + Root Certificate feature Initially the idea was to allow scanning not only the certificate but scan the whole certificate chain for a explicitly supplied hash to be accepted unconditionally. A review of the validation revealed it was always missing a fallback to the certificate.verify() Also decided that even when a hash is supplied and matched, the validity date supersedes other criteria except the acceptAll. --- .../Utility/SslCertificateValidator.cs | 100 +++++++++--------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/Duplicati/Library/Utility/SslCertificateValidator.cs b/Duplicati/Library/Utility/SslCertificateValidator.cs index e21e36469..41e62bd6b 100644 --- a/Duplicati/Library/Utility/SslCertificateValidator.cs +++ b/Duplicati/Library/Utility/SslCertificateValidator.cs @@ -1,25 +1,26 @@ -// Copyright (C) 2025, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; +using System.Linq; using System.Net.Security; using System.Security.Cryptography.X509Certificates; @@ -28,52 +29,55 @@ namespace Duplicati.Library.Utility; public class SslCertificateValidator(bool acceptAll, string[] validHashes) { [Serializable] - public class InvalidCertificateException : Exception + public class InvalidCertificateException(string certificate, SslPolicyErrors error) + : Exception(Strings.SslCertificateValidator.VerifyCertificateException(error, certificate)) { - private readonly string m_certificate; - private readonly SslPolicyErrors m_errors = SslPolicyErrors.None; + private readonly string _mCertificate = certificate; + private readonly SslPolicyErrors _mErrors = error; - public string Certificate => m_certificate; - public SslPolicyErrors SslError => m_errors; - - public InvalidCertificateException(string certificate, SslPolicyErrors error) - : base(Strings.SslCertificateValidator.VerifyCertificateException(error, certificate)) - { - m_certificate = certificate; - m_errors = error; - } + public string Certificate => _mCertificate; + public SslPolicyErrors SslError => _mErrors; } public bool ValidateServerCertificate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - - if (sslPolicyErrors == SslPolicyErrors.None) - return true; - if (acceptAll) return true; - - string certHash; - + try { - certHash = Utility.ByteArrayAsHexString(cert.GetCertHash()); - if (certHash != null && validHashes != null) + DateTime now = DateTime.Now; + + using var certificate = cert as X509Certificate2 ?? new X509Certificate2(cert); + + if (!IsDateValid(certificate, now)) + return false; + + if (validHashes != null) { - foreach (var hash in validHashes) - { - if (!string.IsNullOrEmpty(hash) && certHash.Equals(hash, StringComparison.OrdinalIgnoreCase)) - { + // Check main certificate hash + if (IsTrustedHash(Utility.ByteArrayAsHexString(certificate.GetCertHash()))) + return true; + + // Check chain certificate from root for the hash (this allows custom CA certificates hashes to be added) + if (chain?.ChainElements != null) + if (chain.ChainElements.Any(element => IsTrustedHash(Utility.ByteArrayAsHexString(element.Certificate.GetCertHash())) && IsDateValid(element.Certificate, now))) return true; - } - } } + + // If no hash is found, perform the standard validations + return sslPolicyErrors == SslPolicyErrors.None && certificate.Verify(); } catch (Exception ex) { throw new Exception(Strings.SslCertificateValidator.VerifyCertificateHashError(ex, sslPolicyErrors), ex); } - - return false; } + + private bool IsTrustedHash(string hash) => + !string.IsNullOrWhiteSpace(hash) && + validHashes.Any(validHash => !string.IsNullOrEmpty(validHash) && + hash.Equals(validHash, StringComparison.OrdinalIgnoreCase)); + private bool IsDateValid(X509Certificate2 cert, DateTime now) => now <= cert.NotAfter && now >= cert.NotBefore; + } \ No newline at end of file