@@ -2,11 +2,13 @@ package gospice
22
33import (
44 "context"
5+ "crypto/tls"
56 "crypto/x509"
67 "fmt"
78 "io"
89 "math"
910 "net/http"
11+ "os"
1012 "strings"
1113 "time"
1214
@@ -42,6 +44,10 @@ type SpiceClient struct {
4244 backoffPolicy backoff.BackOff
4345 maxRetries uint
4446 userAgent string
47+
48+ tlsClientCertFile string
49+ tlsClientKeyFile string
50+ tlsRootCertFile string
4551}
4652
4753// NewSpiceClient creates a new SpiceClient
@@ -114,6 +120,32 @@ func WithSpiceCloudAddress() SpiceClientModifier {
114120 }
115121}
116122
123+ // WithTLSClientCertificate configures the client to present a client certificate
124+ // during the TLS handshake for mutual TLS (mTLS) authentication.
125+ // Both certFile and keyFile must be PEM-encoded.
126+ func WithTLSClientCertificate (certFile , keyFile string ) SpiceClientModifier {
127+ return func (c * SpiceClient ) error {
128+ if certFile == "" || keyFile == "" {
129+ return fmt .Errorf ("both certFile and keyFile are required for mTLS" )
130+ }
131+ c .tlsClientCertFile = certFile
132+ c .tlsClientKeyFile = keyFile
133+ return nil
134+ }
135+ }
136+
137+ // WithTLSRootCertificate configures the client to use a custom CA certificate
138+ // file for server verification instead of (in addition to) the system certificate store.
139+ func WithTLSRootCertificate (caFile string ) SpiceClientModifier {
140+ return func (c * SpiceClient ) error {
141+ if caFile == "" {
142+ return fmt .Errorf ("caFile is required" )
143+ }
144+ c .tlsRootCertFile = caFile
145+ return nil
146+ }
147+ }
148+
117149// Init initializes the SpiceClient
118150func (c * SpiceClient ) Init (opts ... SpiceClientModifier ) error {
119151 for _ , opt := range opts {
@@ -128,13 +160,42 @@ func (c *SpiceClient) Init(opts ...SpiceClientModifier) error {
128160 return fmt .Errorf ("error getting system cert pool: %w" , err )
129161 }
130162
163+ if c .tlsRootCertFile != "" {
164+ caPem , err := os .ReadFile (c .tlsRootCertFile )
165+ if err != nil {
166+ return fmt .Errorf ("error reading TLS root certificate '%s': %w" , c .tlsRootCertFile , err )
167+ }
168+ if ! systemCertPool .AppendCertsFromPEM (caPem ) {
169+ return fmt .Errorf ("failed to append CA certificate from '%s'" , c .tlsRootCertFile )
170+ }
171+ }
172+
131173 flightClient , err := c .createClient (c .flightAddress , systemCertPool )
132174 if err != nil {
133175 return fmt .Errorf ("error creating Spice Flight client: %w" , err )
134176 }
135177
136178 c .flightClient = flightClient
137179
180+ // Update the HTTP client transport with the same TLS configuration
181+ // (custom CA and/or client certificate) used by the Flight client.
182+ httpTlsConfig := & tls.Config {
183+ RootCAs : systemCertPool ,
184+ MinVersion : tls .VersionTLS12 ,
185+ }
186+ if c .tlsClientCertFile != "" && c .tlsClientKeyFile != "" {
187+ clientCert , err := tls .LoadX509KeyPair (c .tlsClientCertFile , c .tlsClientKeyFile )
188+ if err != nil {
189+ return fmt .Errorf ("error loading client certificate for HTTP mTLS: %w" , err )
190+ }
191+ httpTlsConfig .Certificates = []tls.Certificate {clientCert }
192+ }
193+ c .httpClient .Transport = & http.Transport {
194+ MaxIdleConnsPerHost : 10 ,
195+ DisableCompression : false ,
196+ TLSClientConfig : httpTlsConfig ,
197+ }
198+
138199 // Initialize ADBC client - non-fatal, will be initialized lazily if needed
139200 // This allows health checks to work even if ADBC connection fails initially
140201 _ = c .initADBC ()
@@ -226,7 +287,20 @@ func (c *SpiceClient) createClient(address string, systemCertPool *x509.CertPool
226287 address = strings .TrimPrefix (address , "grpc://" )
227288 grpcDialOpts = append (grpcDialOpts , grpc .WithTransportCredentials (insecure .NewCredentials ()))
228289 } else {
229- grpcDialOpts = append (grpcDialOpts , grpc .WithTransportCredentials (credentials .NewClientTLSFromCert (systemCertPool , "" )))
290+ tlsConfig := & tls.Config {
291+ RootCAs : systemCertPool ,
292+ MinVersion : tls .VersionTLS12 ,
293+ }
294+
295+ if c .tlsClientCertFile != "" && c .tlsClientKeyFile != "" {
296+ clientCert , err := tls .LoadX509KeyPair (c .tlsClientCertFile , c .tlsClientKeyFile )
297+ if err != nil {
298+ return nil , fmt .Errorf ("error loading client certificate for mTLS: %w" , err )
299+ }
300+ tlsConfig .Certificates = []tls.Certificate {clientCert }
301+ }
302+
303+ grpcDialOpts = append (grpcDialOpts , grpc .WithTransportCredentials (credentials .NewTLS (tlsConfig )))
230304 }
231305
232306 client , err := flight .NewClientWithMiddleware (
0 commit comments