From 494bef817259c72b6ec2c514a15dea1071ccd89b Mon Sep 17 00:00:00 2001 From: Simon Shanks Date: Wed, 8 Apr 2026 16:49:14 +0100 Subject: [PATCH 1/2] improve TLS. helper methods in KdbTls e.g. ability to connection without hostname verification connection = new c(host, port, usernamePassword, 65536, KdbTls.IgnoreHostnameMismatch(host)); --- kx/KdbTls.cs | 74 +++++++++++++++++++++++++++ kx/KdbTlsOptions.cs | 65 +++++++++++++++++++++++ kx/c.cs | 122 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 kx/KdbTls.cs create mode 100644 kx/KdbTlsOptions.cs diff --git a/kx/KdbTls.cs b/kx/KdbTls.cs new file mode 100644 index 0000000..530d08b --- /dev/null +++ b/kx/KdbTls.cs @@ -0,0 +1,74 @@ +using System.Net.Security; + +namespace kx +{ + /// + /// Provides helper methods for creating common TLS configurations + /// for KDB+ client connections. + /// + public static class KdbTls + { + /// + /// Creates a TLS configuration that uses the default platform + /// certificate validation behaviour. + /// + /// + /// The target host name used during TLS authentication. + /// + /// + /// A instance configured for standard TLS validation. + /// + public static KdbTlsOptions Default(string targetHost) => + new KdbTlsOptions + { + Enabled = true, + TargetHost = targetHost + }; + + /// + /// Creates a TLS configuration that ignores certificate host name mismatch + /// errors while still rejecting other certificate validation failures. + /// + /// + /// The target host name used during TLS authentication. + /// + /// + /// A instance configured to ignore + /// . + /// + public static KdbTlsOptions IgnoreHostnameMismatch(string targetHost) => + new KdbTlsOptions + { + Enabled = true, + TargetHost = targetHost, + RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => + { + var filteredErrors = errors & ~SslPolicyErrors.RemoteCertificateNameMismatch; + return filteredErrors == SslPolicyErrors.None; + } + }; + + /// + /// Creates a TLS configuration that accepts any server certificate. + /// + /// + /// The target host name used during TLS authentication. + /// + /// + /// A instance configured to accept all server certificates. + /// + /// + /// This disables certificate validation and should only be used in controlled + /// environments where the security implications are understood. + /// + public static KdbTlsOptions Insecure(string targetHost) => +#pragma warning disable CA5359 + new KdbTlsOptions + { + Enabled = true, + TargetHost = targetHost, + RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true + }; +#pragma warning restore CA5359 + } +} \ No newline at end of file diff --git a/kx/KdbTlsOptions.cs b/kx/KdbTlsOptions.cs new file mode 100644 index 0000000..775b1cb --- /dev/null +++ b/kx/KdbTlsOptions.cs @@ -0,0 +1,65 @@ +using System; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace kx +{ + /// + /// Defines TLS/SSL options for a KDB+ client connection. + /// + public sealed class KdbTlsOptions + { + /// + /// Gets a disabled TLS configuration. + /// + public static KdbTlsOptions Disabled { get; } = new KdbTlsOptions + { + Enabled = false + }; + + /// + /// Gets or sets a value indicating whether TLS is enabled for the connection. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets the target host name used during TLS authentication. + /// + /// + /// This value is typically used for server certificate name validation. + /// + public string TargetHost { get; set; } + + /// + /// Gets the client certificates to present to the server during TLS authentication. + /// + public X509CertificateCollection ClientCertificates { get; } = new X509CertificateCollection(); + + /// + /// Gets or sets the allowed SSL/TLS protocol versions. + /// + public SslProtocols? EnabledSslProtocols { get; set; } + + /// + /// Gets or sets the certificate revocation checking mode to use during TLS authentication. + /// + public X509RevocationMode? CertificateRevocationCheckMode { get; set; } + + /// + /// Gets or sets the callback used to validate the remote server certificate. + /// + public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } + + /// + /// Gets or sets the callback used to select a local client certificate. + /// + public LocalCertificateSelectionCallback LocalCertificateSelectionCallback { get; set; } + + /// + /// Gets or sets an optional callback used to configure + /// before authentication. + /// + public Action ConfigureAuthenticationOptions { get; set; } + } +} \ No newline at end of file diff --git a/kx/c.cs b/kx/c.cs index e50ec3e..220db9e 100644 --- a/kx/c.cs +++ b/kx/c.cs @@ -3,6 +3,8 @@ using System.Net; using System.Net.Security; using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; @@ -129,17 +131,14 @@ public class c : IDisposable /// was null. /// must be between and /// Unable to connect to KDB+ process, access denied or process unavailable. - public c(string host, - int port) - : this(host, port, Environment.UserName) - { - } + public c(string host, int port) : this(host, port, Environment.UserName) { } - private c(string host, + private c( + string host, int port, string userPassword, int maxBufferSize, - bool useTLS, + KdbTlsOptions tlsOptions, bool uds) { if (host == null) @@ -173,10 +172,26 @@ private c(string host, IPAddress.IsLoopback((_socket.RemoteEndPoint as IPEndPoint).Address); } _clientStream = new NetworkStream(_socket); - if (useTLS) - { - _clientStream = new SslStream(_clientStream, false); - ((SslStream)_clientStream).AuthenticateAsClient(host); + if (tlsOptions != null && tlsOptions.Enabled) + { + var sslStream = new SslStream( + _clientStream, + leaveInnerStreamOpen: false, + tlsOptions.RemoteCertificateValidationCallback, + tlsOptions.LocalCertificateSelectionCallback); + var targetHost = tlsOptions.TargetHost ?? host; + var clientCertificates = tlsOptions.ClientCertificates; + var enabledSslProtocols = tlsOptions.EnabledSslProtocols ?? SslProtocols.None; + var checkCertificateRevocation = + tlsOptions.CertificateRevocationCheckMode.HasValue && + tlsOptions.CertificateRevocationCheckMode.Value != X509RevocationMode.NoCheck; + + sslStream.AuthenticateAsClient( + targetHost, + clientCertificates, + enabledSslProtocols, + checkCertificateRevocation); + _clientStream = sslStream; } _writeBuffer = new byte[2 + userPassword.Length]; _writePosition = 0; @@ -204,12 +219,50 @@ private c(string host, /// or was null. /// must be between and /// Unable to connect to KDB+ process, access denied or process unavailable. - public c(string host, + public c( + string host, int port, string userPassword, int maxBufferSize = DefaultMaxBufferSize, bool useTLS = false) - : this(host, port, userPassword, maxBufferSize, useTLS, false) + : this( + host, + port, + userPassword, + maxBufferSize, + useTLS ? KdbTls.Default(host) : KdbTlsOptions.Disabled, + false) + { + } + + /// The hostname or IP address of the KDB+ server. + /// The TCP port of the KDB+ server. + /// The username/password string used for authentication. + /// The maximum buffer size used for IPC messages. + /// The TLS options to use for the connection. + /// + /// or was null. + /// + /// + /// must be between and + /// . + /// + /// + /// Unable to connect to the KDB+ process, access denied, or process unavailable. + /// + public c( + string host, + int port, + string userPassword, + int maxBufferSize, + KdbTlsOptions tlsOptions) + : this( + host, + port, + userPassword, + maxBufferSize, + tlsOptions ?? KdbTlsOptions.Disabled, + false) { } @@ -225,11 +278,50 @@ public c(string host, /// or was null. /// Unable to connect to KDB+ process, access denied or process unavailable. /// The current OS does not support Unix Domain Sockets - public c(string file, + public c( + string file, string userPassword, int maxBufferSize = DefaultMaxBufferSize, bool useTLS = false) - : this(file, 0, userPassword, maxBufferSize, useTLS, true) + : this( + file, + 0, + userPassword, + maxBufferSize, + useTLS ? KdbTls.Default(file) : KdbTlsOptions.Disabled, + true) + { + } + + /// + /// Initialises a new instance of using a Unix Domain Socket connection, + /// a username/password for authentication, a maximum buffer size, and explicit TLS options. + /// + /// The Unix Domain Socket file path. + /// The username/password string used for authentication. + /// The maximum buffer size used for IPC messages. + /// The TLS options to use for the connection. + /// + /// or was null. + /// + /// + /// Unable to connect to the KDB+ process, access denied, or process unavailable. + /// + /// + /// The current operating system does not support Unix Domain Sockets. + /// + public c( + string file, + string userPassword, + int maxBufferSize, + KdbTlsOptions tlsOptions) + : this( + file, + 0, + userPassword, + maxBufferSize, + tlsOptions ?? KdbTlsOptions.Disabled, + true) { } From e793a44c7d57ba6bce81f2fce7604a85b11f6526 Mon Sep 17 00:00:00 2001 From: Simon Shanks Date: Thu, 9 Apr 2026 10:14:00 +0100 Subject: [PATCH 2/2] tls improvements and docs --- docs/README.md | 100 ++++++++++++++++++++++++++++++++++++++++++++ kx/KdbTls.cs | 3 +- kx/KdbTlsOptions.cs | 6 --- kx/c.cs | 36 +++------------- 4 files changed, 107 insertions(+), 38 deletions(-) diff --git a/docs/README.md b/docs/README.md index 1fb237a..189dff2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -238,6 +238,106 @@ This example assumes rows with three fields, symbol, price and size. The `c` class throws exceptions of C\# class Exceptions both for typical socket read/write reasons and in higher level cases; that is, errors at the q level rather than the Socket level. +## SSL/TLS Connections + +SSL/TLS connections can be customized by use of the `KdbTlsOptions` class. A helper class `KdbTls` exists for commonly used settings, which can produce a `KdbTlsOptions`. For full flexibility `KdbTlsOptions` can be used directly. +To gain an understanding on the use of SSL/TLS, its useful to consult the following resources: + +* [`TLS/SSL best practices`](https://learn.microsoft.com/en-us/dotnet/core/extensions/sslstream-best-practices) +* [`SslStream class`](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream?view=net-8.0) +* [`SslStream.authenticateasclient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream.beginauthenticateasclient?view=net-8.0#system-net-security-sslstream-beginauthenticateasclient(system-string-system-security-cryptography-x509certificates-x509certificatecollection-system-security-authentication-sslprotocols-system-boolean-system-asynccallback-system-object))) +* [`Using SSL/TLS with KDB+`](https://code.kx.com/q/kb/ssl/) + +### SSL/TLS Examples + +#### Connecting with host/ip verification + +Connects and checks host/ip on the server cert (i.e. the server certicate has been issues to the host/ip you are connecting to) + +``` +var conn = new c(host, port, usernamePassword, 65536, true); +``` + +#### Connecting without hostname verification + +To connect without hostname verification but reject other validation failures (such as untrusted private-CA certs). + +```c# +var conn = new c(host, port, usernamePassword, 65536, KdbTls.IgnoreHostnameMismatch(host)); +``` + +Connection errors for untrusted server certs recieved by the client can be prevented by installing the server cert to the client OS trust store as a trusted root. + +For example, on Ubuntu/Debian + +```bash +sudo apt-get install -y ca-certificates +sudo cp ca-cert.pem /usr/local/share/ca-certificates/foobar-ca.crt +sudo update-ca-certificates +``` + +Please consult your OS for details on adding trusted certs. + +#### Connecting without hostname verification and cert chain errors + +Tolerate hostname mismatch and chain errors example: + +```c# +var conn = new c(host, port, usernamePassword, 65536, new KdbTlsOptions { + Enabled = true, + TargetHost = targetHost, + RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => + { + if (certificate == null) + { + return false; + } + + var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); + var actualThumbprint = cert2.Thumbprint?.Replace(" ", ""); + var expected = expectedThumbprint?.Replace(" ", ""); + + if (!string.Equals(actualThumbprint, expected, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var allowedErrors = SslPolicyErrors.None; + + if (ignoreHostnameMismatch) + { + allowedErrors |= SslPolicyErrors.RemoteCertificateNameMismatch; + } + + allowedErrors |= SslPolicyErrors.RemoteCertificateChainErrors; + + return (errors & ~allowedErrors) == SslPolicyErrors.None; + }) +``` + +#### Client side key and cert + +If you wish to provide a client side private key and cert to the server, the following demonstrates this: + +```c# +using System.IO; +using System.Security.Cryptography.X509Certificates; + +var certPem = File.ReadAllText("client-cert.pem"); +var keyPem = File.ReadAllText("client-private-key.pem"); + +var clientCert = X509Certificate2.CreateFromPem(certPem, keyPem); + +var tls = new KdbTlsOptions +{ + Enabled = true, + TargetHost = host +}; + +tls.ClientCertificates.Add(clientCert); + +var conn = new c(host, port, usernamePassword, 65536, tls); +``` ## Examples diff --git a/kx/KdbTls.cs b/kx/KdbTls.cs index 530d08b..e36acef 100644 --- a/kx/KdbTls.cs +++ b/kx/KdbTls.cs @@ -27,7 +27,8 @@ public static KdbTlsOptions Default(string targetHost) => /// /// Creates a TLS configuration that ignores certificate host name mismatch - /// errors while still rejecting other certificate validation failures. + /// errors while still rejecting other certificate validation failures + /// such as untrusted private-CA cert or self-signed certs. /// /// /// The target host name used during TLS authentication. diff --git a/kx/KdbTlsOptions.cs b/kx/KdbTlsOptions.cs index 775b1cb..40d6cb3 100644 --- a/kx/KdbTlsOptions.cs +++ b/kx/KdbTlsOptions.cs @@ -55,11 +55,5 @@ public sealed class KdbTlsOptions /// Gets or sets the callback used to select a local client certificate. /// public LocalCertificateSelectionCallback LocalCertificateSelectionCallback { get; set; } - - /// - /// Gets or sets an optional callback used to configure - /// before authentication. - /// - public Action ConfigureAuthenticationOptions { get; set; } } } \ No newline at end of file diff --git a/kx/c.cs b/kx/c.cs index 220db9e..f9fa017 100644 --- a/kx/c.cs +++ b/kx/c.cs @@ -225,13 +225,7 @@ public c( string userPassword, int maxBufferSize = DefaultMaxBufferSize, bool useTLS = false) - : this( - host, - port, - userPassword, - maxBufferSize, - useTLS ? KdbTls.Default(host) : KdbTlsOptions.Disabled, - false) + : this(host, port, userPassword, maxBufferSize, useTLS ? KdbTls.Default(host) : KdbTlsOptions.Disabled, false) { } @@ -256,13 +250,7 @@ public c( string userPassword, int maxBufferSize, KdbTlsOptions tlsOptions) - : this( - host, - port, - userPassword, - maxBufferSize, - tlsOptions ?? KdbTlsOptions.Disabled, - false) + : this(host,port,userPassword,maxBufferSize,tlsOptions ?? KdbTlsOptions.Disabled,false) { } @@ -274,22 +262,14 @@ public c( /// uds file e.g. "/tmp/kx.5010" is the default with kdb+ listening on port 5010 /// The username and passsword, as "username:password" for remote authorisation. /// The maximum buffer size, default is 65536. - /// A boolean flag indicating whether or not TLS authentication is enabled, default is false. /// or was null. /// Unable to connect to KDB+ process, access denied or process unavailable. /// The current OS does not support Unix Domain Sockets public c( string file, string userPassword, - int maxBufferSize = DefaultMaxBufferSize, - bool useTLS = false) - : this( - file, - 0, - userPassword, - maxBufferSize, - useTLS ? KdbTls.Default(file) : KdbTlsOptions.Disabled, - true) + int maxBufferSize = DefaultMaxBufferSize) + : this(file, 0, userPassword, maxBufferSize, KdbTlsOptions.Disabled, true) { } @@ -315,13 +295,7 @@ public c( string userPassword, int maxBufferSize, KdbTlsOptions tlsOptions) - : this( - file, - 0, - userPassword, - maxBufferSize, - tlsOptions ?? KdbTlsOptions.Disabled, - true) + : this(file, 0, userPassword, maxBufferSize, tlsOptions ?? KdbTlsOptions.Disabled, true) { }