From 661867e55b7985084b81701ad0f2532ad3067333 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sat, 15 Nov 2025 11:29:47 -0800 Subject: [PATCH] feat(certificates): add --ca-cert flag Adding a flag to allow passing of custom ca-certificates. fixes #18. --- cmd/aepcli/core.go | 5 + cmd/aepcli/main.go | 10 +- internal/config/config.go | 1 + internal/config/config_test.go | 2 + internal/service/ca.go | 61 ++++++ internal/service/ca_test.go | 317 +++++++++++++++++++++++++++++++ internal/service/service.go | 50 +++-- internal/service/service_test.go | 10 +- 8 files changed, 437 insertions(+), 19 deletions(-) create mode 100644 internal/service/ca.go create mode 100644 internal/service/ca_test.go diff --git a/cmd/aepcli/core.go b/cmd/aepcli/core.go index 4e4d892..c18d0b9 100644 --- a/cmd/aepcli/core.go +++ b/cmd/aepcli/core.go @@ -94,6 +94,7 @@ func configCmd(configFile string) *cobra.Command { var serverURL string var headers []string var pathPrefix string + var caCertPath string configCmd := &cobra.Command{ Use: "config", @@ -111,6 +112,7 @@ func configCmd(configFile string) *cobra.Command { ServerURL: serverURL, Headers: headers, PathPrefix: pathPrefix, + CACertPath: caCertPath, } if err := config.WriteAPIWithName(configFile, api, overwrite); err != nil { fmt.Printf("Error writing API config: %v\n", err) @@ -124,6 +126,7 @@ func configCmd(configFile string) *cobra.Command { addCmd.Flags().StringArrayVar(&headers, "header", []string{}, "Headers in format key=value") addCmd.Flags().StringVar(&serverURL, "server-url", "", "Server URL") addCmd.Flags().StringVar(&pathPrefix, "path-prefix", "", "Path prefix") + addCmd.Flags().StringVar(&caCertPath, "ca-cert", "", "Path to custom CA certificate file (PEM format)") addCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing configuration") readCmd := &cobra.Command{ @@ -148,6 +151,7 @@ func configCmd(configFile string) *cobra.Command { fmt.Printf("Server URL: %s\n", api.ServerURL) fmt.Printf("Headers: %v\n", api.Headers) fmt.Printf("Path Prefix: %s\n", api.PathPrefix) + fmt.Printf("CA Certificate Path: %s\n", api.CACertPath) }, } @@ -173,6 +177,7 @@ func configCmd(configFile string) *cobra.Command { fmt.Printf("Server URL: %s\n", api.ServerURL) fmt.Printf("Headers: %v\n", api.Headers) fmt.Printf("Path Prefix: %s\n", api.PathPrefix) + fmt.Printf("CA Certificate Path: %s\n", api.CACertPath) fmt.Println() } }, diff --git a/cmd/aepcli/main.go b/cmd/aepcli/main.go index e506361..e4d1067 100644 --- a/cmd/aepcli/main.go +++ b/cmd/aepcli/main.go @@ -34,6 +34,7 @@ func aepcli(args []string) (int, error) { var dryRun bool var logHTTP bool var insecure bool + var caCertPath string var logLevel string var fileAliasOrCore string var additionalArgs []string @@ -65,6 +66,7 @@ func aepcli(args []string) (int, error) { rootCmd.PersistentFlags().BoolVar(&logHTTP, "log-http", false, "Set to true to log HTTP requests. This can be helpful when attempting to write your own code or debug.") rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Set to true to not make any changes. This can be helpful when paired with log-http to just view http requests instead of perform them.") rootCmd.PersistentFlags().BoolVar(&insecure, "insecure", false, "Set to true to skip TLS certificate verification. Use with caution.") + rootCmd.PersistentFlags().StringVar(&caCertPath, "ca-cert", "", "Path to custom CA certificate file (PEM format) to add to the trusted certificate pool") rootCmd.PersistentFlags().StringVar(&pathPrefix, "path-prefix", "", "Specify a path prefix that is prepended to all paths in the openapi schema. This will strip them when evaluating the resource hierarchy paths.") rootCmd.PersistentFlags().StringVar(&serverURL, "server-url", "", "Specify a URL to use for the server. If not specified, the first server URL in the OpenAPI definition will be used.") rootCmd.PersistentFlags().StringVar(&configFileVar, "config", "", "Path to config file") @@ -104,6 +106,9 @@ func aepcli(args []string) (int, error) { if pathPrefix == "" { pathPrefix = api.PathPrefix } + if caCertPath == "" { + caCertPath = api.CACertPath + } headers = append(headers, api.Headers...) serverURL = api.ServerURL } @@ -121,7 +126,10 @@ func aepcli(args []string) (int, error) { return CODE_ERR, fmt.Errorf("unable to parse headers: %w", err) } - s = service.NewServiceCommand(api, headersMap, dryRun, logHTTP, insecure) + s, err = service.NewServiceCommand(api, headersMap, dryRun, logHTTP, insecure, caCertPath) + if err != nil { + return CODE_ERR, fmt.Errorf("unable to create service command: %w", err) + } result, err := s.Execute(additionalArgs) returnCode := CODE_OK diff --git a/internal/config/config.go b/internal/config/config.go index c8ad423..357380b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,7 @@ type API struct { ServerURL string Headers []string PathPrefix string + CACertPath string } func ReadConfigFromFile(file string) (*Config, error) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1c42369..d4551fe 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -25,6 +25,7 @@ func TestConfigEndToEnd(t *testing.T) { ServerURL: "https://api.example.com", Headers: []string{"Authorization=Bearer token"}, PathPrefix: "/v1", + CACertPath: "/path/to/ca.pem", } // Write API config to file @@ -54,6 +55,7 @@ func TestWriteAPIWithEmptyName(t *testing.T) { ServerURL: "https://api.example.com", Headers: []string{"Authorization=Bearer token"}, PathPrefix: "/v1", + CACertPath: "/path/to/ca.pem", } err := WriteAPIWithName(testFile, testAPI, false) diff --git a/internal/service/ca.go b/internal/service/ca.go new file mode 100644 index 0000000..8372980 --- /dev/null +++ b/internal/service/ca.go @@ -0,0 +1,61 @@ +package service + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "log/slog" + "os" +) + +// loadCACertificate loads a CA certificate from the specified file path and returns a *x509.CertPool +// that includes the system CA certificates plus the custom CA certificate. +func loadCACertificate(caCertPath string) (*x509.CertPool, error) { + if caCertPath == "" { + // Return system CA pool when no custom CA is specified + return x509.SystemCertPool() + } + + slog.Debug("Loading custom CA certificate", "path", caCertPath) + + // Read the CA certificate file + caCertData, err := os.ReadFile(caCertPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("Failed to read CA certificate from %s: file does not exist\n\nTo fix this issue:\n 1. Verify the file path is correct\n 2. Ensure the file exists and is readable", caCertPath) + } + if os.IsPermission(err) { + return nil, fmt.Errorf("Failed to read CA certificate from %s: permission denied\n\nTo fix this issue:\n 1. Verify you have read permissions for the file\n 2. Check file permissions with: ls -l %s", caCertPath, caCertPath) + } + return nil, fmt.Errorf("Failed to read CA certificate from %s: %v", caCertPath, err) + } + + // Start with system CA certificates + caCertPool, err := x509.SystemCertPool() + if err != nil { + slog.Warn("Failed to load system CA certificates, using empty pool", "error", err) + caCertPool = x509.NewCertPool() + } else { + slog.Debug("System CA certificates loaded from system trust store") + } + + // Parse the PEM block first to validate format + block, _ := pem.Decode(caCertData) + if block == nil || block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("Failed to parse CA certificate from %s: not valid PEM format\n\nExpected format:\n -----BEGIN CERTIFICATE-----\n ...\n -----END CERTIFICATE-----\n\nUse 'openssl x509 -in %s -text -noout' to verify the certificate.", caCertPath, caCertPath) + } + + // Parse the certificate to ensure it's valid + _, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("Failed to parse CA certificate from %s: invalid certificate data: %v\n\nUse 'openssl x509 -in %s -text -noout' to verify the certificate.", caCertPath, err, caCertPath) + } + + // Add the custom CA certificate + if !caCertPool.AppendCertsFromPEM(caCertData) { + return nil, fmt.Errorf("Failed to add CA certificate from %s to certificate pool", caCertPath) + } + + slog.Debug("Custom CA certificate loaded", "path", caCertPath) + return caCertPool, nil +} \ No newline at end of file diff --git a/internal/service/ca_test.go b/internal/service/ca_test.go new file mode 100644 index 0000000..a16a235 --- /dev/null +++ b/internal/service/ca_test.go @@ -0,0 +1,317 @@ +package service + +import ( + "net/http" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestLoadCACertificate(t *testing.T) { + tests := []struct { + name string + setupFunc func(string) error + caCertPath string + expectError bool + errorContains string + }{ + { + name: "empty path returns system certs", + caCertPath: "", + expectError: false, + }, + { + name: "nonexistent file", + caCertPath: "/nonexistent/file.pem", + expectError: true, + errorContains: "file does not exist", + }, + { + name: "valid PEM certificate", + setupFunc: func(path string) error { + // Create a sample valid PEM certificate + validPEM := `-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUJkQOlSxfNroAhpQ9RvvaA+NpG5IwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNTExMTUyMDE5NTBaFw0yNjExMTUy +MDE5NTBaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCkUKFcR5T8Wnf7sAfgHMNoVHxZAToufffFPP/UitdigSrokhDT +3SQ37dYJ/wrerGdBT5kCnfFnfyO7fE+0n4zKxFe4AAt198K+8lBi4/PyepRDYOtO +BkATnyu6idXvg5cFja5cg+qJ1Ccua8e56R8x4e5nOmVmKdKCP8hEE33cROhFkDqp +z7K7lVqjWOSK9nzKG6Rvsz02/1iAW6/LN3nqII65ju1uSIvweEr8uRvjv70zt1Mn +lajQTbjj18tqthaP2BVfNlw/OMG7ijzbc8N7bDgW/lUz41vb15uA3dHdZnv7OZGF +vG8KUbG9htBZOnU2oXV3qnzrRx8cz2DBpMcNAgMBAAGjUzBRMB0GA1UdDgQWBBSm +OgGLv48OGFBQrrbxrHOCYnV5lzAfBgNVHSMEGDAWgBSmOgGLv48OGFBQrrbxrHOC +YnV5lzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBJwynG1SZ2 +8R2shQmASL91t1fnFoZfAm2bEpUvw03BjjLW+wMbYx3xKER3ZZ57Rizagj/LFZPQ +2sASrpArJOXBVZHuKUwKnfp0uCpy5n+gdqPfEuGOp9wm5HQaK3JZaqLo6/3AM4Oh +6UAOxSzo6cf1PWGpxoHn51JjBlwNnQbxgtq4YhmTuuoouXGPqTD8QrOa5Yu5qUh5 +jfqo/7/4VI0vahto6nF0q369a+hRuo+sHqOdX0i343lthZoa5SaiOuR0yihZbOhx +2l4rT3C3MuThnsar+axNQTXm+HEQuuc/eDqzdGUkY9I14LuKz3C+DwEwmbrcrGRM +yUOQDT7vpIja +-----END CERTIFICATE-----` + return os.WriteFile(path, []byte(validPEM), 0644) + }, + expectError: false, + }, + { + name: "invalid PEM format", + caCertPath: "temp", // This will trigger temp file creation + setupFunc: func(path string) error { + // Create an invalid PEM file that looks like PEM with proper Base64 encoding but invalid cert data + invalidPEM := `-----BEGIN CERTIFICATE----- +VGhpcyBpcyBub3QgYSB2YWxpZCBjZXJ0aWZpY2F0ZSBjb250ZW50 +-----END CERTIFICATE-----` + return os.WriteFile(path, []byte(invalidPEM), 0644) + }, + expectError: true, + errorContains: "invalid certificate data", + }, + { + name: "completely invalid format", + caCertPath: "temp", // This will trigger temp file creation + setupFunc: func(path string) error { + // Create a file without proper PEM headers + invalidFormat := `This is not a PEM file at all` + return os.WriteFile(path, []byte(invalidFormat), 0644) + }, + expectError: true, + errorContains: "not valid PEM format", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var testFilePath string + + if tt.caCertPath == "" { + // Empty path test + testFilePath = tt.caCertPath + } else if tt.caCertPath[0] == '/' && filepath.Dir(tt.caCertPath) == "/nonexistent" { + // Non-existent file test - use the path as is + testFilePath = tt.caCertPath + } else { + // Create a temporary file for other tests + tmpFile, err := os.CreateTemp("", "test-ca-*.pem") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + tmpFile.Close() + testFilePath = tmpFile.Name() + defer os.Remove(testFilePath) + + if tt.setupFunc != nil { + err := tt.setupFunc(testFilePath) + if err != nil { + t.Fatalf("Setup function failed: %v", err) + } + } + } + + certPool, err := loadCACertificate(testFilePath) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + return + } + if certPool == nil { + t.Error("Expected cert pool but got nil") + } + } + }) + } +} + +func TestNewServiceCommand_CACertificates(t *testing.T) { + tests := []struct { + name string + setupFunc func(string) error + caCertPath string + expectError bool + checkTLS func(*testing.T, *http.Client) + }{ + { + name: "no ca cert path", + caCertPath: "", + expectError: false, + checkTLS: func(t *testing.T, client *http.Client) { + // Should not have custom transport + if client.Transport != nil { + t.Error("Expected no custom transport for empty CA cert path") + } + }, + }, + { + name: "valid ca cert path", + caCertPath: "temp", // This will trigger temp file creation + setupFunc: func(path string) error { + validPEM := `-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUJkQOlSxfNroAhpQ9RvvaA+NpG5IwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNTExMTUyMDE5NTBaFw0yNjExMTUy +MDE5NTBaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCkUKFcR5T8Wnf7sAfgHMNoVHxZAToufffFPP/UitdigSrokhDT +3SQ37dYJ/wrerGdBT5kCnfFnfyO7fE+0n4zKxFe4AAt198K+8lBi4/PyepRDYOtO +BkATnyu6idXvg5cFja5cg+qJ1Ccua8e56R8x4e5nOmVmKdKCP8hEE33cROhFkDqp +z7K7lVqjWOSK9nzKG6Rvsz02/1iAW6/LN3nqII65ju1uSIvweEr8uRvjv70zt1Mn +lajQTbjj18tqthaP2BVfNlw/OMG7ijzbc8N7bDgW/lUz41vb15uA3dHdZnv7OZGF +vG8KUbG9htBZOnU2oXV3qnzrRx8cz2DBpMcNAgMBAAGjUzBRMB0GA1UdDgQWBBSm +OgGLv48OGFBQrrbxrHOCYnV5lzAfBgNVHSMEGDAWgBSmOgGLv48OGFBQrrbxrHOC +YnV5lzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBJwynG1SZ2 +8R2shQmASL91t1fnFoZfAm2bEpUvw03BjjLW+wMbYx3xKER3ZZ57Rizagj/LFZPQ +2sASrpArJOXBVZHuKUwKnfp0uCpy5n+gdqPfEuGOp9wm5HQaK3JZaqLo6/3AM4Oh +6UAOxSzo6cf1PWGpxoHn51JjBlwNnQbxgtq4YhmTuuoouXGPqTD8QrOa5Yu5qUh5 +jfqo/7/4VI0vahto6nF0q369a+hRuo+sHqOdX0i343lthZoa5SaiOuR0yihZbOhx +2l4rT3C3MuThnsar+axNQTXm+HEQuuc/eDqzdGUkY9I14LuKz3C+DwEwmbrcrGRM +yUOQDT7vpIja +-----END CERTIFICATE-----` + return os.WriteFile(path, []byte(validPEM), 0644) + }, + expectError: false, + checkTLS: func(t *testing.T, client *http.Client) { + if client.Transport == nil { + t.Error("Expected HTTP transport to be set for CA cert") + return + } + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Error("Expected HTTP transport to be *http.Transport type") + return + } + if transport.TLSClientConfig == nil { + t.Error("Expected TLS config to be set for CA cert") + return + } + if transport.TLSClientConfig.RootCAs == nil { + t.Error("Expected RootCAs to be set for CA cert") + } + if transport.TLSClientConfig.InsecureSkipVerify { + t.Error("Expected InsecureSkipVerify to be false when using CA cert") + } + }, + }, + { + name: "invalid ca cert path", + caCertPath: "/nonexistent/file.pem", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var testFilePath string + + if tt.caCertPath == "" { + testFilePath = "" + } else if tt.caCertPath[0] == '/' && filepath.Dir(tt.caCertPath) == "/nonexistent" { + testFilePath = tt.caCertPath + } else { + tmpFile, err := os.CreateTemp("", "test-ca-*.pem") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + tmpFile.Close() + testFilePath = tmpFile.Name() + defer os.Remove(testFilePath) + + if tt.setupFunc != nil { + err := tt.setupFunc(testFilePath) + if err != nil { + t.Fatalf("Setup function failed: %v", err) + } + } + } + + svc, err := NewServiceCommand(getTestAPI(), nil, false, false, false, testFilePath) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + return + } + if svc == nil { + t.Error("Expected service command but got nil") + return + } + if svc.CACertPath != testFilePath { + t.Errorf("Expected CACertPath to be %s, got %s", testFilePath, svc.CACertPath) + } + if tt.checkTLS != nil { + tt.checkTLS(t, svc.Client) + } + } + }) + } +} + +func TestNewServiceCommand_InsecureOverridesCA(t *testing.T) { + // Create a temporary CA certificate file + tmpFile, err := os.CreateTemp("", "test-ca-*.pem") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + tmpFile.Close() + + validPEM := `-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUJkQOlSxfNroAhpQ9RvvaA+NpG5IwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNTExMTUyMDE5NTBaFw0yNjExMTUy +MDE5NTBaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCkUKFcR5T8Wnf7sAfgHMNoVHxZAToufffFPP/UitdigSrokhDT +3SQ37dYJ/wrerGdBT5kCnfFnfyO7fE+0n4zKxFe4AAt198K+8lBi4/PyepRDYOtO +BkATnyu6idXvg5cFja5cg+qJ1Ccua8e56R8x4e5nOmVmKdKCP8hEE33cROhFkDqp +z7K7lVqjWOSK9nzKG6Rvsz02/1iAW6/LN3nqII65ju1uSIvweEr8uRvjv70zt1Mn +lajQTbjj18tqthaP2BVfNlw/OMG7ijzbc8N7bDgW/lUz41vb15uA3dHdZnv7OZGF +vG8KUbG9htBZOnU2oXV3qnzrRx8cz2DBpMcNAgMBAAGjUzBRMB0GA1UdDgQWBBSm +OgGLv48OGFBQrrbxrHOCYnV5lzAfBgNVHSMEGDAWgBSmOgGLv48OGFBQrrbxrHOC +YnV5lzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBJwynG1SZ2 +8R2shQmASL91t1fnFoZfAm2bEpUvw03BjjLW+wMbYx3xKER3ZZ57Rizagj/LFZPQ +2sASrpArJOXBVZHuKUwKnfp0uCpy5n+gdqPfEuGOp9wm5HQaK3JZaqLo6/3AM4Oh +6UAOxSzo6cf1PWGpxoHn51JjBlwNnQbxgtq4YhmTuuoouXGPqTD8QrOa5Yu5qUh5 +jfqo/7/4VI0vahto6nF0q369a+hRuo+sHqOdX0i343lthZoa5SaiOuR0yihZbOhx +2l4rT3C3MuThnsar+axNQTXm+HEQuuc/eDqzdGUkY9I14LuKz3C+DwEwmbrcrGRM +yUOQDT7vpIja +-----END CERTIFICATE-----` + err = os.WriteFile(tmpFile.Name(), []byte(validPEM), 0644) + if err != nil { + t.Fatalf("Failed to write temp file: %v", err) + } + + // Test that insecure flag overrides CA certificate + svc, err := NewServiceCommand(getTestAPI(), nil, false, false, true, tmpFile.Name()) + if err != nil { + t.Fatalf("Expected no error but got: %v", err) + } + + transport, ok := svc.Client.Transport.(*http.Transport) + if !ok { + t.Error("Expected HTTP transport to be set") + return + } + + if transport.TLSClientConfig == nil { + t.Error("Expected TLS config to be set") + return + } + + if !transport.TLSClientConfig.InsecureSkipVerify { + t.Error("Expected InsecureSkipVerify to be true when insecure flag is set") + } + + // RootCAs should not be set when insecure is true + if transport.TLSClientConfig.RootCAs != nil { + t.Error("Expected RootCAs to be nil when insecure flag is set") + } +} diff --git a/internal/service/service.go b/internal/service/service.go index 5de3c8e..f63548d 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -16,32 +16,50 @@ import ( ) type ServiceCommand struct { - API api.API - Headers map[string]string - DryRun bool - LogHTTP bool - Insecure bool - Client *http.Client + API api.API + Headers map[string]string + DryRun bool + LogHTTP bool + Insecure bool + CACertPath string + Client *http.Client } -func NewServiceCommand(api *api.API, headers map[string]string, dryRun bool, logHTTP bool, insecure bool) *ServiceCommand { +func NewServiceCommand(api *api.API, headers map[string]string, dryRun bool, logHTTP bool, insecure bool, caCertPath string) (*ServiceCommand, error) { client := &http.Client{} - + if insecure { + slog.Debug("Loading TLS configuration") + slog.Debug("TLS certificate verification disabled (insecure mode)") tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client.Transport = tr + } else if caCertPath != "" { + slog.Debug("Loading TLS configuration") + // Load custom CA certificate + caCertPool, err := loadCACertificate(caCertPath) + if err != nil { + return nil, err + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: caCertPool}, + } + client.Transport = tr + } else { + // Use default system CA certificates + slog.Debug("Using system CA certificates") } - + return &ServiceCommand{ - API: *api, - Headers: headers, - DryRun: dryRun, - LogHTTP: logHTTP, - Insecure: insecure, - Client: client, - } + API: *api, + Headers: headers, + DryRun: dryRun, + LogHTTP: logHTTP, + Insecure: insecure, + CACertPath: caCertPath, + Client: client, + }, nil } func (s *ServiceCommand) Execute(args []string) (*Result, error) { diff --git a/internal/service/service_test.go b/internal/service/service_test.go index 3f0e28b..12c3d76 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -8,7 +8,10 @@ import ( func TestService_ExecuteCommand_ListResources(t *testing.T) { // Test setup - svc := NewServiceCommand(getTestAPI(), nil, false, false, false) + svc, err := NewServiceCommand(getTestAPI(), nil, false, false, false, "") + if err != nil { + t.Fatalf("Failed to create service command: %v", err) + } tests := []struct { name string @@ -77,7 +80,10 @@ func TestNewServiceCommand_Insecure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - svc := NewServiceCommand(getTestAPI(), nil, false, false, tt.insecure) + svc, err := NewServiceCommand(getTestAPI(), nil, false, false, tt.insecure, "") + if err != nil { + t.Fatalf("Failed to create service command: %v", err) + } if svc.Insecure != tt.insecure { t.Errorf("NewServiceCommand() insecure = %v, want %v", svc.Insecure, tt.insecure)