Skip to content

Commit 8a50bc0

Browse files
committed
Merge remote-tracking branch 'origin/trunk' into feat/dataset-refresh-options
# Conflicts: # Spice/src/Http/SpiceHttpClient.cs
2 parents 65a50ae + 3c43a00 commit 8a50bc0

5 files changed

Lines changed: 554 additions & 4 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2026 The Spice.ai OSS Authors
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
// mTLS client certificates are only supported on NET8_0_OR_GREATER (see the guards
24+
// around both call sites in SpiceFlightClient/SpiceHttpClient); netstandard2.0 has
25+
// neither X509Certificate2.CreateFromPemFile nor OperatingSystem.IsWindows.
26+
#if NET8_0_OR_GREATER
27+
using System.Net.Security;
28+
using System.Security.Cryptography.X509Certificates;
29+
30+
namespace Spice.Common;
31+
32+
/// <summary>
33+
/// Loads a PEM-encoded client certificate for use as a TLS client (mTLS) credential.
34+
/// </summary>
35+
internal static class ClientCertificateLoader
36+
{
37+
/// <summary>
38+
/// Loads the certificate and key at the given paths, in a form usable as an
39+
/// <see cref="SslStream"/>/Schannel client certificate on every platform this
40+
/// SDK targets.
41+
/// </summary>
42+
/// <remarks>
43+
/// <see cref="X509Certificate2.CreateFromPemFile(string, string?)"/> attaches the private
44+
/// key as an ephemeral, in-memory key. That's fine for the OpenSSL-backed TLS stack on
45+
/// macOS/Linux, but Windows' native Schannel provider refuses to present an ephemeral-keyed
46+
/// certificate for client authentication — <c>AuthenticateAsClientAsync</c> fails with
47+
/// "Authentication failed because the platform does not support ephemeral keys." This is a
48+
/// by-design Windows/Schannel limitation, not a .NET bug (see
49+
/// https://github.com/dotnet/runtime/issues/23749, closed as external/by-design) — there is
50+
/// no flag or alternate loading API that avoids it. The fix is the same one ASP.NET Core's
51+
/// Kestrel applies when loading a PEM certificate+key on Windows: round-trip the certificate
52+
/// through a PKCS#12 export/import so the private key is backed by a real (non-ephemeral)
53+
/// key container. On non-Windows platforms this round trip is unnecessary, so it's skipped
54+
/// to keep behavior byte-identical to a direct load.
55+
/// </remarks>
56+
public static X509Certificate2 LoadForClientAuth(string certPemFilePath, string keyPemFilePath)
57+
{
58+
var certificate = X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath);
59+
if (!OperatingSystem.IsWindows())
60+
{
61+
return certificate;
62+
}
63+
64+
using (certificate)
65+
{
66+
var pkcs12Bytes = certificate.Export(X509ContentType.Pkcs12, password: (string?)null);
67+
#if NET9_0_OR_GREATER
68+
return X509CertificateLoader.LoadPkcs12(pkcs12Bytes, password: (string?)null, X509KeyStorageFlags.DefaultKeySet);
69+
#else
70+
#pragma warning disable SYSLIB0057 // X509CertificateLoader isn't available pre-net9.0; the pattern below matches this SDK's existing root-certificate loading.
71+
return new X509Certificate2(pkcs12Bytes, (string?)null, X509KeyStorageFlags.DefaultKeySet);
72+
#pragma warning restore SYSLIB0057
73+
#endif
74+
}
75+
}
76+
}
77+
#endif

Spice/src/Flight/SpiceFlightClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
6262
};
6363
if (tlsClientCertFile != null && tlsClientKeyFile != null)
6464
{
65-
var clientCert = X509Certificate2.CreateFromPemFile(tlsClientCertFile, tlsClientKeyFile);
65+
var clientCert = ClientCertificateLoader.LoadForClientAuth(tlsClientCertFile, tlsClientKeyFile);
6666
handler.SslOptions.ClientCertificates = new X509Certificate2Collection { clientCert };
6767
}
6868
if (tlsRootCertFile != null)
@@ -100,7 +100,7 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
100100
};
101101
if (tlsClientCertFile != null && tlsClientKeyFile != null)
102102
{
103-
var clientCert = X509Certificate2.CreateFromPemFile(tlsClientCertFile, tlsClientKeyFile);
103+
var clientCert = ClientCertificateLoader.LoadForClientAuth(tlsClientCertFile, tlsClientKeyFile);
104104
handler.SslOptions.ClientCertificates = new X509Certificate2Collection { clientCert };
105105
}
106106
if (tlsRootCertFile != null)

Spice/src/Http/SpiceHttpClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2424
using System.Text;
2525
using System.Text.Json;
2626
using Spice.Auth;
27+
using Spice.Common;
2728
using Spice.Datasets;
2829
using Spice.Search;
2930

@@ -58,8 +59,7 @@ internal SpiceHttpClient(string httpAddress, string? appId, string? apiKey, stri
5859
};
5960
if (tlsClientCertFile != null && tlsClientKeyFile != null)
6061
{
61-
var clientCert = System.Security.Cryptography.X509Certificates.X509Certificate2.CreateFromPemFile(
62-
tlsClientCertFile, tlsClientKeyFile);
62+
var clientCert = ClientCertificateLoader.LoadForClientAuth(tlsClientCertFile, tlsClientKeyFile);
6363
handler.SslOptions.ClientCertificates =
6464
new System.Security.Cryptography.X509Certificates.X509Certificate2Collection { clientCert };
6565
}

0 commit comments

Comments
 (0)