@@ -4,7 +4,11 @@ package client
44import (
55 "context"
66 "crypto/tls"
7+ "crypto/x509"
8+ "encoding/pem"
79 "fmt"
10+ "log/slog"
11+ "os"
812 "sync"
913 "testing"
1014 "time"
@@ -23,6 +27,7 @@ import (
2327const (
2428 minConnectTimeout = 5 * time .Second
2529 backoffJitter = 0.2
30+ maxCACertFileSize = 1 << 20 // 1MB
2631)
2732
2833// Client provides gRPC connection to StackRox Central API.
@@ -229,11 +234,112 @@ func (c *Client) tlsConfig() (*tls.Config, error) {
229234 return nil , errors .Wrap (err , "failed to get central URL hostname" )
230235 }
231236
232- return & tls.Config {
237+ tlsCfg := & tls.Config {
233238 InsecureSkipVerify : c .config .InsecureSkipTLSVerify , //nolint:gosec
234239 MinVersion : tls .VersionTLS12 ,
235240 ServerName : hostname ,
236- }, nil
241+ }
242+
243+ if c .config .CACertPath != "" {
244+ certPool , err := loadCACertPool (c .config .CACertPath )
245+ if err != nil {
246+ return nil , err
247+ }
248+
249+ tlsCfg .RootCAs = certPool
250+ }
251+
252+ return tlsCfg , nil
253+ }
254+
255+ func loadCACertPool (caCertPath string ) (* x509.CertPool , error ) {
256+ // File size guard
257+ fileInfo , err := os .Stat (caCertPath )
258+ if err != nil {
259+ return nil , errors .Wrapf (err , "failed to access CA certificate at %s" , caCertPath )
260+ }
261+
262+ if ! fileInfo .Mode ().IsRegular () {
263+ return nil , errors .Errorf ("CA certificate path %s is not a regular file" , caCertPath )
264+ }
265+
266+ if fileInfo .Size () == 0 {
267+ return nil , errors .Errorf ("CA certificate file %s is empty" , caCertPath )
268+ }
269+
270+ if fileInfo .Size () > maxCACertFileSize {
271+ return nil , errors .Errorf (
272+ "CA certificate file %s is too large (%d bytes, max %d)" ,
273+ caCertPath , fileInfo .Size (),
274+ maxCACertFileSize ,
275+ )
276+ }
277+
278+ //nolint: gosec
279+ caCert , err := os .ReadFile (caCertPath )
280+ if err != nil {
281+ return nil , errors .Wrapf (err , "failed to read CA certificate from %s" , caCertPath )
282+ }
283+
284+ // Get system cert pool, warn on fallback
285+ certPool , err := x509 .SystemCertPool ()
286+ if err != nil {
287+ slog .Warn ("Failed to load system CA pool, using custom CA only" , "error" , err )
288+
289+ certPool = x509 .NewCertPool ()
290+ }
291+
292+ if ! certPool .AppendCertsFromPEM (caCert ) {
293+ return nil , errors .Errorf ("failed to parse CA certificate from %s: no valid PEM data found" , caCertPath )
294+ }
295+
296+ showCertInfo (caCert )
297+
298+ return certPool , nil
299+ }
300+
301+ // showCertInfo parses and logs certificate metadata.
302+ func showCertInfo (caCert []byte ) {
303+ block , _ := pem .Decode (caCert )
304+ if block == nil {
305+ slog .Warn ("Unable to decode CA certificate" )
306+
307+ return
308+ }
309+
310+ cert , err := x509 .ParseCertificate (block .Bytes )
311+ if err != nil {
312+ slog .Warn ("Failed to parse CA certificate" , "error" , err )
313+
314+ return
315+ }
316+
317+ slog .Info ("Loaded CA certificate" ,
318+ "subject" , cert .Subject .CommonName ,
319+ "issuer" , cert .Issuer .CommonName ,
320+ "notAfter" , cert .NotAfter ,
321+ "isCA" , cert .IsCA ,
322+ )
323+
324+ if ! cert .IsCA {
325+ slog .Warn ("Provided certificate does not have the CA basic constraint set — TLS verification may fail" ,
326+ "subject" , cert .Subject .CommonName ,
327+ )
328+ }
329+
330+ if time .Now ().After (cert .NotAfter ) {
331+ slog .Warn ("CA certificate is expired — TLS verification will fail" ,
332+ "subject" , cert .Subject .CommonName ,
333+ "expiredAt" , cert .NotAfter ,
334+ )
335+ }
336+
337+ if time .Now ().Before (cert .NotBefore ) {
338+ slog .Warn ("CA certificate is not yet valid" ,
339+ "subject" , cert .Subject .CommonName ,
340+ "validFrom" , cert .NotBefore ,
341+ )
342+ }
237343}
238344
239345func (c * Client ) connectHTTP1 (
0 commit comments