Skip to content

Commit 91a92a3

Browse files
authored
feat(certificates): add --ca-cert flag (#24)
Adding a flag to allow passing of custom ca-certificates. fixes #18.
1 parent c9cfba7 commit 91a92a3

File tree

8 files changed

+437
-19
lines changed

8 files changed

+437
-19
lines changed

cmd/aepcli/core.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func configCmd(configFile string) *cobra.Command {
9494
var serverURL string
9595
var headers []string
9696
var pathPrefix string
97+
var caCertPath string
9798

9899
configCmd := &cobra.Command{
99100
Use: "config",
@@ -111,6 +112,7 @@ func configCmd(configFile string) *cobra.Command {
111112
ServerURL: serverURL,
112113
Headers: headers,
113114
PathPrefix: pathPrefix,
115+
CACertPath: caCertPath,
114116
}
115117
if err := config.WriteAPIWithName(configFile, api, overwrite); err != nil {
116118
fmt.Printf("Error writing API config: %v\n", err)
@@ -124,6 +126,7 @@ func configCmd(configFile string) *cobra.Command {
124126
addCmd.Flags().StringArrayVar(&headers, "header", []string{}, "Headers in format key=value")
125127
addCmd.Flags().StringVar(&serverURL, "server-url", "", "Server URL")
126128
addCmd.Flags().StringVar(&pathPrefix, "path-prefix", "", "Path prefix")
129+
addCmd.Flags().StringVar(&caCertPath, "ca-cert", "", "Path to custom CA certificate file (PEM format)")
127130
addCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing configuration")
128131

129132
readCmd := &cobra.Command{
@@ -148,6 +151,7 @@ func configCmd(configFile string) *cobra.Command {
148151
fmt.Printf("Server URL: %s\n", api.ServerURL)
149152
fmt.Printf("Headers: %v\n", api.Headers)
150153
fmt.Printf("Path Prefix: %s\n", api.PathPrefix)
154+
fmt.Printf("CA Certificate Path: %s\n", api.CACertPath)
151155
},
152156
}
153157

@@ -173,6 +177,7 @@ func configCmd(configFile string) *cobra.Command {
173177
fmt.Printf("Server URL: %s\n", api.ServerURL)
174178
fmt.Printf("Headers: %v\n", api.Headers)
175179
fmt.Printf("Path Prefix: %s\n", api.PathPrefix)
180+
fmt.Printf("CA Certificate Path: %s\n", api.CACertPath)
176181
fmt.Println()
177182
}
178183
},

cmd/aepcli/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func aepcli(args []string) (int, error) {
3434
var dryRun bool
3535
var logHTTP bool
3636
var insecure bool
37+
var caCertPath string
3738
var logLevel string
3839
var fileAliasOrCore string
3940
var additionalArgs []string
@@ -65,6 +66,7 @@ func aepcli(args []string) (int, error) {
6566
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.")
6667
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.")
6768
rootCmd.PersistentFlags().BoolVar(&insecure, "insecure", false, "Set to true to skip TLS certificate verification. Use with caution.")
69+
rootCmd.PersistentFlags().StringVar(&caCertPath, "ca-cert", "", "Path to custom CA certificate file (PEM format) to add to the trusted certificate pool")
6870
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.")
6971
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.")
7072
rootCmd.PersistentFlags().StringVar(&configFileVar, "config", "", "Path to config file")
@@ -104,6 +106,9 @@ func aepcli(args []string) (int, error) {
104106
if pathPrefix == "" {
105107
pathPrefix = api.PathPrefix
106108
}
109+
if caCertPath == "" {
110+
caCertPath = api.CACertPath
111+
}
107112
headers = append(headers, api.Headers...)
108113
serverURL = api.ServerURL
109114
}
@@ -121,7 +126,10 @@ func aepcli(args []string) (int, error) {
121126
return CODE_ERR, fmt.Errorf("unable to parse headers: %w", err)
122127
}
123128

124-
s = service.NewServiceCommand(api, headersMap, dryRun, logHTTP, insecure)
129+
s, err = service.NewServiceCommand(api, headersMap, dryRun, logHTTP, insecure, caCertPath)
130+
if err != nil {
131+
return CODE_ERR, fmt.Errorf("unable to create service command: %w", err)
132+
}
125133

126134
result, err := s.Execute(additionalArgs)
127135
returnCode := CODE_OK

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type API struct {
2020
ServerURL string
2121
Headers []string
2222
PathPrefix string
23+
CACertPath string
2324
}
2425

2526
func ReadConfigFromFile(file string) (*Config, error) {

internal/config/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func TestConfigEndToEnd(t *testing.T) {
2525
ServerURL: "https://api.example.com",
2626
Headers: []string{"Authorization=Bearer token"},
2727
PathPrefix: "/v1",
28+
CACertPath: "/path/to/ca.pem",
2829
}
2930

3031
// Write API config to file
@@ -54,6 +55,7 @@ func TestWriteAPIWithEmptyName(t *testing.T) {
5455
ServerURL: "https://api.example.com",
5556
Headers: []string{"Authorization=Bearer token"},
5657
PathPrefix: "/v1",
58+
CACertPath: "/path/to/ca.pem",
5759
}
5860

5961
err := WriteAPIWithName(testFile, testAPI, false)

internal/service/ca.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package service
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
)
10+
11+
// loadCACertificate loads a CA certificate from the specified file path and returns a *x509.CertPool
12+
// that includes the system CA certificates plus the custom CA certificate.
13+
func loadCACertificate(caCertPath string) (*x509.CertPool, error) {
14+
if caCertPath == "" {
15+
// Return system CA pool when no custom CA is specified
16+
return x509.SystemCertPool()
17+
}
18+
19+
slog.Debug("Loading custom CA certificate", "path", caCertPath)
20+
21+
// Read the CA certificate file
22+
caCertData, err := os.ReadFile(caCertPath)
23+
if err != nil {
24+
if os.IsNotExist(err) {
25+
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)
26+
}
27+
if os.IsPermission(err) {
28+
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)
29+
}
30+
return nil, fmt.Errorf("Failed to read CA certificate from %s: %v", caCertPath, err)
31+
}
32+
33+
// Start with system CA certificates
34+
caCertPool, err := x509.SystemCertPool()
35+
if err != nil {
36+
slog.Warn("Failed to load system CA certificates, using empty pool", "error", err)
37+
caCertPool = x509.NewCertPool()
38+
} else {
39+
slog.Debug("System CA certificates loaded from system trust store")
40+
}
41+
42+
// Parse the PEM block first to validate format
43+
block, _ := pem.Decode(caCertData)
44+
if block == nil || block.Type != "CERTIFICATE" {
45+
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)
46+
}
47+
48+
// Parse the certificate to ensure it's valid
49+
_, err = x509.ParseCertificate(block.Bytes)
50+
if err != nil {
51+
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)
52+
}
53+
54+
// Add the custom CA certificate
55+
if !caCertPool.AppendCertsFromPEM(caCertData) {
56+
return nil, fmt.Errorf("Failed to add CA certificate from %s to certificate pool", caCertPath)
57+
}
58+
59+
slog.Debug("Custom CA certificate loaded", "path", caCertPath)
60+
return caCertPool, nil
61+
}

0 commit comments

Comments
 (0)