diff --git a/internal/utils/private_key.go b/internal/utils/private_key.go index 28e6a86aa..27399e557 100644 --- a/internal/utils/private_key.go +++ b/internal/utils/private_key.go @@ -18,6 +18,9 @@ package utils import ( "context" + "fmt" + "os" + "strings" "github.com/spf13/afero" ) @@ -26,7 +29,43 @@ import ( // This follows the same pattern as cosignSig.PublicKeyFromKeyRef but for private keys. // Supported formats: // - File path: "/path/to/private-key.pem" +// - Kubernetes secret: "k8s://namespace/secret-name" // - Kubernetes secret: "k8s://namespace/secret-name/key-field" func PrivateKeyFromKeyRef(ctx context.Context, keyRef string, fs afero.Fs) ([]byte, error) { - return KeyFromKeyRef(ctx, keyRef, fs) + // If the key-field is not specified assume it is "cosign.key" + adjustedKeyRef := keyRef + if strings.HasPrefix(keyRef, "k8s://") { + parts := strings.Split(strings.TrimPrefix(keyRef, "k8s://"), "/") + if len(parts) == 2 { + adjustedKeyRef = fmt.Sprintf("%s/cosign.key", keyRef) + } + } + return KeyFromKeyRef(ctx, adjustedKeyRef, fs) +} + +// PasswordFromKeyRef resolves a password from either environment variable or a Kubernetes secret reference. +// This provides a unified interface for password resolution similar to PrivateKeyFromKeyRef. +// Supported formats: +// - Environment variable: "" (empty string uses COSIGN_PASSWORD env var) +// - Kubernetes secret: "k8s://namespace/secret-name" (assumes "cosign.password" key) +// - Kubernetes secret: "k8s://namespace/secret-name/key-field" (explicit key field) +func PasswordFromKeyRef(ctx context.Context, keyRef string) ([]byte, error) { + // If keyRef is empty, use environment variable (backward compatibility) + if keyRef == "" { + return []byte(os.Getenv("COSIGN_PASSWORD")), nil + } + + // If it's a Kubernetes secret reference + if strings.HasPrefix(keyRef, "k8s://") { + // If the key-field is not specified assume it is "cosign.password" + adjustedKeyRef := keyRef + parts := strings.Split(strings.TrimPrefix(keyRef, "k8s://"), "/") + if len(parts) == 2 { + adjustedKeyRef = fmt.Sprintf("%s/cosign.password", keyRef) + } + return KeyFromKeyRef(ctx, adjustedKeyRef, nil) // fs not needed for k8s secrets + } + + // For any other format, treat it as environment variable name + return []byte(os.Getenv(keyRef)), nil } diff --git a/internal/utils/private_key_test.go b/internal/utils/private_key_test.go index 9ae677c8f..1f3cd4e50 100644 --- a/internal/utils/private_key_test.go +++ b/internal/utils/private_key_test.go @@ -63,13 +63,29 @@ func TestPrivateKeyFromKeyRef(t *testing.T) { expectErr: false, }, { - name: "k8s secret with multiple keys (no key field specified)", + name: "k8s secret with multiple keys (no key field specified, defaults to cosign.key)", keyRef: "k8s://test-namespace/multi-key-secret", setup: func(fs afero.Fs, ctx context.Context) { // This will be handled in the test loop }, expectErr: true, - errMsg: "contains multiple keys, please specify the key field", + errMsg: "key field \"cosign.key\" not found in secret", + }, + { + name: "k8s secret with default cosign.key field", + keyRef: "k8s://test-namespace/cosign-key-secret", + setup: func(fs afero.Fs, ctx context.Context) { + // This will be handled in the test loop + }, + expectErr: false, + }, + { + name: "k8s secret with cosign.key among multiple keys (defaults to cosign.key)", + keyRef: "k8s://test-namespace/mixed-secret", + setup: func(fs afero.Fs, ctx context.Context) { + // This will be handled in the test loop + }, + expectErr: false, }, { name: "invalid k8s format", @@ -127,6 +143,28 @@ func TestPrivateKeyFromKeyRef(t *testing.T) { "key2": []byte("key2 content"), }, }) + } else if tt.keyRef == "k8s://test-namespace/cosign-key-secret" { + secrets = append(secrets, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-key-secret", + Namespace: "test-namespace", + }, + Data: map[string][]byte{ + "cosign.key": []byte("default cosign key content"), + }, + }) + } else if tt.keyRef == "k8s://test-namespace/mixed-secret" { + secrets = append(secrets, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-secret", + Namespace: "test-namespace", + }, + Data: map[string][]byte{ + "cosign.key": []byte("mixed secret cosign key content"), + "other-key": []byte("other key content"), + "another-key": []byte("another key content"), + }, + }) } if len(secrets) > 0 { @@ -158,6 +196,10 @@ func TestPrivateKeyFromKeyRef(t *testing.T) { assert.Equal(t, []byte("single key content"), keyBytes) } else if tt.keyRef == "k8s://test-namespace/test-secret/private-key" { assert.Equal(t, []byte("test private key content"), keyBytes) + } else if tt.keyRef == "k8s://test-namespace/cosign-key-secret" { + assert.Equal(t, []byte("default cosign key content"), keyBytes) + } else if tt.keyRef == "k8s://test-namespace/mixed-secret" { + assert.Equal(t, []byte("mixed secret cosign key content"), keyBytes) } } }) diff --git a/internal/validate/vsa/attest.go b/internal/validate/vsa/attest.go index 9f2a9f77c..a7579e5df 100644 --- a/internal/validate/vsa/attest.go +++ b/internal/validate/vsa/attest.go @@ -23,7 +23,6 @@ import ( "encoding/json" "fmt" "io" - "os" "path/filepath" "strings" @@ -67,8 +66,12 @@ func NewSigner(ctx context.Context, keyRef string, fs afero.Fs) (*Signer, error) return nil, fmt.Errorf("resolve private key %q: %w", keyRef, err) } - // TODO maybe: Consider another env var for the key password - signerVerifier, err := LoadPrivateKey(keyBytes, []byte(os.Getenv("COSIGN_PASSWORD"))) + password, err := utils.PasswordFromKeyRef(ctx, keyRef) + if err != nil { + return nil, fmt.Errorf("resolve private key password: %w", err) + } + + signerVerifier, err := LoadPrivateKey(keyBytes, password) if err != nil { return nil, fmt.Errorf("load private key: %w", err) } diff --git a/internal/validate/vsa/attest_test.go b/internal/validate/vsa/attest_test.go index cabdc1196..31a6924be 100644 --- a/internal/validate/vsa/attest_test.go +++ b/internal/validate/vsa/attest_test.go @@ -446,7 +446,8 @@ func TestNewSigner_Comprehensive(t *testing.T) { Namespace: "test-namespace", }, Data: map[string][]byte{ - "private-key": []byte("test private key content"), + "private-key": []byte("test private key content"), + "cosign.password": []byte("test password"), }, }) ctx = context.WithValue(ctx, utils.K8sClientKey, client)