From 73683466b27d7532455f302523466109a03e8432 Mon Sep 17 00:00:00 2001
From: Konstantina Chremmou <konstantina.chremmou@citrix.com>
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 <konstantina.chremmou@citrix.com>
Reviewed-by: Mohammed Zahid <mohammed.zahid@citrix.com>

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
         /// </summary>
         /// <returns>True if a redirect has occurred - headers will need to be resent.</returns>
-        private static bool ReadHttpHeaders(ref Stream stream, IWebProxy proxy, bool nodelay, int timeout_ms, List<string> headers = null)
+        private static bool ReadHttpHeaders(ref Stream stream, IWebProxy proxy, RemoteCertificateValidationCallback callback, bool nodelay, int timeoutMs, List<string> 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;
-        }
-
         /// <summary>
         /// Returns a secure MD5 hash of the given input string.
         /// </summary>
@@ -475,45 +461,24 @@ namespace XenAPI
         /// </summary>
         /// <param name="uri"></param>
         /// <param name="proxy"></param>
+        /// <param name="callback"></param>
         /// <param name="nodelay"></param>
         /// <param name="timeoutMs">Timeout, in ms. 0 for no timeout.</param>
-        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<string> initialResponse = new List<string>();
-                    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<string> 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<string>();
+            ReadHttpHeaders(ref stream, proxy, callback, nodelay, timeoutMs, initialResponse);
+
             // perform authentication only if proxy requires it
             List<string> 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<string> authenticatedResponse = new List<string>();
-            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
         /// <summary>
         /// Adds HTTP CONNECT headers returning the stream ready for use
         /// </summary>
-        public static Stream HttpConnectStream(Uri uri, IWebProxy proxy, string session, int timeoutMs, Dictionary<string, string> additionalHeaders = null)
+        public static Stream HttpConnectStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, string session, int timeoutMs, Dictionary<string, string> additionalHeaders = null)
         {
             var allHeaders = new List<string>
             {
@@ -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());
         }
 
         /// <summary>
         /// Adds HTTP PUT headers returning the stream ready for use
         /// </summary>
-        public static Stream HttpPutStream(Uri uri, IWebProxy proxy, long contentLength, int timeoutMs, Dictionary<string, string> additionalHeaders = null)
+        public static Stream HttpPutStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, long contentLength, int timeoutMs, Dictionary<string, string> additionalHeaders = null)
         {
             var allHeaders = new List<string>
             {
@@ -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());
         }
 
         /// <summary>
         /// Adds HTTP GET headers returning the stream ready for use
         /// </summary>
-        public static Stream HttpGetStream(Uri uri, IWebProxy proxy, int timeoutMs, Dictionary<string, string> additionalHeaders = null)
+        public static Stream HttpGetStream(Uri uri, IWebProxy proxy, RemoteCertificateValidationCallback callback, int timeoutMs, Dictionary<string, string> additionalHeaders = null)
         {
             var allHeaders = new List<string>
             {
@@ -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());
         }
 
         /// <summary>
@@ -783,13 +763,14 @@ namespace XenAPI
         /// <param name="cancellingDelegate">Delegate called periodically to see if need to cancel</param>
         /// <param name="uri">URI to PUT to</param>
         /// <param name="proxy">A proxy to handle the HTTP connection</param>
+        /// <param name="callback"></param>
         /// <param name="path">Path to file to put</param>
         /// <param name="timeoutMs">Timeout for the connection in ms. 0 for no timeout.</param>
         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
         /// <param name="cancellingDelegate">Delegate called periodically to see if need to cancel</param>
         /// <param name="uri">URI to GET from</param>
         /// <param name="proxy">A proxy to handle the HTTP connection</param>
+        /// <param name="callback"></param>
         /// <param name="path">Path to file to receive the data</param>
         /// <param name="timeoutMs">Timeout for the connection in ms. 0 for no timeout.</param>
         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<string, Session> 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<string, string> LoadCertificates(string certPath)
+        {
+            var certificates = new Dictionary<string, string>();
+
+            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<string, string> 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<string, string> 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<string, string> LoadCertificates(string certPath)
-        {
-            var certificates = new Dictionary<string, string>();
-
-            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<string, string> 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<string, string> 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}}));
         }
 
