From 88b337c0873375606d375768b3a76271c52ccc40 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 10 Feb 2026 10:00:39 +0100 Subject: [PATCH 1/6] feat(cli): support inline CA certificates in gRPC connections Store CA certificate content (base64-encoded) in config instead of file paths, enabling portable configurations across environments. The gRPC connection layer now accepts CA content directly via WithCAContent option. Signed-off-by: Javier Rodriguez --- app/cli/cmd/config_save.go | 3 ++ app/cli/cmd/root.go | 32 +++++++++++++++-- app/cli/pkg/action/action.go | 7 +++- pkg/grpcconn/grpcconn.go | 66 +++++++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/app/cli/cmd/config_save.go b/app/cli/cmd/config_save.go index 25851ecaf..6441ed442 100644 --- a/app/cli/cmd/config_save.go +++ b/app/cli/cmd/config_save.go @@ -29,6 +29,9 @@ func newConfigSaveCmd() *cobra.Command { skipActionOptsInit: trueString, }, RunE: func(cmd *cobra.Command, args []string) error { + // Process CA flags - read file contents and encode to base64 if needed + processCAFlag(confOptions.controlplaneCA) + processCAFlag(confOptions.CASCA) return viper.WriteConfig() }, } diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index ab4d2a6b8..01d9d93c8 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -18,6 +18,7 @@ package cmd import ( "context" "crypto/sha256" + "encoding/base64" "errors" "fmt" "os" @@ -123,8 +124,13 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { grpcconn.WithInsecure(apiInsecure()), } - if caFilePath := viper.GetString(confOptions.controlplaneCA.viperKey); caFilePath != "" { - opts = append(opts, grpcconn.WithCAFile(caFilePath)) + if caValue := viper.GetString(confOptions.controlplaneCA.viperKey); caValue != "" { + // Check if it's a file path or base64/PEM content + if grpcconn.IsFilePath(caValue) { + opts = append(opts, grpcconn.WithCAFile(caValue)) + } else { + opts = append(opts, grpcconn.WithCAContent(caValue)) + } } controlplaneURL := viper.GetString(confOptions.controlplaneAPI.viperKey) @@ -495,3 +501,25 @@ func isAPITokenPreferred(cmd *cobra.Command) bool { func getConfigDir(appName string) string { return filepath.Join(xdg.ConfigHome, appName) } + +// processCAFlag reads CA file content and encodes it to base64 if value is a file path +func processCAFlag(opt *confOpt) { + value := viper.GetString(opt.viperKey) + if value == "" { + return + } + + // If it's a file path, read and encode + if grpcconn.IsFilePath(value) { + content, err := os.ReadFile(value) + if err != nil { + // Log warning but don't fail + logger.Warn().Err(err).Str("file", value).Msg("Failed to read CA file") + return + } + + // Store base64-encoded content in viper (will be persisted by config save) + encoded := base64.StdEncoding.EncodeToString(content) + viper.Set(opt.viperKey, encoded) + } +} diff --git a/app/cli/pkg/action/action.go b/app/cli/pkg/action/action.go index 55225bbdf..608e635cd 100644 --- a/app/cli/pkg/action/action.go +++ b/app/cli/pkg/action/action.go @@ -142,7 +142,12 @@ func getCASBackend(ctx context.Context, client pb.AttestationServiceClient, work opts := []grpcconn.Option{grpcconn.WithInsecure(casConnectionInsecure)} if casCAPath != "" { - opts = append(opts, grpcconn.WithCAFile(casCAPath)) + // Check if it's a file path or base64/PEM content + if grpcconn.IsFilePath(casCAPath) { + opts = append(opts, grpcconn.WithCAFile(casCAPath)) + } else { + opts = append(opts, grpcconn.WithCAContent(casCAPath)) + } } artifactCASConn, err := grpcconn.New(casURI, result.Token, opts...) diff --git a/pkg/grpcconn/grpcconn.go b/pkg/grpcconn/grpcconn.go index d72010589..ff52a2924 100644 --- a/pkg/grpcconn/grpcconn.go +++ b/pkg/grpcconn/grpcconn.go @@ -18,8 +18,10 @@ package grpcconn import ( "context" "crypto/x509" + "encoding/base64" "fmt" "os" + "strings" grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" "google.golang.org/grpc" @@ -29,6 +31,7 @@ import ( type newOptionalArg struct { caFilePath string + caContent string insecure bool orgName string } @@ -41,6 +44,13 @@ func WithCAFile(caFilePath string) Option { } } +// WithCAContent sets the CA certificate content (PEM format or base64-encoded) +func WithCAContent(content string) Option { + return func(opt *newOptionalArg) { + opt.caContent = content + } +} + func WithInsecure(insecure bool) Option { return func(opt *newOptionalArg) { opt.insecure = insecure @@ -83,7 +93,13 @@ func New(uri, authToken string, opt ...Option) (*grpc.ClientConn, error) { return nil, err } - if optionalArgs.caFilePath != "" { + // Load CA from content if provided (takes precedence) + if optionalArgs.caContent != "" { + if err = appendCAFromContent(optionalArgs.caContent, certsPool); err != nil { + return nil, fmt.Errorf("failed to load CA from content: %w", err) + } + } else if optionalArgs.caFilePath != "" { + // Fallback to file path for backward compatibility if err = appendCAFromFile(optionalArgs.caFilePath, certsPool); err != nil { return nil, fmt.Errorf("failed to load CA cert: %w", err) } @@ -116,6 +132,27 @@ func appendCAFromFile(path string, certsPool *x509.CertPool) error { return nil } +func appendCAFromContent(content string, certsPool *x509.CertPool) error { + var pemContent []byte + + // Try to decode as base64 first + decoded, err := base64.StdEncoding.DecodeString(content) + if err == nil && len(decoded) > 0 { + // Successfully decoded as base64 + pemContent = decoded + } else { + // Not base64, assume it's PEM content directly + pemContent = []byte(content) + } + + // Append to cert pool + if ok := certsPool.AppendCertsFromPEM(pemContent); !ok { + return fmt.Errorf("failed to append CA cert to pool") + } + + return nil +} + type tokenAuth struct { token string insecure bool @@ -140,3 +177,30 @@ func (t tokenAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[strin func (t tokenAuth) RequireTransportSecurity() bool { return !t.insecure } + +// IsFilePath checks if a value looks like a file path (vs base64/PEM content). +// It returns true if the value appears to be a file path, false if it appears to be content. +func IsFilePath(value string) bool { + // Check if value looks like a file path + // If it starts with /, ./, ../, or ~/, it's a path + // If it contains a newline, it's likely PEM content + // If it's valid base64 without path separators, assume it's content + + if strings.HasPrefix(value, "/") || + strings.HasPrefix(value, "./") || + strings.HasPrefix(value, "../") || + strings.HasPrefix(value, "~/") { + return true + } + + if strings.Contains(value, "\n") { + return false // PEM content + } + + // Check if file exists + if _, err := os.Stat(value); err == nil { + return true + } + + return false +} From 0291a3f7c507a058c95e8af847789f8b80f0edba Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 10 Feb 2026 10:35:33 +0100 Subject: [PATCH 2/6] test(grpcconn): add comprehensive CA loading tests Add unit tests for CA certificate loading functionality including file path detection, PEM content loading, base64-encoded content, and option functions. Tests verify backward compatibility with file paths and new inline content support. Signed-off-by: Javier Rodriguez --- pkg/grpcconn/grpcconn_test.go | 159 +++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/pkg/grpcconn/grpcconn_test.go b/pkg/grpcconn/grpcconn_test.go index e41ca03fc..ea77f58e3 100644 --- a/pkg/grpcconn/grpcconn_test.go +++ b/pkg/grpcconn/grpcconn_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,9 +17,14 @@ package grpcconn import ( "context" + "crypto/x509" + "encoding/base64" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetRequestMetadata(t *testing.T) { @@ -46,3 +51,155 @@ func TestRequireTransportSecurity(t *testing.T) { assert.Equal(t, tc.want, auth.RequireTransportSecurity()) } } + +func TestIsFilePath(t *testing.T) { + // Create a temporary file for testing + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "test.pem") + err := os.WriteFile(tmpFile, []byte("test content"), 0600) + require.NoError(t, err) + + testCases := []struct { + name string + value string + want bool + }{ + { + name: "absolute path", + value: "/path/to/ca.pem", + want: true, + }, + { + name: "relative path with ./", + value: "./ca.pem", + want: true, + }, + { + name: "relative path with ../", + value: "../ca.pem", + want: true, + }, + { + name: "home directory path", + value: "~/ca.pem", + want: true, + }, + { + name: "existing file", + value: tmpFile, + want: true, + }, + { + name: "PEM content with newlines", + value: "-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n-----END CERTIFICATE-----", + want: false, + }, + { + name: "base64 encoded content", + value: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=", + want: false, + }, + { + name: "non-existent file without path prefix", + value: "nonexistent.pem", + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := IsFilePath(tc.value) + assert.Equal(t, tc.want, got, "IsFilePath(%q) = %v, want %v", tc.value, got, tc.want) + }) + } +} + +func TestAppendCAFromFile(t *testing.T) { + // Use the test CA certificate from the devkeys + caPath := "../../devel/devkeys/selfsigned/rootCA.crt" + + // Check if the file exists, skip test if not + if _, err := os.Stat(caPath); os.IsNotExist(err) { + t.Skip("Test CA file not found, skipping test") + } + + certsPool, err := x509.SystemCertPool() + require.NoError(t, err) + + err = appendCAFromFile(caPath, certsPool) + assert.NoError(t, err) +} + +func TestAppendCAFromFile_NonExistent(t *testing.T) { + certsPool, err := x509.SystemCertPool() + require.NoError(t, err) + + err = appendCAFromFile("/nonexistent/ca.pem", certsPool) + assert.Error(t, err) +} + +func TestAppendCAFromContent_PEM(t *testing.T) { + // Use the test CA certificate from the devkeys + caPath := "../../devel/devkeys/selfsigned/rootCA.crt" + + // Check if the file exists, skip test if not + if _, err := os.Stat(caPath); os.IsNotExist(err) { + t.Skip("Test CA file not found, skipping test") + } + + // Read the PEM content + pemContent, err := os.ReadFile(caPath) + require.NoError(t, err) + + certsPool, err := x509.SystemCertPool() + require.NoError(t, err) + + // Test with raw PEM content + err = appendCAFromContent(string(pemContent), certsPool) + assert.NoError(t, err) +} + +func TestAppendCAFromContent_Base64(t *testing.T) { + // Use the test CA certificate from the devkeys + caPath := "../../devel/devkeys/selfsigned/rootCA.crt" + + // Check if the file exists, skip test if not + if _, err := os.Stat(caPath); os.IsNotExist(err) { + t.Skip("Test CA file not found, skipping test") + } + + // Read the PEM content and encode as base64 + pemContent, err := os.ReadFile(caPath) + require.NoError(t, err) + base64Content := base64.StdEncoding.EncodeToString(pemContent) + + certsPool, err := x509.SystemCertPool() + require.NoError(t, err) + + // Test with base64-encoded content + err = appendCAFromContent(base64Content, certsPool) + assert.NoError(t, err) +} + +func TestAppendCAFromContent_Invalid(t *testing.T) { + certsPool, err := x509.SystemCertPool() + require.NoError(t, err) + + // Test with invalid content + err = appendCAFromContent("invalid certificate content", certsPool) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to append CA cert to pool") +} + +func TestWithCAFile(t *testing.T) { + opt := &newOptionalArg{} + WithCAFile("/path/to/ca.pem")(opt) + assert.Equal(t, "/path/to/ca.pem", opt.caFilePath) +} + +func TestWithCAContent(t *testing.T) { + opt := &newOptionalArg{} + testContent := "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----" + WithCAContent(testContent)(opt) + assert.Equal(t, testContent, opt.caContent) +} From a1286ae8712680f244a1f20fa498728477453cd4 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 10 Feb 2026 10:41:14 +0100 Subject: [PATCH 3/6] fix linter Signed-off-by: Javier Rodriguez --- pkg/grpcconn/grpcconn_test.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pkg/grpcconn/grpcconn_test.go b/pkg/grpcconn/grpcconn_test.go index ea77f58e3..b443dc7e8 100644 --- a/pkg/grpcconn/grpcconn_test.go +++ b/pkg/grpcconn/grpcconn_test.go @@ -27,6 +27,8 @@ import ( "github.com/stretchr/testify/require" ) +const caPath = "../../devel/devkeys/selfsigned/rootCA.crt" + func TestGetRequestMetadata(t *testing.T) { const wantOrg = "org-1" want := map[string]string{"authorization": "Bearer token", "Chainloop-Organization": wantOrg} @@ -115,9 +117,6 @@ func TestIsFilePath(t *testing.T) { } func TestAppendCAFromFile(t *testing.T) { - // Use the test CA certificate from the devkeys - caPath := "../../devel/devkeys/selfsigned/rootCA.crt" - // Check if the file exists, skip test if not if _, err := os.Stat(caPath); os.IsNotExist(err) { t.Skip("Test CA file not found, skipping test") @@ -139,9 +138,6 @@ func TestAppendCAFromFile_NonExistent(t *testing.T) { } func TestAppendCAFromContent_PEM(t *testing.T) { - // Use the test CA certificate from the devkeys - caPath := "../../devel/devkeys/selfsigned/rootCA.crt" - // Check if the file exists, skip test if not if _, err := os.Stat(caPath); os.IsNotExist(err) { t.Skip("Test CA file not found, skipping test") @@ -160,9 +156,6 @@ func TestAppendCAFromContent_PEM(t *testing.T) { } func TestAppendCAFromContent_Base64(t *testing.T) { - // Use the test CA certificate from the devkeys - caPath := "../../devel/devkeys/selfsigned/rootCA.crt" - // Check if the file exists, skip test if not if _, err := os.Stat(caPath); os.IsNotExist(err) { t.Skip("Test CA file not found, skipping test") From 6422b1ca3fa3b00fd4455a65ddad07d353a297d8 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 10 Feb 2026 10:46:45 +0100 Subject: [PATCH 4/6] test(grpcconn): add backward compatibility tests Add tests verifying that new clients can consume old configurations with stored file paths, and that the file path detection correctly routes to the legacy loading method. Addresses PR feedback on backward compatibility. Signed-off-by: Javier Rodriguez --- pkg/grpcconn/grpcconn_test.go | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/pkg/grpcconn/grpcconn_test.go b/pkg/grpcconn/grpcconn_test.go index b443dc7e8..2c149312f 100644 --- a/pkg/grpcconn/grpcconn_test.go +++ b/pkg/grpcconn/grpcconn_test.go @@ -196,3 +196,87 @@ func TestWithCAContent(t *testing.T) { WithCAContent(testContent)(opt) assert.Equal(t, testContent, opt.caContent) } + +func TestBackwardCompatibility_StoredFilePath(t *testing.T) { + // This test verifies that if a user has an old config with a stored file path, + // the new code will still load it correctly via the file path method. + + // Check if the file exists, skip test if not + if _, err := os.Stat(caPath); os.IsNotExist(err) { + t.Skip("Test CA file not found, skipping test") + } + + // Simulate an old config with a stored file path + storedValue := caPath + + // Verify IsFilePath detects it as a file path + assert.True(t, IsFilePath(storedValue), "stored file path should be detected as a file path") + + // Verify it can be loaded using the file path method + certsPool, err := x509.SystemCertPool() + require.NoError(t, err) + + err = appendCAFromFile(storedValue, certsPool) + assert.NoError(t, err, "should successfully load CA from stored file path") +} + +func TestBackwardCompatibility_NewClientOldConfig(t *testing.T) { + // This test verifies the complete flow: new client reading old config with file path + + // Check if the file exists, skip test if not + if _, err := os.Stat(caPath); os.IsNotExist(err) { + t.Skip("Test CA file not found, skipping test") + } + + // Simulate config value (could be file path or content) + oldConfigValue := caPath + + // New client logic: detect and load appropriately + var opts []Option + if IsFilePath(oldConfigValue) { + opts = append(opts, WithCAFile(oldConfigValue)) + } else { + opts = append(opts, WithCAContent(oldConfigValue)) + } + + // Verify the correct option was chosen + require.Len(t, opts, 1) + + // Apply the option and verify it set caFilePath (not caContent) + optArg := &newOptionalArg{} + opts[0](optArg) + assert.Equal(t, caPath, optArg.caFilePath, "should use file path method for old config") + assert.Empty(t, optArg.caContent, "should not use content method for old config") +} + +func TestBackwardCompatibility_OldClientNewConfig(t *testing.T) { + // This test verifies that if a path is stored in config, both old and new + // clients can load it. Old clients would directly use WithCAFile, new clients + // would detect it via IsFilePath and use WithCAFile. + + // Check if the file exists, skip test if not + if _, err := os.Stat(caPath); os.IsNotExist(err) { + t.Skip("Test CA file not found, skipping test") + } + + // Stored config value (file path) + configValue := caPath + + certsPool1, err := x509.SystemCertPool() + require.NoError(t, err) + + certsPool2, err := x509.SystemCertPool() + require.NoError(t, err) + + // Old client behavior: directly use file path + err = appendCAFromFile(configValue, certsPool1) + assert.NoError(t, err, "old client should load file path") + + // New client behavior: detect then use file path + if IsFilePath(configValue) { + err = appendCAFromFile(configValue, certsPool2) + } else { + err = appendCAFromContent(configValue, certsPool2) + } + assert.NoError(t, err, "new client should load file path via detection") +} From 739cf62c16eadacf8d4f70a0a9ba2c254dd2ff06 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 10 Feb 2026 11:35:40 +0100 Subject: [PATCH 5/6] simplfy logic Signed-off-by: Javier Rodriguez --- app/cli/cmd/config_save.go | 10 +++-- app/cli/cmd/root.go | 18 ++++----- app/cli/pkg/action/action.go | 6 +-- pkg/grpcconn/grpcconn.go | 30 +-------------- pkg/grpcconn/grpcconn_test.go | 70 ++--------------------------------- 5 files changed, 24 insertions(+), 110 deletions(-) diff --git a/app/cli/cmd/config_save.go b/app/cli/cmd/config_save.go index 6441ed442..fd3210a4b 100644 --- a/app/cli/cmd/config_save.go +++ b/app/cli/cmd/config_save.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -30,8 +30,12 @@ func newConfigSaveCmd() *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { // Process CA flags - read file contents and encode to base64 if needed - processCAFlag(confOptions.controlplaneCA) - processCAFlag(confOptions.CASCA) + if err := processCAFlag(confOptions.controlplaneCA); err != nil { + return err + } + if err := processCAFlag(confOptions.CASCA); err != nil { + return err + } return viper.WriteConfig() }, } diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 01d9d93c8..351980a5f 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -125,8 +125,8 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { } if caValue := viper.GetString(confOptions.controlplaneCA.viperKey); caValue != "" { - // Check if it's a file path or base64/PEM content - if grpcconn.IsFilePath(caValue) { + // Check if the value is a file path, if it is we read the content and encode it to base64, if not we assume it's the content already + if _, err := os.Stat(caValue); err == nil { opts = append(opts, grpcconn.WithCAFile(caValue)) } else { opts = append(opts, grpcconn.WithCAContent(caValue)) @@ -503,23 +503,23 @@ func getConfigDir(appName string) string { } // processCAFlag reads CA file content and encodes it to base64 if value is a file path -func processCAFlag(opt *confOpt) { +func processCAFlag(opt *confOpt) error { value := viper.GetString(opt.viperKey) if value == "" { - return + return nil } // If it's a file path, read and encode - if grpcconn.IsFilePath(value) { + if _, err := os.Stat(value); err == nil { content, err := os.ReadFile(value) if err != nil { - // Log warning but don't fail - logger.Warn().Err(err).Str("file", value).Msg("Failed to read CA file") - return + return fmt.Errorf("failed to read CA file %s: %w", value, err) } // Store base64-encoded content in viper (will be persisted by config save) encoded := base64.StdEncoding.EncodeToString(content) viper.Set(opt.viperKey, encoded) } + + return nil } diff --git a/app/cli/pkg/action/action.go b/app/cli/pkg/action/action.go index 608e635cd..aef2d9707 100644 --- a/app/cli/pkg/action/action.go +++ b/app/cli/pkg/action/action.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -142,8 +142,8 @@ func getCASBackend(ctx context.Context, client pb.AttestationServiceClient, work opts := []grpcconn.Option{grpcconn.WithInsecure(casConnectionInsecure)} if casCAPath != "" { - // Check if it's a file path or base64/PEM content - if grpcconn.IsFilePath(casCAPath) { + // Check if it's a file path or content. If it's a file path, it should exist. If not, treat it as content. + if _, err := os.Stat(casCAPath); err == nil { opts = append(opts, grpcconn.WithCAFile(casCAPath)) } else { opts = append(opts, grpcconn.WithCAContent(casCAPath)) diff --git a/pkg/grpcconn/grpcconn.go b/pkg/grpcconn/grpcconn.go index ff52a2924..ff73febf6 100644 --- a/pkg/grpcconn/grpcconn.go +++ b/pkg/grpcconn/grpcconn.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import ( "encoding/base64" "fmt" "os" - "strings" grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" "google.golang.org/grpc" @@ -177,30 +176,3 @@ func (t tokenAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[strin func (t tokenAuth) RequireTransportSecurity() bool { return !t.insecure } - -// IsFilePath checks if a value looks like a file path (vs base64/PEM content). -// It returns true if the value appears to be a file path, false if it appears to be content. -func IsFilePath(value string) bool { - // Check if value looks like a file path - // If it starts with /, ./, ../, or ~/, it's a path - // If it contains a newline, it's likely PEM content - // If it's valid base64 without path separators, assume it's content - - if strings.HasPrefix(value, "/") || - strings.HasPrefix(value, "./") || - strings.HasPrefix(value, "../") || - strings.HasPrefix(value, "~/") { - return true - } - - if strings.Contains(value, "\n") { - return false // PEM content - } - - // Check if file exists - if _, err := os.Stat(value); err == nil { - return true - } - - return false -} diff --git a/pkg/grpcconn/grpcconn_test.go b/pkg/grpcconn/grpcconn_test.go index 2c149312f..137f8c68d 100644 --- a/pkg/grpcconn/grpcconn_test.go +++ b/pkg/grpcconn/grpcconn_test.go @@ -20,7 +20,6 @@ import ( "crypto/x509" "encoding/base64" "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -54,68 +53,6 @@ func TestRequireTransportSecurity(t *testing.T) { } } -func TestIsFilePath(t *testing.T) { - // Create a temporary file for testing - tmpDir := t.TempDir() - tmpFile := filepath.Join(tmpDir, "test.pem") - err := os.WriteFile(tmpFile, []byte("test content"), 0600) - require.NoError(t, err) - - testCases := []struct { - name string - value string - want bool - }{ - { - name: "absolute path", - value: "/path/to/ca.pem", - want: true, - }, - { - name: "relative path with ./", - value: "./ca.pem", - want: true, - }, - { - name: "relative path with ../", - value: "../ca.pem", - want: true, - }, - { - name: "home directory path", - value: "~/ca.pem", - want: true, - }, - { - name: "existing file", - value: tmpFile, - want: true, - }, - { - name: "PEM content with newlines", - value: "-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n-----END CERTIFICATE-----", - want: false, - }, - { - name: "base64 encoded content", - value: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=", - want: false, - }, - { - name: "non-existent file without path prefix", - value: "nonexistent.pem", - want: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := IsFilePath(tc.value) - assert.Equal(t, tc.want, got, "IsFilePath(%q) = %v, want %v", tc.value, got, tc.want) - }) - } -} - func TestAppendCAFromFile(t *testing.T) { // Check if the file exists, skip test if not if _, err := os.Stat(caPath); os.IsNotExist(err) { @@ -210,7 +147,8 @@ func TestBackwardCompatibility_StoredFilePath(t *testing.T) { storedValue := caPath // Verify IsFilePath detects it as a file path - assert.True(t, IsFilePath(storedValue), "stored file path should be detected as a file path") + _, err := os.Stat(storedValue) + assert.Nil(t, err, "stored file path should be detected as a file path") // Verify it can be loaded using the file path method certsPool, err := x509.SystemCertPool() @@ -233,7 +171,7 @@ func TestBackwardCompatibility_NewClientOldConfig(t *testing.T) { // New client logic: detect and load appropriately var opts []Option - if IsFilePath(oldConfigValue) { + if _, err := os.Stat(oldConfigValue); err == nil { opts = append(opts, WithCAFile(oldConfigValue)) } else { opts = append(opts, WithCAContent(oldConfigValue)) @@ -273,7 +211,7 @@ func TestBackwardCompatibility_OldClientNewConfig(t *testing.T) { assert.NoError(t, err, "old client should load file path") // New client behavior: detect then use file path - if IsFilePath(configValue) { + if _, statErr := os.Stat(configValue); statErr == nil { err = appendCAFromFile(configValue, certsPool2) } else { err = appendCAFromContent(configValue, certsPool2) From afe9e55ca6965264eefe8f2cbe7683d70670e139 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 11 Feb 2026 19:49:38 +0000 Subject: [PATCH 6/6] store PEM directly Signed-off-by: Javier Rodriguez --- app/cli/cmd/root.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 351980a5f..937b76084 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -18,7 +18,6 @@ package cmd import ( "context" "crypto/sha256" - "encoding/base64" "errors" "fmt" "os" @@ -502,23 +501,22 @@ func getConfigDir(appName string) string { return filepath.Join(xdg.ConfigHome, appName) } -// processCAFlag reads CA file content and encodes it to base64 if value is a file path +// processCAFlag reads CA file content and stores it as PEM if value is a file path func processCAFlag(opt *confOpt) error { value := viper.GetString(opt.viperKey) if value == "" { return nil } - // If it's a file path, read and encode + // If it's a file path, read and store PEM content directly if _, err := os.Stat(value); err == nil { content, err := os.ReadFile(value) if err != nil { return fmt.Errorf("failed to read CA file %s: %w", value, err) } - // Store base64-encoded content in viper (will be persisted by config save) - encoded := base64.StdEncoding.EncodeToString(content) - viper.Set(opt.viperKey, encoded) + // Store PEM content directly + viper.Set(opt.viperKey, string(content)) } return nil