From da0a5590b28ac04f7e0a06a5f0def0a7e6c7a93b Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Sat, 14 Dec 2024 13:38:07 -0500 Subject: [PATCH] fix: improves kms key validation across providers. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> --- .../certificate_maker_test.go | 76 +- go.mod | 11 +- go.sum | 28 +- pkg/certmaker/certmaker.go | 45 +- pkg/certmaker/certmaker_test.go | 1123 +++++++++++------ pkg/certmaker/template_test.go | 133 +- 6 files changed, 860 insertions(+), 556 deletions(-) diff --git a/cmd/certificate_maker/certificate_maker_test.go b/cmd/certificate_maker/certificate_maker_test.go index ac810e77..f711605d 100644 --- a/cmd/certificate_maker/certificate_maker_test.go +++ b/cmd/certificate_maker/certificate_maker_test.go @@ -18,11 +18,10 @@ package main import ( "os" "path/filepath" + "strings" "testing" "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestGetConfigValue(t *testing.T) { @@ -84,22 +83,27 @@ func TestGetConfigValue(t *testing.T) { defer os.Unsetenv(tt.envVar) } got := getConfigValue(tt.flagValue, tt.envVar) - assert.Equal(t, tt.want, got) + if got != tt.want { + t.Errorf("got %v, want %v", got, tt.want) + } }) } } func TestInitLogger(t *testing.T) { logger := initLogger() - require.NotNil(t, logger) + if logger == nil { + t.Errorf("logger is nil") + } } func TestRunCreate(t *testing.T) { tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) + if err != nil { + t.Errorf("unexpected error: %v", err) + } defer os.RemoveAll(tmpDir) - // Create test template files rootTemplate := `{ "subject": { "commonName": "Test TSA Root CA" @@ -132,9 +136,13 @@ func TestRunCreate(t *testing.T) { rootTmplPath := filepath.Join(tmpDir, "root-template.json") leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0600) - require.NoError(t, err) + if err != nil { + t.Errorf("unexpected error: %v", err) + } err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0600) - require.NoError(t, err) + if err != nil { + t.Errorf("unexpected error: %v", err) + } tests := []struct { name string @@ -223,7 +231,6 @@ func TestRunCreate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Set environment variables for k, v := range tt.envVars { os.Setenv(k, v) defer os.Unsetenv(k) @@ -234,7 +241,6 @@ func TestRunCreate(t *testing.T) { RunE: runCreate, } - // Add all flags that runCreate expects cmd.Flags().StringVar(&kmsType, "kms-type", "", "KMS provider type (awskms, gcpkms, azurekms)") cmd.Flags().StringVar(&kmsRegion, "aws-region", "", "AWS KMS region") cmd.Flags().StringVar(&kmsKeyID, "kms-key-id", "", "KMS key identifier") @@ -254,17 +260,21 @@ func TestRunCreate(t *testing.T) { err := cmd.Execute() if tt.wantError { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errMsg) + if err == nil { + t.Errorf("expected error, but got nil") + } else if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("error %q should contain %q", err.Error(), tt.errMsg) + } } else { - require.NoError(t, err) + if err != nil { + t.Errorf("unexpected error: %v", err) + } } }) } } func TestCreateCommand(t *testing.T) { - // Create a test command cmd := &cobra.Command{ Use: "test", RunE: func(_ *cobra.Command, _ []string) error { @@ -272,40 +282,50 @@ func TestCreateCommand(t *testing.T) { }, } - // Add flags cmd.Flags().StringVar(&kmsType, "kms-type", "", "KMS type") cmd.Flags().StringVar(&kmsRegion, "aws-region", "", "AWS KMS region") cmd.Flags().StringVar(&rootKeyID, "root-key-id", "", "Root key ID") cmd.Flags().StringVar(&leafKeyID, "leaf-key-id", "", "Leaf key ID") - // Test missing required flags err := cmd.Execute() - require.NoError(t, err) // No required flags set yet + if err != nil { + t.Errorf("unexpected error: %v", err) + } - // Test flag parsing err = cmd.ParseFlags([]string{ "--kms-type", "awskms", "--aws-region", "us-west-2", "--root-key-id", "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab", "--leaf-key-id", "arn:aws:kms:us-west-2:123456789012:key/9876fedc-ba98-7654-3210-fedcba987654", }) - require.NoError(t, err) + if err != nil { + t.Errorf("unexpected error: %v", err) + } - // Verify flag values - assert.Equal(t, "awskms", kmsType) - assert.Equal(t, "us-west-2", kmsRegion) - assert.Equal(t, "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab", rootKeyID) - assert.Equal(t, "arn:aws:kms:us-west-2:123456789012:key/9876fedc-ba98-7654-3210-fedcba987654", leafKeyID) + if kmsType != "awskms" { + t.Errorf("got kms-type %v, want awskms", kmsType) + } + if kmsRegion != "us-west-2" { + t.Errorf("got aws-region %v, want us-west-2", kmsRegion) + } + if rootKeyID != "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" { + t.Errorf("got root-key-id %v, want arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab", rootKeyID) + } + if leafKeyID != "arn:aws:kms:us-west-2:123456789012:key/9876fedc-ba98-7654-3210-fedcba987654" { + t.Errorf("got leaf-key-id %v, want arn:aws:kms:us-west-2:123456789012:key/9876fedc-ba98-7654-3210-fedcba987654", leafKeyID) + } } func TestRootCommand(t *testing.T) { - // Test help output rootCmd.SetArgs([]string{"--help"}) err := rootCmd.Execute() - require.NoError(t, err) + if err != nil { + t.Errorf("unexpected error: %v", err) + } - // Test unknown command rootCmd.SetArgs([]string{"unknown"}) err = rootCmd.Execute() - require.Error(t, err) + if err == nil { + t.Errorf("expected error, but got nil") + } } diff --git a/go.mod b/go.mod index 0a129af4..6fdb15d4 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,6 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 github.com/urfave/negroni v1.0.0 go.step.sm/crypto v0.55.0 go.uber.org/zap v1.27.0 @@ -47,7 +46,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect cloud.google.com/go/iam v1.2.2 // indirect - cloud.google.com/go/kms v1.20.1 // indirect + cloud.google.com/go/kms v1.20.2 // indirect cloud.google.com/go/longrunning v0.6.2 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -56,9 +55,9 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -82,7 +81,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -133,7 +131,6 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect diff --git a/go.sum b/go.sum index be3f5314..5f637698 100644 --- a/go.sum +++ b/go.sum @@ -31,10 +31,10 @@ github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04= @@ -322,16 +322,16 @@ github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbm github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sigstore/sigstore v1.8.10 h1:r4t+TYzJlG9JdFxMy+um9GZhZ2N1hBTyTex0AHEZxFs= -github.com/sigstore/sigstore v1.8.10/go.mod h1:BekjqxS5ZtHNJC4u3Q3Stvfx2eyisbW/lUZzmPU2u4A= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.10 h1:e5GfVngPjGap/N3ODefayt7vKIPS1/v3hWLZ9+4MrN4= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.10/go.mod h1:HOr3AdFPKdND2FNl/sUD5ZifPl1OMJvrbf9xIaaWcus= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.10 h1:9tZEpfIL/ewAG9G87AHe3aVoy8Ujos2F1qLfCckX6jQ= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.10/go.mod h1:VnIAcitund62R45ezK/dtUeEhuRtB3LsAgJ8m0H34zc= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.10 h1:Xre51HdjIIaVo5ox5zyL+6h0tkrx7Ke9Neh7fLmmZK0= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.10/go.mod h1:VNfdklQDbyGJog8S7apdxiEfmYmCkKyxrsCL9xprkTY= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.10 h1:HjfjL3x3dP2kaGqQHVog974cTcKfzFaGjfZyLQ9KXrg= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.10/go.mod h1:jaeEjkTW1p3gUyPjz9lTcT4TydCs208FoyAwIs6bIT4= +github.com/sigstore/sigstore v1.8.11 h1:tEqeQqbT+awtM87ec9KEeSUxT/AFvJNawneYJyAkFrQ= +github.com/sigstore/sigstore v1.8.11/go.mod h1:fdrFQosxCQ4wTL5H1NrZcQkqQ72AQbPjtpcL2QOGKV0= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.11 h1:4jIEBOtqDZHyQNQSw/guGmIY0y3CVdOGQu3l2FNlqpY= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.11/go.mod h1:rzfk1r8p6Mgjp5tidjzNC+/Kh1h6Eh/ON7xI7ApqBSM= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.11 h1:GXL/OitAMBbLg61nbbk0bXOgOIgDgyFE+9T2Ng3P3o8= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.11/go.mod h1:a9KhG9LZJFcGJB2PtFga1jUIUB0gr0Ix44TDMMXUjJU= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.11 h1:jxKeAMOzaxjwEfmpMMYxF5Vf35tEhQOUXURaUx0ctgo= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.11/go.mod h1:fIAOBcL2s+Vq2Fp9WZByUDdWAmhNuZkJGLCUVUjkdtI= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.11 h1:nH6Cpsz9c7v8jpGiJcH+3+zijfdJha+9mK07MAzZjbc= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.11/go.mod h1:bTBdhPvdaDsHccD9zsSHe/q4ah2OXkdfL/qK7JCuRno= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go index c8bb4be9..b9a80820 100644 --- a/pkg/certmaker/certmaker.go +++ b/pkg/certmaker/certmaker.go @@ -228,7 +228,19 @@ func ValidateKMSConfig(config KMSConfig) error { if keyID == "" { return nil } - if !strings.HasPrefix(keyID, "arn:aws:kms:") && !strings.HasPrefix(keyID, "alias/") { + if strings.HasPrefix(keyID, "arn:aws:kms:") { + parts := strings.Split(keyID, ":") + if len(parts) < 6 { + return fmt.Errorf("invalid AWS KMS ARN format for %s", keyType) + } + if parts[3] != config.Region { + return fmt.Errorf("region in ARN (%s) does not match configured region (%s)", parts[3], config.Region) + } + } else if strings.HasPrefix(keyID, "alias/") { + if strings.TrimPrefix(keyID, "alias/") == "" { + return fmt.Errorf("alias name cannot be empty for %s", keyType) + } + } else { return fmt.Errorf("awskms %s must start with 'arn:aws:kms:' or 'alias/'", keyType) } return nil @@ -249,11 +261,20 @@ func ValidateKMSConfig(config KMSConfig) error { if keyID == "" { return nil } - if !strings.HasPrefix(keyID, "projects/") { - return fmt.Errorf("gcpkms %s must start with 'projects/'", keyType) + requiredComponents := []struct { + component string + message string + }{ + {"projects/", "must start with 'projects/'"}, + {"/locations/", "must contain '/locations/'"}, + {"/keyRings/", "must contain '/keyRings/'"}, + {"/cryptoKeys/", "must contain '/cryptoKeys/'"}, + {"/cryptoKeyVersions/", "must contain '/cryptoKeyVersions/'"}, } - if !strings.Contains(keyID, "/locations/") || !strings.Contains(keyID, "/keyRings/") { - return fmt.Errorf("invalid gcpkms key format for %s: %s", keyType, keyID) + for _, req := range requiredComponents { + if !strings.Contains(keyID, req.component) { + return fmt.Errorf("gcpkms %s %s", keyType, req.message) + } } return nil } @@ -269,6 +290,9 @@ func ValidateKMSConfig(config KMSConfig) error { case "azurekms": // Azure KMS validation + if config.Options == nil { + return fmt.Errorf("options map is required for Azure KMS") + } if config.Options["tenant-id"] == "" { return fmt.Errorf("tenant-id is required for Azure KMS") } @@ -276,13 +300,20 @@ func ValidateKMSConfig(config KMSConfig) error { if keyID == "" { return nil } - // Validate format: azurekms:name=;vault= if !strings.HasPrefix(keyID, "azurekms:name=") { return fmt.Errorf("azurekms %s must start with 'azurekms:name='", keyType) } - if !strings.Contains(keyID, ";vault=") { + nameStart := strings.Index(keyID, "name=") + 5 + vaultIndex := strings.Index(keyID, ";vault=") + if vaultIndex == -1 { return fmt.Errorf("azurekms %s must contain ';vault=' parameter", keyType) } + if strings.TrimSpace(keyID[nameStart:vaultIndex]) == "" { + return fmt.Errorf("key name cannot be empty for %s", keyType) + } + if strings.TrimSpace(keyID[vaultIndex+7:]) == "" { + return fmt.Errorf("vault name cannot be empty for %s", keyType) + } return nil } if err := validateAzureKeyID(config.RootKeyID, "RootKeyID"); err != nil { diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index 3def594b..9ee42709 100644 --- a/pkg/certmaker/certmaker_test.go +++ b/pkg/certmaker/certmaker_test.go @@ -21,46 +21,32 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" - "crypto/x509/pkix" "encoding/pem" "fmt" - "math/big" "os" "path/filepath" + "strings" "testing" - "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.step.sm/crypto/kms/apiv1" - "go.step.sm/crypto/x509util" ) -// mockKMSProvider is a mock implementation of apiv1.KeyManager type mockKMSProvider struct { - name string - keys map[string]*ecdsa.PrivateKey - signers map[string]crypto.Signer + keys map[string]crypto.Signer } func newMockKMSProvider() *mockKMSProvider { - m := &mockKMSProvider{ - name: "test", - keys: make(map[string]*ecdsa.PrivateKey), - signers: make(map[string]crypto.Signer), + keys := make(map[string]crypto.Signer) + for _, id := range []string{"root-key", "intermediate-key", "leaf-key"} { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + keys[id] = priv } - - // Pre-create test keys - rootKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - intermediateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - leafKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - - m.keys["root-key"] = rootKey - m.keys["intermediate-key"] = intermediateKey - m.keys["leaf-key"] = leafKey - - return m + return &mockKMSProvider{keys: keys} } func (m *mockKMSProvider) CreateKey(*apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { @@ -72,7 +58,6 @@ func (m *mockKMSProvider) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.S if !ok { return nil, fmt.Errorf("key not found: %s", req.SigningKey) } - m.signers[req.SigningKey] = key return key, nil } @@ -145,7 +130,7 @@ func TestValidateKMSConfig(t *testing.T) { wantError: "awskms LeafKeyID must start with 'arn:aws:kms:' or 'alias/'", }, { - name: "GCP KMS invalid root key ID", + name: "GCP_KMS_invalid_root_key_ID", config: KMSConfig{ Type: "gcpkms", RootKeyID: "invalid-key-id", @@ -153,16 +138,16 @@ func TestValidateKMSConfig(t *testing.T) { wantError: "gcpkms RootKeyID must start with 'projects/'", }, { - name: "GCP KMS invalid intermediate key ID", + name: "GCP_KMS_invalid_intermediate_key_ID", config: KMSConfig{ Type: "gcpkms", - RootKeyID: "projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key", + RootKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", IntermediateKeyID: "invalid-key-id", }, wantError: "gcpkms IntermediateKeyID must start with 'projects/'", }, { - name: "GCP KMS invalid leaf key ID", + name: "GCP_KMS_invalid_leaf_key_ID", config: KMSConfig{ Type: "gcpkms", LeafKeyID: "invalid-key-id", @@ -173,31 +158,21 @@ func TestValidateKMSConfig(t *testing.T) { name: "GCP KMS missing required parts", config: KMSConfig{ Type: "gcpkms", - RootKeyID: "projects/my-project", + RootKeyID: "projects/test-project", }, - wantError: "invalid gcpkms key format", + wantError: "gcpkms RootKeyID must contain '/locations/'", }, { - name: "Azure KMS missing tenant ID", + name: "Azure_KMS_missing_tenant_ID", config: KMSConfig{ Type: "azurekms", RootKeyID: "azurekms:name=my-key;vault=my-vault", + Options: map[string]string{}, }, wantError: "tenant-id is required for Azure KMS", }, { - name: "Azure KMS invalid root key ID prefix", - config: KMSConfig{ - Type: "azurekms", - RootKeyID: "invalid-key-id", - Options: map[string]string{ - "tenant-id": "tenant-id", - }, - }, - wantError: "azurekms RootKeyID must start with 'azurekms:name='", - }, - { - name: "Azure KMS missing vault parameter", + name: "Azure_KMS_missing_vault_parameter", config: KMSConfig{ Type: "azurekms", RootKeyID: "azurekms:name=my-key", @@ -207,29 +182,6 @@ func TestValidateKMSConfig(t *testing.T) { }, wantError: "azurekms RootKeyID must contain ';vault=' parameter", }, - { - name: "Azure KMS invalid intermediate key ID", - config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=my-key;vault=my-vault", - IntermediateKeyID: "invalid-key-id", - Options: map[string]string{ - "tenant-id": "tenant-id", - }, - }, - wantError: "azurekms IntermediateKeyID must start with 'azurekms:name='", - }, - { - name: "Azure KMS invalid leaf key ID", - config: KMSConfig{ - Type: "azurekms", - LeafKeyID: "invalid-key-id", - Options: map[string]string{ - "tenant-id": "tenant-id", - }, - }, - wantError: "azurekms LeafKeyID must start with 'azurekms:name='", - }, { name: "unsupported KMS type", config: KMSConfig{ @@ -238,445 +190,826 @@ func TestValidateKMSConfig(t *testing.T) { }, wantError: "unsupported KMS type", }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidateKMSConfig(tt.config) - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - }) - } - - // Test valid configurations - validConfigs := []KMSConfig{ - { - Type: "awskms", - Region: "us-west-2", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab", - }, { - Type: "awskms", - Region: "us-west-2", - LeafKeyID: "alias/my-key", - }, - { - Type: "gcpkms", - RootKeyID: "projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key", + name: "aws_kms_invalid_arn_format", + config: KMSConfig{ + Type: "awskms", + Region: "us-west-2", + RootKeyID: "arn:aws:kms:us-west-2:invalid", + }, + wantError: "invalid AWS KMS ARN format for RootKeyID", }, { - Type: "azurekms", - RootKeyID: "azurekms:name=my-key;vault=my-vault", - Options: map[string]string{ - "tenant-id": "tenant-id", + name: "aws_kms_region_mismatch", + config: KMSConfig{ + Type: "awskms", + Region: "us-west-2", + RootKeyID: "arn:aws:kms:us-east-1:123456789012:key/test-key", }, + wantError: "region in ARN (us-east-1) does not match configured region (us-west-2)", }, - } - - for _, config := range validConfigs { - t.Run(fmt.Sprintf("valid %s config", config.Type), func(t *testing.T) { - err := ValidateKMSConfig(config) - require.NoError(t, err) - }) - } -} - -func TestValidateTemplatePath(t *testing.T) { - // Create a temporary directory for test files - tmpDir, err := os.MkdirTemp("", "template-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - // Create a valid JSON file - validPath := filepath.Join(tmpDir, "valid.json") - err = os.WriteFile(validPath, []byte("{}"), 0600) - require.NoError(t, err) - - // Create a non-JSON file - nonJSONPath := filepath.Join(tmpDir, "invalid.txt") - err = os.WriteFile(nonJSONPath, []byte("{}"), 0600) - require.NoError(t, err) - - tests := []struct { - name string - path string - wantError string - }{ { - name: "valid JSON file", - path: validPath, + name: "aws_kms_empty_alias", + config: KMSConfig{ + Type: "awskms", + Region: "us-west-2", + RootKeyID: "alias/", + }, + wantError: "alias name cannot be empty for RootKeyID", }, { - name: "non-existent file", - path: "/nonexistent/template.json", - wantError: "template not found", + name: "azure_kms_empty_key_name", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=;vault=test-vault", + Options: map[string]string{ + "tenant-id": "test-tenant", + }, + }, + wantError: "key name cannot be empty for RootKeyID", }, { - name: "wrong extension", - path: nonJSONPath, - wantError: "template file must have .json extension", + name: "azure_kms_empty_vault_name", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=", + Options: map[string]string{ + "tenant-id": "test-tenant", + }, + }, + wantError: "vault name cannot be empty for RootKeyID", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateTemplatePath(tt.path) + err := ValidateKMSConfig(tt.config) if tt.wantError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - } else { - require.NoError(t, err) + if err == nil { + t.Error("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("Expected error containing %q, got %q", tt.wantError, err.Error()) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) } }) } } -func TestWriteCertificateToFile(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-write-test-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) - - // Create a key pair - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Create a certificate template - template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: "Test Cert", - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - BasicConstraintsValid: true, - IsCA: true, - MaxPathLen: 0, - MaxPathLenZero: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - SignatureAlgorithm: x509.ECDSAWithSHA256, - PublicKeyAlgorithm: x509.ECDSA, - } - - // Create a self-signed certificate - cert, err := x509util.CreateCertificate(template, template, key.Public(), key) - require.NoError(t, err) - - t.Run("success", func(t *testing.T) { - testFile := filepath.Join(tmpDir, "test-cert.pem") - err = WriteCertificateToFile(cert, testFile) - require.NoError(t, err) - - content, err := os.ReadFile(testFile) - require.NoError(t, err) - - block, _ := pem.Decode(content) - require.NotNil(t, block) - assert.Equal(t, "CERTIFICATE", block.Type) - - parsedCert, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - assert.Equal(t, "Test Cert", parsedCert.Subject.CommonName) - }) - - t.Run("error writing to file", func(t *testing.T) { - // Try to write to a non-existent directory - testFile := filepath.Join(tmpDir, "nonexistent", "test-cert.pem") - err = WriteCertificateToFile(cert, testFile) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to create file") - }) -} - func TestCreateCertificates(t *testing.T) { - rootContent := `{ - "subject": { - "country": ["US"], - "organization": ["Sigstore"], - "organizationalUnit": ["Timestamp Authority Root CA"], - "commonName": "https://tsa.com" - }, - "issuer": { - "commonName": "https://tsa.com" - }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "basicConstraints": { - "isCA": true, - "maxPathLen": 1 - }, - "keyUsage": [ - "certSign", - "crlSign" - ] - }` - - leafContent := `{ - "subject": { - "country": ["US"], - "organization": ["Sigstore"], - "organizationalUnit": ["Timestamp Authority"], - "commonName": "https://tsa.com" - }, - "issuer": { - "commonName": "https://tsa.com" - }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "basicConstraints": { - "isCA": false - }, - "keyUsage": [ - "digitalSignature" - ], - "extKeyUsage": [ - "timeStamping" - ] - }` - t.Run("TSA without intermediate", func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-tsa-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + leafTemplate := filepath.Join(tmpDir, "leaf.json") + err = os.WriteFile(leafTemplate, []byte(`{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "keyUsage": ["digitalSignature"], + "basicConstraints": { + "isCA": false + }, + "extKeyUsage": ["timeStamping"], + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write leaf template: %v", err) + } - km := newMockKMSProvider() config := KMSConfig{ - Type: "mockkms", + Type: "test", RootKeyID: "root-key", LeafKeyID: "leaf-key", - Options: make(map[string]string), } - rootTmplPath := filepath.Join(tmpDir, "root-template.json") - leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") - rootCertPath := filepath.Join(tmpDir, "root.pem") - leafCertPath := filepath.Join(tmpDir, "leaf.pem") - - err = os.WriteFile(rootTmplPath, []byte(rootContent), 0600) - require.NoError(t, err) - - err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) - require.NoError(t, err) + outDir := filepath.Join(tmpDir, "out") + err = os.MkdirAll(outDir, 0755) + if err != nil { + t.Fatalf("Failed to create output directory: %v", err) + } - err = CreateCertificates(km, config, - rootTmplPath, leafTmplPath, - rootCertPath, leafCertPath, + kms := newMockKMSProvider() + err = CreateCertificates(kms, config, + rootTemplate, leafTemplate, + filepath.Join(outDir, "root.crt"), filepath.Join(outDir, "leaf.crt"), "", "", "") - require.NoError(t, err) + if err != nil { + t.Fatalf("Failed to create certificates: %v", err) + } - // Verify certificates were created - _, err = os.Stat(rootCertPath) - require.NoError(t, err) - _, err = os.Stat(leafCertPath) - require.NoError(t, err) + verifyGeneratedCertificates(t, outDir) }) t.Run("TSA with intermediate", func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-tsa-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) - intermediateContent := `{ + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ "subject": { - "country": ["US"], - "organization": ["Sigstore"], - "organizationalUnit": ["TSA Intermediate CA"], - "commonName": "https://tsa.com" + "commonName": "Test Root CA" }, "issuer": { - "commonName": "https://tsa.com" + "commonName": "Test Root CA" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 }, "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") + err = os.WriteFile(intermediateTemplate, []byte(`{ + "subject": { + "commonName": "Test Intermediate CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, "maxPathLen": 0 }, - "keyUsage": [ - "certSign", - "crlSign" - ] - }` + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write intermediate template: %v", err) + } + + leafTemplate := filepath.Join(tmpDir, "leaf.json") + err = os.WriteFile(leafTemplate, []byte(`{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Intermediate CA" + }, + "keyUsage": ["digitalSignature"], + "basicConstraints": { + "isCA": false + }, + "extKeyUsage": ["timeStamping"], + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write leaf template: %v", err) + } - km := newMockKMSProvider() config := KMSConfig{ - Type: "mockkms", + Type: "test", RootKeyID: "root-key", IntermediateKeyID: "intermediate-key", LeafKeyID: "leaf-key", - Options: make(map[string]string), } - rootTmplPath := filepath.Join(tmpDir, "root-template.json") - leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") - intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") - rootCertPath := filepath.Join(tmpDir, "root.pem") - intermediateCertPath := filepath.Join(tmpDir, "intermediate.pem") - leafCertPath := filepath.Join(tmpDir, "leaf.pem") - - err = os.WriteFile(rootTmplPath, []byte(rootContent), 0600) - require.NoError(t, err) - err = os.WriteFile(intermediateTmplPath, []byte(intermediateContent), 0600) - require.NoError(t, err) - err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) - require.NoError(t, err) - - err = CreateCertificates(km, config, - rootTmplPath, leafTmplPath, - rootCertPath, leafCertPath, - "intermediate-key", intermediateTmplPath, intermediateCertPath) - require.NoError(t, err) - - // Verify certificates were created - _, err = os.Stat(rootCertPath) - require.NoError(t, err) - _, err = os.Stat(intermediateCertPath) - require.NoError(t, err) - _, err = os.Stat(leafCertPath) - require.NoError(t, err) + outDir := filepath.Join(tmpDir, "out") + err = os.MkdirAll(outDir, 0755) + if err != nil { + t.Fatalf("Failed to create output directory: %v", err) + } + + kms := newMockKMSProvider() + err = CreateCertificates(kms, config, + rootTemplate, leafTemplate, + filepath.Join(outDir, "root.crt"), filepath.Join(outDir, "leaf.crt"), + "intermediate-key", intermediateTemplate, filepath.Join(outDir, "intermediate.crt")) + if err != nil { + t.Fatalf("Failed to create certificates: %v", err) + } + + verifyGeneratedCertificates(t, outDir) }) t.Run("invalid root template path", func(t *testing.T) { - km := newMockKMSProvider() + kms := newMockKMSProvider() config := KMSConfig{ - Type: "mockkms", + Type: "test", RootKeyID: "root-key", LeafKeyID: "leaf-key", - Options: make(map[string]string), } - err := CreateCertificates(km, config, + err := CreateCertificates(kms, config, "/nonexistent/root.json", "/nonexistent/leaf.json", - "/nonexistent/root.pem", "/nonexistent/leaf.pem", + "/nonexistent/root.crt", "/nonexistent/leaf.crt", "", "", "") - require.Error(t, err) - assert.Contains(t, err.Error(), "error reading template file") + if err == nil { + t.Error("Expected error but got none") + } + if !strings.Contains(err.Error(), "error reading template file") { + t.Errorf("Expected error containing 'error reading template file', got %v", err) + } }) +} - t.Run("invalid intermediate template path", func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-tsa-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) +func verifyGeneratedCertificates(t *testing.T, outDir string) { + files := []string{ + "root.crt", + "leaf.crt", + } - km := newMockKMSProvider() - config := KMSConfig{ - Type: "mockkms", - RootKeyID: "root-key", - IntermediateKeyID: "intermediate-key", - LeafKeyID: "leaf-key", - Options: make(map[string]string), + intermediateExists := false + intermediatePath := filepath.Join(outDir, "intermediate.crt") + if _, err := os.Stat(intermediatePath); err == nil { + intermediateExists = true + files = append(files, "intermediate.crt") + } + + for _, f := range files { + path := filepath.Join(outDir, f) + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Errorf("Expected file %s does not exist", f) } + } - rootTmplPath := filepath.Join(tmpDir, "root-template.json") - leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") - rootCertPath := filepath.Join(tmpDir, "root.pem") - leafCertPath := filepath.Join(tmpDir, "leaf.pem") - - err = os.WriteFile(rootTmplPath, []byte(rootContent), 0600) - require.NoError(t, err) - err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) - require.NoError(t, err) - - err = CreateCertificates(km, config, - rootTmplPath, leafTmplPath, - rootCertPath, leafCertPath, - "intermediate-key", "/nonexistent/intermediate.json", "/nonexistent/intermediate.pem") - require.Error(t, err) - assert.Contains(t, err.Error(), "error reading template file") - }) + rootCertPath := filepath.Join(outDir, "root.crt") + rootCertBytes, err := os.ReadFile(rootCertPath) + if err != nil { + t.Fatalf("Failed to read root certificate: %v", err) + } - t.Run("invalid leaf template path", func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-tsa-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + rootBlock, _ := pem.Decode(rootCertBytes) + if rootBlock == nil { + t.Fatal("Failed to decode root certificate PEM") + } - km := newMockKMSProvider() - config := KMSConfig{ - Type: "mockkms", - RootKeyID: "root-key", - LeafKeyID: "leaf-key", - Options: make(map[string]string), + rootCert, err := x509.ParseCertificate(rootBlock.Bytes) + if err != nil { + t.Fatalf("Failed to parse root certificate: %v", err) + } + + if rootCert.Subject.CommonName != "Test Root CA" { + t.Errorf("Expected root CN %q, got %q", "Test Root CA", rootCert.Subject.CommonName) + } + + if !rootCert.IsCA { + t.Error("Expected root certificate to be CA") + } + + var intermediateCert *x509.Certificate + if intermediateExists { + intermediateCertBytes, err := os.ReadFile(intermediatePath) + if err != nil { + t.Fatalf("Failed to read intermediate certificate: %v", err) + } + + intermediateBlock, _ := pem.Decode(intermediateCertBytes) + if intermediateBlock == nil { + t.Fatal("Failed to decode intermediate certificate PEM") } - rootTmplPath := filepath.Join(tmpDir, "root-template.json") - rootCertPath := filepath.Join(tmpDir, "root.pem") - leafCertPath := filepath.Join(tmpDir, "leaf.pem") + intermediateCert, err = x509.ParseCertificate(intermediateBlock.Bytes) + if err != nil { + t.Fatalf("Failed to parse intermediate certificate: %v", err) + } - err = os.WriteFile(rootTmplPath, []byte(rootContent), 0600) - require.NoError(t, err) + if intermediateCert.Subject.CommonName != "Test Intermediate CA" { + t.Errorf("Expected intermediate CN %q, got %q", "Test Intermediate CA", intermediateCert.Subject.CommonName) + } - err = CreateCertificates(km, config, - rootTmplPath, "/nonexistent/leaf.json", - rootCertPath, leafCertPath, - "", "", "") - require.Error(t, err) - assert.Contains(t, err.Error(), "error reading template file") - }) + if !intermediateCert.IsCA { + t.Error("Expected intermediate certificate to be CA") + } + } + + leafCertPath := filepath.Join(outDir, "leaf.crt") + leafCertBytes, err := os.ReadFile(leafCertPath) + if err != nil { + t.Fatalf("Failed to read leaf certificate: %v", err) + } + + leafBlock, _ := pem.Decode(leafCertBytes) + if leafBlock == nil { + t.Fatal("Failed to decode leaf certificate PEM") + } + + leafCert, err := x509.ParseCertificate(leafBlock.Bytes) + if err != nil { + t.Fatalf("Failed to parse leaf certificate: %v", err) + } + + if leafCert.Subject.CommonName != "Test TSA" { + t.Errorf("Expected leaf CN %q, got %q", "Test TSA", leafCert.Subject.CommonName) + } + + if leafCert.IsCA { + t.Error("Expected leaf certificate not to be CA") + } + + roots := x509.NewCertPool() + roots.AddCert(rootCert) + + intermediates := x509.NewCertPool() + if intermediateCert != nil { + intermediates.AddCert(intermediateCert) + } + + opts := x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{ + x509.ExtKeyUsageTimeStamping, + }, + } + + if _, err := leafCert.Verify(opts); err != nil { + t.Errorf("Failed to verify certificate chain: %v", err) + } } func TestInitKMS(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "kms-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate private key: %v", err) + } + + privKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privKey), + }) + + credsFile := filepath.Join(tmpDir, "test-credentials.json") + err = os.WriteFile(credsFile, []byte(fmt.Sprintf(`{ + "type": "service_account", + "project_id": "test-project", + "private_key_id": "test-key-id", + "private_key": %q, + "client_email": "test@test-project.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test@test-project.iam.gserviceaccount.com" + }`, string(privKeyPEM))), 0600) + if err != nil { + t.Fatalf("Failed to write credentials file: %v", err) + } + ctx := context.Background() tests := []struct { name string config KMSConfig - wantError string + wantError bool + errMsg string }{ { - name: "AWS KMS", + name: "valid AWS KMS config", config: KMSConfig{ Type: "awskms", Region: "us-west-2", - RootKeyID: "test-key", - Options: map[string]string{ - "access-key-id": "test-access-key", - "secret-access-key": "test-secret-key", - }, + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", + Options: map[string]string{}, }, + wantError: false, }, { - name: "GCP KMS", + name: "valid GCP KMS config", config: KMSConfig{ Type: "gcpkms", - RootKeyID: "test-key", + RootKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", + LeafKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/leaf-key/cryptoKeyVersions/1", Options: map[string]string{ - "credentials-file": "/path/to/credentials.json", + "credentials-file": credsFile, }, }, + wantError: false, }, { - name: "Azure KMS", + name: "valid Azure KMS config", config: KMSConfig{ Type: "azurekms", - RootKeyID: "test-key", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + LeafKeyID: "azurekms:name=leaf-key;vault=test-vault", Options: map[string]string{ - "tenant-id": "test-tenant", - "client-id": "test-client", - "client-secret": "test-secret", + "tenant-id": "test-tenant", }, }, + wantError: false, }, { - name: "unsupported KMS type", + name: "AWS KMS missing region", config: KMSConfig{ - Type: "unsupportedkms", - RootKeyID: "test-key", + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", }, - wantError: "unsupported KMS type", + wantError: true, + errMsg: "region is required for AWS KMS", + }, + { + name: "GCP KMS invalid credentials", + config: KMSConfig{ + Type: "gcpkms", + RootKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", + Options: map[string]string{ + "credentials-file": "/nonexistent/credentials.json", + }, + }, + wantError: true, + errMsg: "credentials file not found", + }, + { + name: "Azure KMS missing tenant ID", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + Options: map[string]string{}, + }, + wantError: true, + errMsg: "tenant-id is required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { km, err := InitKMS(ctx, tt.config) + if tt.wantError { + if err == nil { + t.Error("expected error but got nil") + } else if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("error %q should contain %q", err.Error(), tt.errMsg) + } + if km != nil { + t.Error("expected nil KMS but got non-nil") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if km == nil { + t.Error("expected non-nil KMS but got nil") + } + } + }) + } +} + +func TestValidateTemplatePath(t *testing.T) { + tests := []struct { + name string + path string + setup func() string + wantError string + }{ + { + name: "nonexistent file", + path: "/nonexistent/template.json", + wantError: "template not found", + }, + { + name: "wrong extension", + path: "template.txt", + setup: func() string { + f, err := os.CreateTemp("", "template.txt") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return f.Name() + }, + wantError: "must have .json extension", + }, + { + name: "valid JSON template", + path: "valid.json", + setup: func() string { + f, err := os.CreateTemp("", "template*.json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = os.WriteFile(f.Name(), []byte(`{"key": "value"}`), 0600) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return f.Name() + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := tt.path + if tt.setup != nil { + path = tt.setup() + defer os.Remove(path) + } + + err := ValidateTemplatePath(path) if tt.wantError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - assert.Nil(t, km) + if err == nil { + t.Errorf("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("error %q should contain %q", err.Error(), tt.wantError) + } } else { - // Since we can't actually connect to KMS providers in tests, - // we expect an error but not the "unsupported KMS type" error - require.Error(t, err) - assert.NotContains(t, err.Error(), "unsupported KMS type") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + } + }) + } +} + +func TestCreateCertificatesErrors(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T) (string, KMSConfig, apiv1.KeyManager) + wantError string + }{ + { + name: "error creating intermediate signer", + setup: func(t *testing.T) (string, KMSConfig, apiv1.KeyManager) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ + "subject": {"commonName": "Test Root CA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": {"isCA": true, "maxPathLen": 1}, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") + err = os.WriteFile(intermediateTemplate, []byte(`{ + "subject": {"commonName": "Test Intermediate CA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": {"isCA": true, "maxPathLen": 0}, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write intermediate template: %v", err) + } + + leafTemplate := filepath.Join(tmpDir, "leaf.json") + err = os.WriteFile(leafTemplate, []byte(`{ + "subject": {"commonName": "Test TSA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["digitalSignature"], + "basicConstraints": {"isCA": false}, + "extKeyUsage": ["timeStamping"], + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write leaf template: %v", err) + } + + config := KMSConfig{ + Type: "test", + RootKeyID: "root-key", + IntermediateKeyID: "nonexistent-key", + LeafKeyID: "leaf-key", + } + + return tmpDir, config, newMockKMSProvider() + }, + wantError: "error creating intermediate signer", + }, + { + name: "error creating leaf signer", + setup: func(t *testing.T) (string, KMSConfig, apiv1.KeyManager) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ + "subject": {"commonName": "Test Root CA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": {"isCA": true, "maxPathLen": 1}, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + leafTemplate := filepath.Join(tmpDir, "leaf.json") + err = os.WriteFile(leafTemplate, []byte(`{ + "subject": {"commonName": "Test TSA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["digitalSignature"], + "basicConstraints": {"isCA": false}, + "extKeyUsage": ["timeStamping"], + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write leaf template: %v", err) + } + + config := KMSConfig{ + Type: "test", + RootKeyID: "root-key", + LeafKeyID: "nonexistent-key", + } + + return tmpDir, config, newMockKMSProvider() + }, + wantError: "error creating leaf signer", + }, + { + name: "error creating root certificate", + setup: func(t *testing.T) (string, KMSConfig, apiv1.KeyManager) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ + "subject": {}, + "issuer": {}, + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + leafTemplate := filepath.Join(tmpDir, "leaf.json") + err = os.WriteFile(leafTemplate, []byte(`{ + "subject": {"commonName": "Test TSA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["digitalSignature"], + "basicConstraints": {"isCA": false}, + "extKeyUsage": ["timeStamping"], + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write leaf template: %v", err) + } + + config := KMSConfig{ + Type: "test", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + } + + return tmpDir, config, newMockKMSProvider() + }, + wantError: "error parsing root template: template validation error: notBefore time must be specified", + }, + { + name: "error writing certificates", + setup: func(t *testing.T) (string, KMSConfig, apiv1.KeyManager) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ + "subject": {"commonName": "Test Root CA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": {"isCA": true, "maxPathLen": 1}, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + outDir := filepath.Join(tmpDir, "out") + err = os.MkdirAll(outDir, 0444) + if err != nil { + t.Fatalf("Failed to create output directory: %v", err) + } + + config := KMSConfig{ + Type: "test", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + } + + return tmpDir, config, newMockKMSProvider() + }, + wantError: "error writing root certificate", + }, + { + name: "error with nonexistent signer", + setup: func(t *testing.T) (string, KMSConfig, apiv1.KeyManager) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + rootTemplate := filepath.Join(tmpDir, "root.json") + err = os.WriteFile(rootTemplate, []byte(`{ + "subject": {"commonName": "Test Root CA"}, + "issuer": {"commonName": "Test Root CA"}, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": {"isCA": true, "maxPathLen": 1}, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }`), 0600) + if err != nil { + t.Fatalf("Failed to write root template: %v", err) + } + + config := KMSConfig{ + Type: "test", + RootKeyID: "nonexistent-key", + LeafKeyID: "leaf-key", + } + + return tmpDir, config, newMockKMSProvider() + }, + wantError: "error creating root signer", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, config, kms := tt.setup(t) + defer os.RemoveAll(tmpDir) + + outDir := filepath.Join(tmpDir, "out") + err := os.MkdirAll(outDir, 0755) + if err != nil { + t.Fatalf("Failed to create output directory: %v", err) + } + + var intermediateKeyID string + if tt.name == "error creating intermediate signer" { + intermediateKeyID = "nonexistent-key" + } + + err = CreateCertificates(kms, config, + filepath.Join(tmpDir, "root.json"), + filepath.Join(tmpDir, "leaf.json"), + filepath.Join(outDir, "root.crt"), + filepath.Join(outDir, "leaf.crt"), + intermediateKeyID, + filepath.Join(tmpDir, "intermediate.json"), + filepath.Join(outDir, "intermediate.crt")) + + if tt.wantError != "" { + if err == nil { + t.Error("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("Expected error containing %q, got %q", tt.wantError, err.Error()) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) } }) } diff --git a/pkg/certmaker/template_test.go b/pkg/certmaker/template_test.go index 77442fe8..876b184b 100644 --- a/pkg/certmaker/template_test.go +++ b/pkg/certmaker/template_test.go @@ -21,18 +21,17 @@ import ( "encoding/base64" "os" "path/filepath" + "strings" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestParseTemplate(t *testing.T) { tmpDir, err := os.MkdirTemp("", "cert-template-*") - require.NoError(t, err) + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } defer os.RemoveAll(tmpDir) - // Create a parent certificate for template data parent := &x509.Certificate{ Subject: pkix.Name{ CommonName: "Parent CA", @@ -107,26 +106,35 @@ func TestParseTemplate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create temp file for template tmpFile := filepath.Join(tmpDir, "template.json") err := os.WriteFile(tmpFile, []byte(tt.content), 0600) - require.NoError(t, err) + if err != nil { + t.Fatalf("Failed to write template file: %v", err) + } cert, err := ParseTemplate(tmpFile, tt.parent) if tt.wantError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - assert.Nil(t, cert) + if err == nil { + t.Error("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("Expected error containing %q, got %q", tt.wantError, err.Error()) + } + if cert != nil { + t.Error("Expected nil certificate when error occurs") + } } else { - require.NoError(t, err) - require.NotNil(t, cert) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if cert == nil { + t.Error("Expected non-nil certificate") + } } }) } } func TestValidateTemplate(t *testing.T) { - // Create a parent certificate for testing parent := &x509.Certificate{ Subject: pkix.Name{ CommonName: "Parent CA", @@ -231,104 +239,19 @@ func TestValidateTemplate(t *testing.T) { }, wantError: "invalid notBefore time format", }, - { - name: "invalid extension OID", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - }{ - CommonName: "Test TSA", - }, - Issuer: struct { - CommonName string `json:"commonName"` - }{ - CommonName: "Test TSA", - }, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, - Extensions: []struct { - ID string `json:"id"` - Critical bool `json:"critical"` - Value string `json:"value"` - }{ - { - ID: "invalid.oid", - Critical: true, - Value: "AQID", - }, - }, - }, - wantError: "invalid OID component in extension", - }, - { - name: "empty extension ID", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - }{ - CommonName: "Test TSA", - }, - Issuer: struct { - CommonName string `json:"commonName"` - }{ - CommonName: "Test TSA", - }, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, - Extensions: []struct { - ID string `json:"id"` - Critical bool `json:"critical"` - Value string `json:"value"` - }{ - { - ID: "", - Critical: true, - Value: "AQID", - }, - }, - }, - wantError: "extension ID cannot be empty", - }, - { - name: "notBefore after notAfter", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - }{ - CommonName: "Test TSA", - }, - Issuer: struct { - CommonName string `json:"commonName"` - }{ - CommonName: "Test TSA", - }, - NotBefore: "2025-01-01T00:00:00Z", // Later than NotAfter - NotAfter: "2024-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, - }, - wantError: "NotBefore time must be before NotAfter time", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateTemplate(tt.tmpl, tt.parent) if tt.wantError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - } else { - require.NoError(t, err) + if err == nil { + t.Error("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("Expected error containing %q, got %q", tt.wantError, err.Error()) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) } }) }