From 73683466b27d7532455f302523466109a03e8432 Mon Sep 17 00:00:00 2001 From: Konstantina Chremmou Date: Fri, 3 Jul 2026 14:55:26 +0100 Subject: [PATCH] Corrected certificate validation for HTTP calls This is XSA-498 / CVE-2026-42491. Signed-off-by: Konstantina Chremmou Reviewed-by: Mohammed Zahid diff --git a/ocaml/sdk-gen/csharp/autogen/src/HTTP.cs b/ocaml/sdk-gen/csharp/autogen/src/HTTP.cs index c9b07eba9..41609101a 100644 --- a/ocaml/sdk-gen/csharp/autogen/src/HTTP.cs +++ b/ocaml/sdk-gen/csharp/autogen/src/HTTP.cs @@ -203,7 +203,7 @@ namespace XenAPI /// Read HTTP headers, doing any redirects as necessary /// /// True if a redirect has occurred - headers will need to be resent. - private static bool ReadHttpHeaders(ref Stream stream, IWebProxy proxy, bool nodelay, int timeout_ms, List headers = null) + private static bool ReadHttpHeaders(ref Stream stream, IWebProxy proxy, RemoteCertificateValidationCallback callback, bool nodelay, int timeoutMs, List headers = null) { // read headers/fields string line = ReadLine(stream); @@ -277,7 +277,7 @@ namespace XenAPI string url = header == null ? "" : header.Substring(9).Trim(); Uri redirect = new Uri(url); stream.Close(); - stream = ConnectStream(redirect, proxy, nodelay, timeout_ms); + stream = ConnectStream(redirect, proxy, callback, nodelay, timeoutMs); return true; // headers need to be sent again default: @@ -309,20 +309,6 @@ namespace XenAPI return bits.Length < 2 ? 0 : Int32.Parse(bits[1]); } - public static bool UseSSL(Uri uri) - { - return uri.Scheme == "https" || uri.Port == DEFAULT_HTTPS_PORT; - } - - private static bool ValidateServerCertificate( - object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors) - { - return true; - } - /// /// Returns a secure MD5 hash of the given input string. /// @@ -475,45 +461,24 @@ namespace XenAPI /// /// /// + /// /// /// Timeout, in ms. 0 for no timeout. - public static Stream ConnectStream(Uri uri, IWebProxy proxy, bool nodelay, int timeoutMs) + private static Stream ConnectStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, bool nodelay, int timeoutMs) { if (proxy is IMockWebProxy mockProxy) return mockProxy.GetStream(uri); - Stream stream; - bool useProxy = proxy != null && !proxy.IsBypassed(uri); - - if (useProxy) - { - Uri proxyURI = proxy.GetProxy(uri); - stream = ConnectSocket(proxyURI, nodelay, timeoutMs); - } - else - { - stream = ConnectSocket(uri, nodelay, timeoutMs); - } + Stream stream = null; try { - if (useProxy) - { - string line = $"CONNECT {uri.Host}:{uri.Port} HTTP/1.0"; - WriteLine(line, stream); - WriteLine(stream); + stream = AuthenticateProxy(uri, proxy, callback, nodelay, timeoutMs); - List initialResponse = new List(); - ReadHttpHeaders(ref stream, proxy, nodelay, timeoutMs, initialResponse); - - AuthenticateProxy(ref stream, uri, proxy, nodelay, timeoutMs, initialResponse, line); - } - - if (UseSSL(uri)) + if (uri.Scheme == "https" || uri.Port == DEFAULT_HTTPS_PORT) { - SslStream sslStream = new SslStream(stream, false, ValidateServerCertificate, null); + SslStream sslStream = new SslStream(stream, false, callback, null); sslStream.AuthenticateAsClient("", null, SslProtocols.Tls12, true); - stream = sslStream; } @@ -521,25 +486,37 @@ namespace XenAPI } catch { - stream.Close(); + stream?.Close(); throw; } } - private static void AuthenticateProxy(ref Stream stream, Uri uri, IWebProxy proxy, bool nodelay, int timeoutMs, List initialResponse, string header) + private static Stream AuthenticateProxy(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, bool nodelay, int timeoutMs) { + if (proxy == null || proxy.IsBypassed(uri)) + return ConnectSocket(uri, nodelay, timeoutMs); + + Uri proxyUri = proxy.GetProxy(uri); + Stream stream = ConnectSocket(proxyUri, nodelay, timeoutMs); + + string header = $"CONNECT {uri.Host}:{uri.Port} HTTP/1.0"; + WriteLine(header, stream); + WriteLine(stream); + + var initialResponse = new List(); + ReadHttpHeaders(ref stream, proxy, callback, nodelay, timeoutMs, initialResponse); + // perform authentication only if proxy requires it List fields = initialResponse.FindAll(str => str.StartsWith("Proxy-Authenticate:", StringComparison.InvariantCultureIgnoreCase)); if (fields.Count <= 0) - return; + return stream; // clean up (if initial server response specifies "Proxy-Connection: Close" then stream cannot be re-used) string field = initialResponse.Find(str => str.StartsWith("Proxy-Connection: Close", StringComparison.InvariantCultureIgnoreCase)); if (!string.IsNullOrEmpty(field)) { stream.Close(); - Uri proxyURI = proxy.GetProxy(uri); - stream = ConnectSocket(proxyURI, nodelay, timeoutMs); + stream = ConnectSocket(proxyUri, nodelay, timeoutMs); } if (proxy.Credentials == null) @@ -675,7 +652,8 @@ namespace XenAPI // handle authentication attempt response List authenticatedResponse = new List(); - ReadHttpHeaders(ref stream, proxy, nodelay, timeoutMs, authenticatedResponse); + ReadHttpHeaders(ref stream, proxy, callback, nodelay, timeoutMs, authenticatedResponse); + if (authenticatedResponse.Count == 0) throw new BadServerResponseException("No response from the proxy server after authentication attempt."); @@ -688,11 +666,13 @@ namespace XenAPI default: throw new BadServerResponseException($"Received error code {authenticatedResponse[0]} from the server"); } + + return stream; } - private static Stream DoHttp(Uri uri, IWebProxy proxy, bool noDelay, int timeoutMs, params string[] headers) + private static Stream DoHttp(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, bool noDelay, int timeoutMs, params string[] headers) { - Stream stream = ConnectStream(uri, proxy, noDelay, timeoutMs); + Stream stream = ConnectStream(uri, proxy, callback, noDelay, timeoutMs); int redirects = 0; @@ -709,7 +689,7 @@ namespace XenAPI stream.Flush(); } - while (ReadHttpHeaders(ref stream, proxy, noDelay, timeoutMs)); + while (ReadHttpHeaders(ref stream, proxy, callback, noDelay, timeoutMs)); return stream; } @@ -717,7 +697,7 @@ namespace XenAPI /// /// Adds HTTP CONNECT headers returning the stream ready for use /// - public static Stream HttpConnectStream(Uri uri, IWebProxy proxy, string session, int timeoutMs, Dictionary additionalHeaders = null) + public static Stream HttpConnectStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, string session, int timeoutMs, Dictionary additionalHeaders = null) { var allHeaders = new List { @@ -732,13 +712,13 @@ namespace XenAPI allHeaders.Add($"{kvp.Key}: {kvp.Value}"); } - return DoHttp(uri, proxy, true, timeoutMs, allHeaders.ToArray()); + return DoHttp(uri, proxy, callback, true, timeoutMs, allHeaders.ToArray()); } /// /// Adds HTTP PUT headers returning the stream ready for use /// - public static Stream HttpPutStream(Uri uri, IWebProxy proxy, long contentLength, int timeoutMs, Dictionary additionalHeaders = null) + public static Stream HttpPutStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, long contentLength, int timeoutMs, Dictionary additionalHeaders = null) { var allHeaders = new List { @@ -753,13 +733,13 @@ namespace XenAPI allHeaders.Add($"{kvp.Key}: {kvp.Value}"); } - return DoHttp(uri, proxy, false, timeoutMs, allHeaders.ToArray()); + return DoHttp(uri, proxy, callback, false, timeoutMs, allHeaders.ToArray()); } /// /// Adds HTTP GET headers returning the stream ready for use /// - public static Stream HttpGetStream(Uri uri, IWebProxy proxy, int timeoutMs, Dictionary additionalHeaders = null) + public static Stream HttpGetStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, int timeoutMs, Dictionary additionalHeaders = null) { var allHeaders = new List { @@ -773,7 +753,7 @@ namespace XenAPI allHeaders.Add($"{kvp.Key}: {kvp.Value}"); } - return DoHttp(uri, proxy, false, timeoutMs, allHeaders.ToArray()); + return DoHttp(uri, proxy, callback, false, timeoutMs, allHeaders.ToArray()); } /// @@ -783,13 +763,14 @@ namespace XenAPI /// Delegate called periodically to see if need to cancel /// URI to PUT to /// A proxy to handle the HTTP connection + /// /// Path to file to put /// Timeout for the connection in ms. 0 for no timeout. public static void Put(UpdateProgressDelegate progressDelegate, FuncBool cancellingDelegate, - Uri uri, IWebProxy proxy, string path, int timeoutMs) + Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, string path, int timeoutMs) { using (Stream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read), - requestStream = HttpPutStream(uri, proxy, fileStream.Length, timeoutMs)) + requestStream = HttpPutStream(uri, proxy, callback, fileStream.Length, timeoutMs)) { long len = fileStream.Length; DataCopiedDelegate dataCopiedDelegate = delegate(long bytes) @@ -809,10 +790,11 @@ namespace XenAPI /// Delegate called periodically to see if need to cancel /// URI to GET from /// A proxy to handle the HTTP connection + /// /// Path to file to receive the data /// Timeout for the connection in ms. 0 for no timeout. public static void Get(DataCopiedDelegate dataCopiedDelegate, FuncBool cancellingDelegate, - Uri uri, IWebProxy proxy, string path, int timeoutMs) + Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, string path, int timeoutMs) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException(nameof(path)); @@ -833,7 +815,7 @@ namespace XenAPI try { using (Stream fileStream = new FileStream(tmpFile, FileMode.Create, FileAccess.Write, FileShare.None), - downloadStream = HttpGetStream(uri, proxy, timeoutMs)) + downloadStream = HttpGetStream(uri, proxy, callback, timeoutMs)) { CopyStream(downloadStream, fileStream, dataCopiedDelegate, cancellingDelegate); fileStream.Flush(); diff --git a/ocaml/sdk-gen/csharp/templates/HTTP_actions.mustache b/ocaml/sdk-gen/csharp/templates/HTTP_actions.mustache index 3c702c2af..f1531bb63 100644 --- a/ocaml/sdk-gen/csharp/templates/HTTP_actions.mustache +++ b/ocaml/sdk-gen/csharp/templates/HTTP_actions.mustache @@ -28,44 +28,45 @@ */ using System.Net; +using System.Net.Security; namespace XenAPI { public partial class HTTP_actions { private static void Get(HTTP.DataCopiedDelegate dataCopiedDelegate, HTTP.FuncBool cancellingDelegate, int timeout_ms, - string hostname, string remotePath, IWebProxy proxy, string localPath, params object[] args) + string hostname, string remotePath, IWebProxy proxy, RemoteCertificateValidationCallback callback, string localPath, params object[] args) { - HTTP.Get(dataCopiedDelegate, cancellingDelegate, HTTP.BuildUri(hostname, remotePath, args), proxy, localPath, timeout_ms); + HTTP.Get(dataCopiedDelegate, cancellingDelegate, HTTP.BuildUri(hostname, remotePath, args), proxy, callback, localPath, timeout_ms); } private static void Put(HTTP.UpdateProgressDelegate progressDelegate, HTTP.FuncBool cancellingDelegate, int timeout_ms, - string hostname, string remotePath, IWebProxy proxy, string localPath, params object[] args) + string hostname, string remotePath, IWebProxy proxy, RemoteCertificateValidationCallback callback, string localPath, params object[] args) { - HTTP.Put(progressDelegate, cancellingDelegate, HTTP.BuildUri(hostname, remotePath, args), proxy, localPath, timeout_ms); + HTTP.Put(progressDelegate, cancellingDelegate, HTTP.BuildUri(hostname, remotePath, args), proxy, callback, localPath, timeout_ms); } {{#http_actions}} public static void {{name}}(HTTP.{{#isPut}}UpdateProgressDelegate progressDelegate{{/isPut}}{{#isGet}}DataCopiedDelegate dataCopiedDelegate{{/isGet}}, HTTP.FuncBool cancellingDelegate, int timeout_ms, - string hostname, IWebProxy proxy, string path, string task_id = null, string session_id = null{{#args}}, {{{arg_decl}}}{{/args}}) + string hostname, IWebProxy proxy, RemoteCertificateValidationCallback callback, string path, string task_id = null, string session_id = null{{#args}}, {{{arg_decl}}}{{/args}}) { - {{#isPut}}Put{{/isPut}}{{#isGet}}Get{{/isGet}}({{#isPut}}progressDelegate{{/isPut}}{{#isGet}}dataCopiedDelegate{{/isGet}}, cancellingDelegate, timeout_ms, hostname, "{{uri}}", proxy, path, + {{#isPut}}Put{{/isPut}}{{#isGet}}Get{{/isGet}}({{#isPut}}progressDelegate{{/isPut}}{{#isGet}}dataCopiedDelegate{{/isGet}}, cancellingDelegate, timeout_ms, hostname, "{{uri}}", proxy, callback, path, "task_id", task_id, "session_id", session_id{{#args}}, {{{arg_use}}}{{/args}}); } {{/http_actions}} public static void get_pool_patch_download(HTTP.DataCopiedDelegate dataCopiedDelegate, HTTP.FuncBool cancellingDelegate, int timeout_ms, - string hostname, IWebProxy proxy, string path, string task_id, string session_id, string uuid) + string hostname, IWebProxy proxy, RemoteCertificateValidationCallback callback, string path, string task_id, string session_id, string uuid) { - Get(dataCopiedDelegate, cancellingDelegate, timeout_ms, hostname, "/pool_patch_download", proxy, path, + Get(dataCopiedDelegate, cancellingDelegate, timeout_ms, hostname, "/pool_patch_download", proxy, callback, path, "task_id", task_id, "session_id", session_id, "uuid", uuid); } public static void put_oem_patch_stream(HTTP.UpdateProgressDelegate progressDelegate, HTTP.FuncBool cancellingDelegate, int timeout_ms, - string hostname, IWebProxy proxy, string path, string task_id, string session_id) + string hostname, IWebProxy proxy, RemoteCertificateValidationCallback callback, string path, string task_id, string session_id) { - Put(progressDelegate, cancellingDelegate, timeout_ms, hostname, "/oem_patch_stream", proxy, path, + Put(progressDelegate, cancellingDelegate, timeout_ms, hostname, "/oem_patch_stream", proxy, callback, path, "task_id", task_id, "session_id", session_id); } } diff --git a/ocaml/sdk-gen/powershell/autogen/src/CommonCmdletFunctions.cs b/ocaml/sdk-gen/powershell/autogen/src/CommonCmdletFunctions.cs index 943c91a76..5c10656a0 100644 --- a/ocaml/sdk-gen/powershell/autogen/src/CommonCmdletFunctions.cs +++ b/ocaml/sdk-gen/powershell/autogen/src/CommonCmdletFunctions.cs @@ -30,7 +30,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Management.Automation; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Xml; using XenAPI; namespace Citrix.XenServer @@ -39,6 +43,7 @@ namespace Citrix.XenServer { private const string SessionsVariable = "global:Citrix.XenServer.Sessions"; private const string DefaultSessionVariable = "global:XenServer_Default_Session"; + private const string CertificatesPathVariable = "global:KnownServerCertificatesFilePath"; internal static Dictionary GetAllSessions(PSCmdlet cmdlet) { @@ -137,5 +142,125 @@ namespace Citrix.XenServer } } } + + internal static bool VerifyInAllStores(X509Certificate2 certificate2) + { + try + { + X509Chain chain = new X509Chain(true); + return chain.Build(certificate2) || certificate2.Verify(); + } + catch (CryptographicException) + { + return false; + } + } + + internal static string GetCertificatesPath(PSCmdlet cmdlet) + { + var certPathObject = cmdlet.SessionState.PSVariable.GetValue(CertificatesPathVariable); + + return certPathObject is PSObject psObject + ? psObject.BaseObject as string + : certPathObject?.ToString() ?? string.Empty; + } + + internal static Dictionary LoadCertificates(string certPath) + { + var certificates = new Dictionary(); + + if (File.Exists(certPath)) + { + var doc = new XmlDocument(); + doc.Load(certPath); + + foreach (XmlNode node in doc.GetElementsByTagName("certificate")) + { + var hostAtt = node.Attributes?["hostname"]; + var fngprtAtt = node.Attributes?["fingerprint"]; + + if (hostAtt != null && fngprtAtt != null) + certificates[hostAtt.Value] = fngprtAtt.Value; + } + } + + return certificates; + } + + internal static void SaveCertificates(string certPath, Dictionary certificates) + { + string dirName = Path.GetDirectoryName(certPath); + + if (!Directory.Exists(dirName)) + Directory.CreateDirectory(dirName); + + XmlDocument doc = new XmlDocument(); + XmlDeclaration decl = doc.CreateXmlDeclaration("1.0", "utf-8", null); + doc.AppendChild(decl); + XmlNode node = doc.CreateElement("certificates"); + + foreach (KeyValuePair cert in certificates) + { + XmlNode certNode = doc.CreateElement("certificate"); + XmlAttribute hostname = doc.CreateAttribute("hostname"); + XmlAttribute fingerprint = doc.CreateAttribute("fingerprint"); + hostname.Value = cert.Key; + fingerprint.Value = cert.Value; + certNode.Attributes?.Append(hostname); + certNode.Attributes?.Append(fingerprint); + node.AppendChild(certNode); + } + + doc.AppendChild(node); + doc.Save(certPath); + } + } + + internal abstract class CertificateValidationException : Exception + { + protected const string CERT_TRUSTED = "The certificate on this server is trusted. It is recommended you re-issue this server's certificate."; + protected const string CERT_NOT_TRUSTED = "The certificate on this server is not trusted."; + + protected CertificateValidationException(string fingerprint, bool trusted, string hostname) + { + Fingerprint = fingerprint; + Trusted = trusted; + Hostname = hostname; + } + + protected bool Trusted { get; } + public string Fingerprint { get; } + public string Hostname { get; } + public abstract string Caption { get; } + } + + internal class CertificateChangedException : CertificateValidationException + { + public CertificateChangedException(string fingerprint, bool trusted, string hostname) + : base(fingerprint, trusted, hostname) + { + } + + public override string Caption => "Security Certificate Changed"; + + public override string Message => + $"The certificate thumbprint of server {Hostname} has changed since the last time you connected.\n" + + $"The certificate thumbprint of the server is:\n{Fingerprint}\n" + + (Trusted ? CERT_TRUSTED : CERT_NOT_TRUSTED) + + "\nDo you wish to continue?"; + } + + internal class CertificateNotFoundException : CertificateValidationException + { + public CertificateNotFoundException(string fingerprint, bool trusted, string hostname) + : base(fingerprint, trusted, hostname) + { + } + + public override string Caption => "New Security Certificate"; + + public override string Message => $"The certificate thumbprint of the server you have connected to is :\n{Fingerprint}\n" + + (Trusted ? CERT_TRUSTED : CERT_NOT_TRUSTED) + + "\nDo you wish to continue?"; } } diff --git a/ocaml/sdk-gen/powershell/autogen/src/Connect-XenServer.cs b/ocaml/sdk-gen/powershell/autogen/src/Connect-XenServer.cs index dd61f3585..f97199791 100644 --- a/ocaml/sdk-gen/powershell/autogen/src/Connect-XenServer.cs +++ b/ocaml/sdk-gen/powershell/autogen/src/Connect-XenServer.cs @@ -29,7 +29,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Management.Automation; using System.Net; #if NET8_0_OR_GREATER @@ -38,9 +37,7 @@ using System.Net.Http; using System.Net.Security; using System.Runtime.InteropServices; using System.Security; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Xml; using XenAPI; namespace Citrix.XenServer.Commands @@ -48,11 +45,8 @@ namespace Citrix.XenServer.Commands [Cmdlet("Connect", "XenServer")] public class ConnectXenServerCommand : PSCmdlet { - private const string CertificatesPathVariable = "global:KnownServerCertificatesFilePath"; - - private readonly object _certificateValidationLock = new object(); - - private static readonly string DefaultUserAgent = $"XenServerPSModule/@SDK_VERSION@"; + private static readonly string DefaultUserAgent = "XenServerPSModule/@SDK_VERSION@"; + private static readonly object CertificateValidationLock = new object(); public ConnectXenServerCommand() { @@ -228,10 +222,10 @@ namespace Citrix.XenServer.Commands { if (ShouldContinue(ex.Message, ex.Caption)) { - var certPath = GetCertificatesPath(); - var certificates = LoadCertificates(certPath); + var certPath = CommonCmdletFunctions.GetCertificatesPath(this); + var certificates = CommonCmdletFunctions.LoadCertificates(certPath); certificates[ex.Hostname] = ex.Fingerprint; - SaveCertificates(certPath, certificates); + CommonCmdletFunctions.SaveCertificates(certPath, certificates); i--; continue; } @@ -274,171 +268,53 @@ namespace Citrix.XenServer.Commands WriteObject(newSessions.Values, true); } - private bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + private bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, + SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors == SslPolicyErrors.None) return true; - lock (_certificateValidationLock) - { - bool ignoreChanged = Force || NoWarnCertificates || (bool)GetVariableValue("NoWarnCertificates", false); - bool ignoreNew = Force || NoWarnNewCertificates || (bool)GetVariableValue("NoWarnNewCertificates", false); - #if NET8_0_OR_GREATER - var requestMessage = sender as HttpRequestMessage; - string hostname = requestMessage?.RequestUri?.Host ?? string.Empty; + var requestMessage = sender as HttpRequestMessage; + string hostname = requestMessage?.RequestUri?.Host ?? string.Empty; #else - var webreq = sender as HttpWebRequest; - string hostname = webreq?.Address?.Host ?? string.Empty; + var webreq = sender as HttpWebRequest; + string hostname = webreq?.Address?.Host ?? string.Empty; #endif - string fingerprint = CommonCmdletFunctions.FingerprintPrettyString(certificate.GetCertHashString()); - bool trusted = VerifyInAllStores(new X509Certificate2(certificate)); + string fingerprint = CommonCmdletFunctions.FingerprintPrettyString(certificate.GetCertHashString()); - var certPath = GetCertificatesPath(); - var certificates = LoadCertificates(certPath); + lock (CertificateValidationLock) + { + var certPath = CommonCmdletFunctions.GetCertificatesPath(this); + var certificates = CommonCmdletFunctions.LoadCertificates(certPath); if (certificates.TryGetValue(hostname, out var fingerprintOld)) { if (fingerprintOld == fingerprint) return true; + bool ignoreChanged = Force || NoWarnCertificates || (bool)GetVariableValue("NoWarnCertificates", false); if (!ignoreChanged) - throw new CertificateChangedException(fingerprint, fingerprintOld, trusted, hostname); + { + var trusted = CommonCmdletFunctions.VerifyInAllStores(new X509Certificate2(certificate)); + throw new CertificateChangedException(fingerprint, trusted, hostname); + } } else { + bool ignoreNew = Force || NoWarnNewCertificates || (bool)GetVariableValue("NoWarnNewCertificates", false); if (!ignoreNew) + { + var trusted = CommonCmdletFunctions.VerifyInAllStores(new X509Certificate2(certificate)); throw new CertificateNotFoundException(fingerprint, trusted, hostname); + } } certificates[hostname] = fingerprint; - SaveCertificates(certPath, certificates); + CommonCmdletFunctions.SaveCertificates(certPath, certificates); return true; } } - - private bool VerifyInAllStores(X509Certificate2 certificate2) - { - try - { - X509Chain chain = new X509Chain(true); - return chain.Build(certificate2) || certificate2.Verify(); - } - catch (CryptographicException) - { - return false; - } - } - - private string GetCertificatesPath() - { - var certPathObject = SessionState.PSVariable.GetValue(CertificatesPathVariable); - - return certPathObject is PSObject psObject - ? psObject.BaseObject as string - : certPathObject?.ToString() ?? string.Empty; - } - - private Dictionary LoadCertificates(string certPath) - { - var certificates = new Dictionary(); - - if (File.Exists(certPath)) - { - var doc = new XmlDocument(); - doc.Load(certPath); - - foreach (XmlNode node in doc.GetElementsByTagName("certificate")) - { - var hostAtt = node.Attributes?["hostname"]; - var fngprtAtt = node.Attributes?["fingerprint"]; - - if (hostAtt != null && fngprtAtt != null) - certificates[hostAtt.Value] = fngprtAtt.Value; - } - } - - return certificates; - } - - private void SaveCertificates(string certPath, Dictionary certificates) - { - string dirName = Path.GetDirectoryName(certPath); - - if (!Directory.Exists(dirName)) - Directory.CreateDirectory(dirName); - - XmlDocument doc = new XmlDocument(); - XmlDeclaration decl = doc.CreateXmlDeclaration("1.0", "utf-8", null); - doc.AppendChild(decl); - XmlNode node = doc.CreateElement("certificates"); - - foreach (KeyValuePair cert in certificates) - { - XmlNode certNode = doc.CreateElement("certificate"); - XmlAttribute hostname = doc.CreateAttribute("hostname"); - XmlAttribute fingerprint = doc.CreateAttribute("fingerprint"); - hostname.Value = cert.Key; - fingerprint.Value = cert.Value; - certNode.Attributes?.Append(hostname); - certNode.Attributes?.Append(fingerprint); - node.AppendChild(certNode); - } - - doc.AppendChild(node); - doc.Save(certPath); - } - } - - internal abstract class CertificateValidationException : Exception - { - protected const string CERT_TRUSTED = "The certificate on this server is trusted. It is recommended you re-issue this server's certificate."; - protected const string CERT_NOT_TRUSTED = "The certificate on this server is not trusted."; - - protected readonly bool Trusted; - public readonly string Fingerprint; - public readonly string Hostname; - - protected CertificateValidationException(string fingerprint, bool trusted, string hostname) - { - Fingerprint = fingerprint; - Trusted = trusted; - Hostname = hostname; - } - - public abstract string Caption { get; } - } - - internal class CertificateChangedException : CertificateValidationException - { - private readonly string _oldFingerprint; - - public CertificateChangedException(string fingerprint, string oldFingerprint, bool trusted, string hostname) - : base(fingerprint, trusted, hostname) - { - _oldFingerprint = oldFingerprint; - } - - public override string Caption => "Security Certificate Changed"; - - public override string Message => $"The certificate fingerprint of the server you have connected to is:\n{Fingerprint}\n" + - $"But was expected to be:\n{_oldFingerprint}\n" + - (Trusted ? CERT_TRUSTED : CERT_NOT_TRUSTED) + - "\nDo you wish to continue?"; - } - - internal class CertificateNotFoundException : CertificateValidationException - { - public CertificateNotFoundException(string fingerprint, bool trusted, string hostname) - : base(fingerprint, trusted, hostname) - { - } - - public override string Caption => "New Security Certificate"; - - public override string Message => $"The certificate fingerprint of the server you have connected to is :\n{Fingerprint}\n" + - (Trusted ? CERT_TRUSTED : CERT_NOT_TRUSTED) + - "\nDo you wish to continue?"; } } diff --git a/ocaml/sdk-gen/powershell/autogen/src/Receive-XenPoolPatch.cs b/ocaml/sdk-gen/powershell/autogen/src/Receive-XenPoolPatch.cs index 7442e047e..836819e12 100644 --- a/ocaml/sdk-gen/powershell/autogen/src/Receive-XenPoolPatch.cs +++ b/ocaml/sdk-gen/powershell/autogen/src/Receive-XenPoolPatch.cs @@ -58,8 +58,8 @@ namespace Citrix.XenServer.Commands GetSession(); RunApiCall(() => XenAPI.HTTP_actions.get_pool_patch_download(DataCopiedDelegate, - CancellingDelegate, TimeoutMs, XenHost, Proxy, Path, TaskRef, - session.opaque_ref, Uuid)); + CancellingDelegate, TimeoutMs, XenHost, Proxy, CertificateValidationCallback, + Path, TaskRef, session.opaque_ref, Uuid)); } #endregion diff --git a/ocaml/sdk-gen/powershell/autogen/src/Send-XenOemPatchStream.cs b/ocaml/sdk-gen/powershell/autogen/src/Send-XenOemPatchStream.cs index 3c6768f6d..2da05781c 100644 --- a/ocaml/sdk-gen/powershell/autogen/src/Send-XenOemPatchStream.cs +++ b/ocaml/sdk-gen/powershell/autogen/src/Send-XenOemPatchStream.cs @@ -58,8 +58,8 @@ namespace Citrix.XenServer.Commands return; RunApiCall(() => XenAPI.HTTP_actions.put_oem_patch_stream(ProgressDelegate, - CancellingDelegate, TimeoutMs, XenHost, Proxy, Path, TaskRef, - session.opaque_ref)); + CancellingDelegate, TimeoutMs, XenHost, Proxy, CertificateValidationCallback, + Path, TaskRef, session.opaque_ref)); } #endregion diff --git a/ocaml/sdk-gen/powershell/autogen/src/XenServerCmdlet.cs b/ocaml/sdk-gen/powershell/autogen/src/XenServerCmdlet.cs index fffa65b58..1572b346d 100644 --- a/ocaml/sdk-gen/powershell/autogen/src/XenServerCmdlet.cs +++ b/ocaml/sdk-gen/powershell/autogen/src/XenServerCmdlet.cs @@ -29,7 +29,6 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Management.Automation; @@ -113,23 +112,44 @@ namespace Citrix.XenServer.Commands protected void RunApiCall(XenApiCall call) { - try + for (int i = 0; i < 1; i++) { - call.Invoke(); - } - catch (Exception e) - { - // if you want to trap errors either set command-line switch "-BestEffort" - // or session-state variable "$BestEffort" to "$true" - - bool bestEffort = (bool)GetVariableValue("BestEffort", false) || BestEffort; - if (!bestEffort) - throw; - - // catch exception and write it to the terminal then return - // don't throw it because this will break piping a list into the cmd (won't run rest of list) - - ThrowTerminatingError(new ErrorRecord(e, string.Empty, ErrorCategory.InvalidOperation, null)); + try + { + call.Invoke(); + } + catch (Exception e) + { + if (e is CertificateValidationException ex) + { + if (ShouldContinue(ex.Message, ex.Caption)) + { + var certPath = CommonCmdletFunctions.GetCertificatesPath(this); + var certificates = CommonCmdletFunctions.LoadCertificates(certPath); + certificates[ex.Hostname] = ex.Fingerprint; + CommonCmdletFunctions.SaveCertificates(certPath, certificates); + i--; + continue; + } + + ThrowTerminatingError(new ErrorRecord(ex, "", ErrorCategory.AuthenticationError, ex.Hostname) + { + ErrorDetails = new ErrorDetails($"Certificate fingerprint rejected. ({ex.Fingerprint} - {ex.Hostname}).") + }); + } + + // if you want to trap errors either set command-line switch "-BestEffort" + // or session-state variable "$BestEffort" to "$true" + + bool bestEffort = (bool)GetVariableValue("BestEffort", false) || BestEffort; + if (!bestEffort) + throw; + + // catch exception and write it to the terminal then return + // don't throw it because this will break piping a list into the cmd (won't run rest of list) + + ThrowTerminatingError(new ErrorRecord(e, string.Empty, ErrorCategory.InvalidOperation, null)); + } } } diff --git a/ocaml/sdk-gen/powershell/autogen/src/XenServerHttpCmdlet.cs b/ocaml/sdk-gen/powershell/autogen/src/XenServerHttpCmdlet.cs index 0fdecbab0..fcbf4d18b 100644 --- a/ocaml/sdk-gen/powershell/autogen/src/XenServerHttpCmdlet.cs +++ b/ocaml/sdk-gen/powershell/autogen/src/XenServerHttpCmdlet.cs @@ -29,16 +29,24 @@ using System; -using System.Collections.Generic; using System.Management.Automation; using System.Net; - +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using XenAPI; namespace Citrix.XenServer.Commands { public class XenServerHttpCmdlet : XenServerCmdlet { + private bool _hitonce; + private static readonly object CertificateValidationLock = new object(); + + protected XenServerHttpCmdlet() + { + CertificateValidationCallback = ValidateServerCertificate; + } + #region Cmdlet Parameters [Parameter] @@ -53,6 +61,15 @@ namespace Citrix.XenServer.Commands [Parameter] public IWebProxy Proxy { get; set; } + [Parameter] + public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } + + [Parameter] + public SwitchParameter NoWarnNewCertificates { get; set; } + + [Parameter] + public SwitchParameter NoWarnCertificates { get; set; } + [Parameter(Mandatory = true)] public string Path { get; set; } @@ -60,5 +77,55 @@ namespace Citrix.XenServer.Commands public string TaskRef { get; set; } #endregion + + private bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + try + { + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + string fingerprintToCheck = CommonCmdletFunctions.FingerprintPrettyString(certificate.GetCertHashString()); + + lock (CertificateValidationLock) + { + var certPath = CommonCmdletFunctions.GetCertificatesPath(this); + var certificates = CommonCmdletFunctions.LoadCertificates(certPath); + + if (certificates.TryGetValue(XenHost, out var hostFingerprint)) + { + if (fingerprintToCheck == hostFingerprint) + return true; + + bool ignoreChanged = NoWarnCertificates || (bool)GetVariableValue("NoWarnCertificates", false); + if (!ignoreChanged) + { + if (_hitonce) + throw new Exception($"{XenHost} is not the correct server for this operation. Please specify a different server in the pool."); + + var trusted = CommonCmdletFunctions.VerifyInAllStores(new X509Certificate2(certificate)); + throw new CertificateChangedException(fingerprintToCheck, trusted, XenHost); + } + } + else + { + bool ignoreNew = NoWarnNewCertificates || (bool)GetVariableValue("NoWarnNewCertificates", false); + if (!ignoreNew) + { + var trusted = CommonCmdletFunctions.VerifyInAllStores(new X509Certificate2(certificate)); + throw new CertificateNotFoundException(fingerprintToCheck, trusted, XenHost); + } + } + + certificates[XenHost] = fingerprintToCheck; + CommonCmdletFunctions.SaveCertificates(certPath, certificates); + return true; + } + } + finally + { + _hitonce = true; + } + } } } diff --git a/ocaml/sdk-gen/powershell/templates/HttpAction.mustache b/ocaml/sdk-gen/powershell/templates/HttpAction.mustache index e346a68b8..40b5b8977 100644 --- a/ocaml/sdk-gen/powershell/templates/HttpAction.mustache +++ b/ocaml/sdk-gen/powershell/templates/HttpAction.mustache @@ -71,7 +71,7 @@ namespace Citrix.XenServer.Commands {{/isPut}} RunApiCall(() => HTTP_actions.{{action_name}}({{#isPut}}ProgressDelegate{{/isPut}}{{#isGet}}DataCopiedDelegate{{/isGet}}, - CancellingDelegate, TimeoutMs, XenHost, Proxy, Path, TaskRef, + CancellingDelegate, TimeoutMs, XenHost, Proxy, CertificateValidationCallback, Path, TaskRef, session.opaque_ref{{#args}}, {{arg_name}}{{/args}})); }