Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion internal/utils/private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package utils

import (
"context"
"fmt"
"os"
"strings"

"github.com/spf13/afero"
)
Expand All @@ -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
}
46 changes: 44 additions & 2 deletions internal/utils/private_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
})
Expand Down
9 changes: 6 additions & 3 deletions internal/validate/vsa/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"

Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/validate/vsa/attest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading