RunApiCall(() => XenAPI.HTTP_actions.get_pool_patch_download(DataCopiedDelegate,
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
Xen Security Advisory CVE-2026-42491 / XSA-498
version 2
XAPI: Missing TLS verification in some SDKs
UPDATES IN VERSION 2
====================
Public release.
ISSUE DESCRIPTION
=================
XAPI provides SDKs; bindings for the API in various programming
languages. These are artefacts from a build of XAPI, intended for
integration into 3rd party products.
In all SDKs, there is a main RPC connection to the XAPI host. TLS for
this connection is verified properly, or delegated to the 3rd party
logic, in all language bindings.
There are also certain HTTP handlers which open a separate connection to
the XAPI host. For two of the language bindings only (C# and
Powershell), TLS verification is improperly implemented on these
connections.
IMPACT
======
A malicious actor able to perform a Man-in-the-Middle attack on the
network communication between a program using one of the affected SDKs
and a host running XAPI may be able to intercept the communication and:
* Steal a session token, allowing for hijack of the administrative
session, or
* Read or tamper with exported/imported disk images, host backups, RRD
(performance) data and patches/updates in transit.
VULNERABLE SYSTEMS
==================
This issues has been present in the XAPI C# and Powershell SDKs since
TLS support was added.
MITIGATION
==========
There are no known mitigations.
CREDITS
=======
This issue was discovered by the Veeam Team.
RESOLUTION
==========
Applying the attached patch to XAPI master will generate fixed SDKs.
3rd party programs using vulnerable SDKs must be rebuilt against the
fixed SDKs.
xsa498.patch XAPI master
$ sha256sum xsa498*
f2db9e16561edc59b920e0ed95cce3a19147abc6eb2b8500bd73b87dc285d4ba xsa498.patch
$
DEPLOYMENT DURING EMBARGO
=========================
Deployment of the patches and/or mitigations described above (or
others which are substantially similar) is permitted during the
embargo, even on public-facing systems with untrusted guest users and
administrators.
But: Distribution of updated software is prohibited (except to other
members of the predisclosure list).
Predisclosure list members who wish to deploy significantly different
patches and/or mitigations, please contact the Xen Project Security
Team.
(Note: this during-embargo deployment notice is retained in
post-embargo publicly released Xen Project advisories, even though it
is then no longer applicable. This is to enable the community to have
oversight of the Xen Project Security Team's decisionmaking.)
For more information about permissible uses of embargoed information,
consult the Xen Project community's agreed Security Policy:
http://www.xenproject.org/security-policy.html
-----BEGIN PGP SIGNATURE-----
iQFABAEBCAAqFiEEI+MiLBRfRHX6gGCng/4UyVfoK9kFAmpWJfMMHHBncEB4ZW4u
b3JnAAoJEIP+FMlX6CvZgYEH/jqpKmvSl2oVn5v8Wdk3KZfOxW67ejyA5NpUyW1i
MOwFQyA7gCrxwDaHN62u0aabs4kJNErOzeuVPCtElIyQN+bpB/5lR6nxnxQaQM8K
Ddhtg6rFQfP121+XGYjtbUOr5fvDkLu290XExow5XWKNmk3P1B3HKImPtkmzGpN6
z9FceAqzGX9N7eCPw3waMGsy50/n8L9cLvkyjPHpfb5beTq0AVQFJVD38P8humk0
xY9r+Iq0aGCxyyEZkHJsDaQfuKo3OYGPVoEEB5YiXPS+1P72lRrwkSFL+qJqAPLn
pDAqAGx1wRYvM6zITDnqbwnBYQ7je3UNZlMVEJj2dHWDOLs=
=3Knk
-----END PGP SIGNATURE-----
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}}));
}
© 2016 - 2026 Red Hat, Inc.