Skip to content

Commit b1eae1c

Browse files
feat: add mTLS client certificate support
1 parent 0326918 commit b1eae1c

3 files changed

Lines changed: 41 additions & 12 deletions

File tree

Spice/src/Flight/SpiceFlightClient.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
*/
2222

2323
using System.Net.Http.Headers;
24+
using System.Security.Cryptography.X509Certificates;
2425
using Apache.Arrow.Flight;
2526
using Apache.Arrow.Flight.Client;
2627
using Grpc.Core;
@@ -39,7 +40,7 @@ internal class SpiceFlightClient : IDisposable
3940
private readonly HttpClient? _httpClient;
4041
private readonly AsyncRetryPolicy _retryPolicy;
4142

42-
private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? apiKey, string? userAgent, bool useTls)
43+
private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? apiKey, string? userAgent, bool useTls, string? tlsClientCertFile = null, string? tlsClientKeyFile = null)
4344
{
4445
var options = new GrpcChannelOptions();
4546

@@ -57,12 +58,13 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
5758
var handler = new SocketsHttpHandler
5859
{
5960
EnableMultipleHttp2Connections = true,
60-
// Force periodic connection recycling to trigger DNS re-resolution.
61-
// Without this, HTTP/2 connections are kept alive indefinitely and
62-
// the client can get stuck on stale IPs when backend targets change
63-
// (e.g. AWS ALB target rotation).
6461
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
6562
};
63+
if (tlsClientCertFile != null && tlsClientKeyFile != null)
64+
{
65+
var clientCert = X509Certificate2.CreateFromPemFile(tlsClientCertFile, tlsClientKeyFile);
66+
handler.SslOptions.ClientCertificates = new X509Certificate2Collection { clientCert };
67+
}
6668
options.HttpHandler = handler;
6769
}
6870
#endif
@@ -80,12 +82,13 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
8082
messageHandler = new SocketsHttpHandler
8183
{
8284
EnableMultipleHttp2Connections = true,
83-
// Force periodic connection recycling to trigger DNS re-resolution.
84-
// Without this, HTTP/2 connections are kept alive indefinitely and
85-
// the client can get stuck on stale IPs when backend targets change
86-
// (e.g. AWS ALB target rotation).
8785
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
8886
};
87+
if (tlsClientCertFile != null && tlsClientKeyFile != null)
88+
{
89+
var clientCert = X509Certificate2.CreateFromPemFile(tlsClientCertFile, tlsClientKeyFile);
90+
((SocketsHttpHandler)messageHandler).SslOptions.ClientCertificates = new X509Certificate2Collection { clientCert };
91+
}
8992
}
9093
else
9194
#endif
@@ -112,13 +115,13 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
112115
return responseHeaders.Get("authorization") ?? trailers.Get("authorization");
113116
}
114117

115-
internal SpiceFlightClient(string address, int maxRetries, string? appId, string? apiKey, string? userAgent, bool useTls)
118+
internal SpiceFlightClient(string address, int maxRetries, string? appId, string? apiKey, string? userAgent, bool useTls, string? tlsClientCertFile = null, string? tlsClientKeyFile = null)
116119
{
117120
_retryPolicy = RetryPolicyFactory.CreateRpcRetryPolicy(
118121
maxRetries,
119122
(ex, ts, attempt) => RetryPolicyFactory.LogRetry("Flight", ex, ts, attempt));
120123

121-
var options = GetGrpcChannelOptions(appId, apiKey, userAgent, useTls);
124+
var options = GetGrpcChannelOptions(appId, apiKey, userAgent, useTls, tlsClientCertFile, tlsClientKeyFile);
122125
_httpClient = options.HttpClient;
123126

124127
_channel = GrpcChannel.ForAddress(address, options);

Spice/src/SpiceClient.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,26 @@ public class SpiceClient : IDisposable
6666
/// </summary>
6767
public bool UseTls { get; internal set; }
6868

69+
/// <summary>
70+
/// Gets or sets the path to a PEM-encoded client certificate file for mTLS.
71+
/// Must be used together with <see cref="TlsClientKeyFile"/>.
72+
/// </summary>
73+
public string? TlsClientCertFile { get; internal set; }
74+
75+
/// <summary>
76+
/// Gets or sets the path to a PEM-encoded client private key file for mTLS.
77+
/// Must be used together with <see cref="TlsClientCertFile"/>.
78+
/// </summary>
79+
public string? TlsClientKeyFile { get; internal set; }
80+
6981
private SpiceFlightClient? FlightClient { get; set; }
7082
private SpiceAdbcClient? AdbcClient { get; set; }
7183
private SpiceHttpClient? HttpClient { get; set; }
7284

7385

7486
internal void Init()
7587
{
76-
FlightClient = new SpiceFlightClient(FlightAddress, MaxRetries, AppId, ApiKey, UserAgent, UseTls);
88+
FlightClient = new SpiceFlightClient(FlightAddress, MaxRetries, AppId, ApiKey, UserAgent, UseTls, TlsClientCertFile, TlsClientKeyFile);
7789
AdbcClient = new SpiceAdbcClient(FlightAddress, MaxRetries, AppId, ApiKey, UserAgent, UseTls);
7890
HttpClient = new SpiceHttpClient(HttpAddress, AppId, ApiKey, UserAgent);
7991
}

Spice/src/SpiceClientBuilder.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,20 @@ public SpiceClientBuilder WithTls(bool useTls = true)
148148
return this;
149149
}
150150

151+
/// <summary>
152+
/// Sets the paths to PEM-encoded client certificate and key files for mTLS.
153+
/// </summary>
154+
/// <param name="certFile">Path to the client certificate PEM file.</param>
155+
/// <param name="keyFile">Path to the client private key PEM file.</param>
156+
/// <returns>The current instance of <see cref="SpiceClientBuilder"/> for method chaining.</returns>
157+
public SpiceClientBuilder WithTlsClientCertificate(string certFile, string keyFile)
158+
{
159+
_spiceClient.TlsClientCertFile = certFile;
160+
_spiceClient.TlsClientKeyFile = keyFile;
161+
_spiceClient.UseTls = true;
162+
return this;
163+
}
164+
151165
/// <summary>
152166
/// Initiates <see cref="SpiceClient" /> with provided parameters.
153167
/// </summary>

0 commit comments

Comments
 (0)