@@ -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 "sync"
1214 "time"
@@ -44,6 +46,10 @@ type SpiceClient struct {
4446 backoffPolicy backoff.BackOff
4547 maxRetries uint
4648 userAgent string
49+
50+ tlsClientCertFile string
51+ tlsClientKeyFile string
52+ tlsRootCertFile string
4753}
4854
4955// NewSpiceClient creates a new SpiceClient
@@ -116,6 +122,32 @@ func WithSpiceCloudAddress() SpiceClientModifier {
116122 }
117123}
118124
125+ // WithTLSClientCertificate configures the client to present a client certificate
126+ // during the TLS handshake for mutual TLS (mTLS) authentication.
127+ // Both certFile and keyFile must be PEM-encoded.
128+ func WithTLSClientCertificate (certFile , keyFile string ) SpiceClientModifier {
129+ return func (c * SpiceClient ) error {
130+ if certFile == "" || keyFile == "" {
131+ return fmt .Errorf ("both certFile and keyFile are required for mTLS" )
132+ }
133+ c .tlsClientCertFile = certFile
134+ c .tlsClientKeyFile = keyFile
135+ return nil
136+ }
137+ }
138+
139+ // WithTLSRootCertificate configures the client to use a custom CA certificate
140+ // file for server verification instead of (in addition to) the system certificate store.
141+ func WithTLSRootCertificate (caFile string ) SpiceClientModifier {
142+ return func (c * SpiceClient ) error {
143+ if caFile == "" {
144+ return fmt .Errorf ("caFile is required" )
145+ }
146+ c .tlsRootCertFile = caFile
147+ return nil
148+ }
149+ }
150+
119151// Init initializes the SpiceClient
120152func (c * SpiceClient ) Init (opts ... SpiceClientModifier ) error {
121153 for _ , opt := range opts {
@@ -130,13 +162,42 @@ func (c *SpiceClient) Init(opts ...SpiceClientModifier) error {
130162 return fmt .Errorf ("error getting system cert pool: %w" , err )
131163 }
132164
165+ if c .tlsRootCertFile != "" {
166+ caPem , err := os .ReadFile (c .tlsRootCertFile )
167+ if err != nil {
168+ return fmt .Errorf ("error reading TLS root certificate '%s': %w" , c .tlsRootCertFile , err )
169+ }
170+ if ! systemCertPool .AppendCertsFromPEM (caPem ) {
171+ return fmt .Errorf ("failed to append CA certificate from '%s'" , c .tlsRootCertFile )
172+ }
173+ }
174+
133175 flightClient , err := c .createClient (c .flightAddress , systemCertPool )
134176 if err != nil {
135177 return fmt .Errorf ("error creating Spice Flight client: %w" , err )
136178 }
137179
138180 c .flightClient = flightClient
139181
182+ // Update the HTTP client transport with the same TLS configuration
183+ // (custom CA and/or client certificate) used by the Flight client.
184+ httpTlsConfig := & tls.Config {
185+ RootCAs : systemCertPool ,
186+ MinVersion : tls .VersionTLS12 ,
187+ }
188+ if c .tlsClientCertFile != "" && c .tlsClientKeyFile != "" {
189+ clientCert , err := tls .LoadX509KeyPair (c .tlsClientCertFile , c .tlsClientKeyFile )
190+ if err != nil {
191+ return fmt .Errorf ("error loading client certificate for HTTP mTLS: %w" , err )
192+ }
193+ httpTlsConfig .Certificates = []tls.Certificate {clientCert }
194+ }
195+ c .httpClient .Transport = & http.Transport {
196+ MaxIdleConnsPerHost : 10 ,
197+ DisableCompression : false ,
198+ TLSClientConfig : httpTlsConfig ,
199+ }
200+
140201 // Initialize ADBC client - non-fatal, will be initialized lazily if needed
141202 // This allows health checks to work even if ADBC connection fails initially
142203 _ = c .initADBC ()
@@ -228,7 +289,20 @@ func (c *SpiceClient) createClient(address string, systemCertPool *x509.CertPool
228289 address = strings .TrimPrefix (address , "grpc://" )
229290 grpcDialOpts = append (grpcDialOpts , grpc .WithTransportCredentials (insecure .NewCredentials ()))
230291 } else {
231- grpcDialOpts = append (grpcDialOpts , grpc .WithTransportCredentials (credentials .NewClientTLSFromCert (systemCertPool , "" )))
292+ tlsConfig := & tls.Config {
293+ RootCAs : systemCertPool ,
294+ MinVersion : tls .VersionTLS12 ,
295+ }
296+
297+ if c .tlsClientCertFile != "" && c .tlsClientKeyFile != "" {
298+ clientCert , err := tls .LoadX509KeyPair (c .tlsClientCertFile , c .tlsClientKeyFile )
299+ if err != nil {
300+ return nil , fmt .Errorf ("error loading client certificate for mTLS: %w" , err )
301+ }
302+ tlsConfig .Certificates = []tls.Certificate {clientCert }
303+ }
304+
305+ grpcDialOpts = append (grpcDialOpts , grpc .WithTransportCredentials (credentials .NewTLS (tlsConfig )))
232306 }
233307
234308 client , err := flight .NewClientWithMiddleware (
0 commit comments