diff --git a/cmd/certificate_maker/certificate_maker.go b/cmd/certificate_maker/certificate_maker.go index 9f419ae0..39d37256 100644 --- a/cmd/certificate_maker/certificate_maker.go +++ b/cmd/certificate_maker/certificate_maker.go @@ -174,14 +174,18 @@ func runCreate(_ *cobra.Command, _ []string) error { } // Validate template paths - if err := certmaker.ValidateTemplatePath(viper.GetString("root-template")); err != nil { + rootTemplate := viper.GetString("root-template") + leafTemplate := viper.GetString("leaf-template") + intermediateTemplate := viper.GetString("intermediate-template") + + if err := certmaker.ValidateTemplatePath(rootTemplate); err != nil { return fmt.Errorf("root template error: %w", err) } - if err := certmaker.ValidateTemplatePath(viper.GetString("leaf-template")); err != nil { + if err := certmaker.ValidateTemplatePath(leafTemplate); err != nil { return fmt.Errorf("leaf template error: %w", err) } if viper.GetString("intermediate-key-id") != "" { - if err := certmaker.ValidateTemplatePath(viper.GetString("intermediate-template")); err != nil { + if err := certmaker.ValidateTemplatePath(intermediateTemplate); err != nil { return fmt.Errorf("intermediate template error: %w", err) } } diff --git a/cmd/certificate_maker/certificate_maker_test.go b/cmd/certificate_maker/certificate_maker_test.go index ed4ad954..b0f75fb4 100644 --- a/cmd/certificate_maker/certificate_maker_test.go +++ b/cmd/certificate_maker/certificate_maker_test.go @@ -166,8 +166,7 @@ func TestRunCreate(t *testing.T) { "issuer": { "commonName": "Test TSA Root CA" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z", + "certLifetime": "8760h", "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, @@ -179,8 +178,10 @@ func TestRunCreate(t *testing.T) { "subject": { "commonName": "Test TSA" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z", + "issuer": { + "commonName": "Test TSA Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], "extKeyUsage": ["TimeStamping"], "basicConstraints": { @@ -324,7 +325,7 @@ func TestRunCreate(t *testing.T) { viper.Reset() cmd := &cobra.Command{} for i := 0; i < len(tt.args); i += 2 { - flag := tt.args[i][2:] // Remove "--" prefix + flag := tt.args[i][2:] value := tt.args[i+1] viper.Set(flag, value) } @@ -352,8 +353,7 @@ func TestCreateCommand(t *testing.T) { "issuer": { "commonName": "Test TSA Root CA" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z", + "certLifetime": "8760h", "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, @@ -365,8 +365,10 @@ func TestCreateCommand(t *testing.T) { "subject": { "commonName": "Test TSA" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z", + "issuer": { + "commonName": "Test TSA Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], "extKeyUsage": ["TimeStamping"], "basicConstraints": { @@ -432,7 +434,7 @@ func TestCreateCommand(t *testing.T) { viper.Reset() cmd := &cobra.Command{} for i := 0; i < len(tt.args); i += 2 { - flag := tt.args[i][2:] // Remove "--" prefix + flag := tt.args[i][2:] value := tt.args[i+1] viper.Set(flag, value) } @@ -479,3 +481,239 @@ func TestRootCommand(t *testing.T) { }) } } + +func TestEnvironmentVariableHandling(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + args []string + wantError bool + errorString string + }{ + { + name: "AWS KMS from environment", + envVars: map[string]string{ + "KMS_TYPE": "awskms", + "AWS_REGION": "us-west-2", + "KMS_ROOT_KEY_ID": "alias/test-root", + "KMS_LEAF_KEY_ID": "alias/test-leaf", + }, + args: []string{"create"}, + wantError: true, + }, + { + name: "GCP KMS from environment", + envVars: map[string]string{ + "KMS_TYPE": "gcpkms", + "GCP_CREDENTIALS_FILE": "/path/to/creds.json", + "KMS_ROOT_KEY_ID": "projects/test/locations/global/keyRings/test/cryptoKeys/root/cryptoKeyVersions/1", + "KMS_LEAF_KEY_ID": "projects/test/locations/global/keyRings/test/cryptoKeys/leaf/cryptoKeyVersions/1", + }, + args: []string{"create"}, + wantError: true, + errorString: "credentials file not found", + }, + { + name: "Azure KMS from environment", + envVars: map[string]string{ + "KMS_TYPE": "azurekms", + "AZURE_TENANT_ID": "test-tenant", + "KMS_ROOT_KEY_ID": "azurekms:name=test-key;vault=test-vault", + "KMS_LEAF_KEY_ID": "azurekms:name=test-key;vault=test-vault", + }, + args: []string{"create"}, + wantError: true, + }, + { + name: "HashiVault KMS from environment", + envVars: map[string]string{ + "KMS_TYPE": "hashivault", + "VAULT_TOKEN": "test-token", + "VAULT_ADDR": "http://vault:8200", + "KMS_ROOT_KEY_ID": "transit/keys/test-root", + "KMS_LEAF_KEY_ID": "transit/keys/test-leaf", + }, + args: []string{"create"}, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + oldEnv := map[string]string{} + for k := range tt.envVars { + if v, ok := os.LookupEnv(k); ok { + oldEnv[k] = v + } + } + + for k, v := range tt.envVars { + os.Setenv(k, v) + } + + viper.Reset() + + viper.BindEnv("kms-type", "KMS_TYPE") + viper.BindEnv("aws-region", "AWS_REGION") + viper.BindEnv("azure-tenant-id", "AZURE_TENANT_ID") + viper.BindEnv("gcp-credentials-file", "GCP_CREDENTIALS_FILE") + viper.BindEnv("vault-token", "VAULT_TOKEN") + viper.BindEnv("vault-address", "VAULT_ADDR") + viper.BindEnv("root-key-id", "KMS_ROOT_KEY_ID") + viper.BindEnv("leaf-key-id", "KMS_LEAF_KEY_ID") + + defer func() { + for k := range tt.envVars { + if v, ok := oldEnv[k]; ok { + os.Setenv(k, v) + } else { + os.Unsetenv(k) + } + } + }() + + cmd := &cobra.Command{ + Use: "test", + RunE: runCreate, + } + + cmd.Flags().String("kms-type", "", "KMS type") + cmd.Flags().String("aws-region", "", "AWS region") + cmd.Flags().String("azure-tenant-id", "", "Azure tenant ID") + cmd.Flags().String("gcp-credentials-file", "", "GCP credentials file") + cmd.Flags().String("vault-token", "", "HashiVault token") + cmd.Flags().String("vault-address", "", "HashiVault address") + cmd.Flags().String("root-key-id", "", "Root key ID") + cmd.Flags().String("leaf-key-id", "", "Leaf key ID") + cmd.Flags().String("root-template", "templates/root-template.json", "Root template") + cmd.Flags().String("leaf-template", "templates/leaf-template.json", "Leaf template") + + viper.BindPFlag("kms-type", cmd.Flags().Lookup("kms-type")) + viper.BindPFlag("aws-region", cmd.Flags().Lookup("aws-region")) + viper.BindPFlag("azure-tenant-id", cmd.Flags().Lookup("azure-tenant-id")) + viper.BindPFlag("gcp-credentials-file", cmd.Flags().Lookup("gcp-credentials-file")) + viper.BindPFlag("vault-token", cmd.Flags().Lookup("vault-token")) + viper.BindPFlag("vault-address", cmd.Flags().Lookup("vault-address")) + viper.BindPFlag("root-key-id", cmd.Flags().Lookup("root-key-id")) + viper.BindPFlag("leaf-key-id", cmd.Flags().Lookup("leaf-key-id")) + viper.BindPFlag("root-template", cmd.Flags().Lookup("root-template")) + viper.BindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template")) + + cmd.SetArgs(tt.args) + err := cmd.Execute() + + if tt.wantError { + require.Error(t, err) + if tt.errorString != "" { + assert.Contains(t, err.Error(), tt.errorString) + } + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.envVars["KMS_TYPE"], viper.GetString("kms-type")) + assert.Equal(t, tt.envVars["KMS_ROOT_KEY_ID"], viper.GetString("root-key-id")) + assert.Equal(t, tt.envVars["KMS_LEAF_KEY_ID"], viper.GetString("leaf-key-id")) + }) + } +} + +func TestKMSProviderConfigurationValidation(t *testing.T) { + tests := []struct { + name string + args []string + wantError bool + errorString string + }{ + { + name: "AWS KMS invalid key format", + args: []string{ + "create", + "--kms-type", "awskms", + "--aws-region", "us-west-2", + "--root-key-id", "invalid-format", + "--leaf-key-id", "invalid-format", + }, + wantError: true, + errorString: "must start with 'arn:aws:kms:' or 'alias/'", + }, + { + name: "GCP KMS missing key version", + args: []string{ + "create", + "--kms-type", "gcpkms", + "--root-key-id", "projects/test/locations/global/keyRings/test/cryptoKeys/test", + "--leaf-key-id", "projects/test/locations/global/keyRings/test/cryptoKeys/test", + }, + wantError: true, + errorString: "must contain '/cryptoKeyVersions/'", + }, + { + name: "Azure KMS invalid key format", + args: []string{ + "create", + "--kms-type", "azurekms", + "--azure-tenant-id", "test-tenant", + "--root-key-id", "invalid-format", + "--leaf-key-id", "invalid-format", + }, + wantError: true, + errorString: "must start with 'azurekms:name='", + }, + { + name: "HashiVault KMS invalid key path", + args: []string{ + "create", + "--kms-type", "hashivault", + "--vault-token", "test-token", + "--vault-address", "http://vault:8200", + "--root-key-id", "invalid/path", + "--leaf-key-id", "invalid/path", + }, + wantError: true, + errorString: "must be in format: transit/keys/keyname", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + cmd := &cobra.Command{ + Use: "test", + RunE: runCreate, + } + + cmd.Flags().String("kms-type", "", "KMS type") + cmd.Flags().String("aws-region", "", "AWS region") + cmd.Flags().String("azure-tenant-id", "", "Azure tenant ID") + cmd.Flags().String("vault-token", "", "HashiVault token") + cmd.Flags().String("vault-address", "", "HashiVault address") + cmd.Flags().String("root-key-id", "", "Root key ID") + cmd.Flags().String("leaf-key-id", "", "Leaf key ID") + cmd.Flags().String("root-template", "templates/root-template.json", "Root template") + cmd.Flags().String("leaf-template", "templates/leaf-template.json", "Leaf template") + + viper.BindPFlag("kms-type", cmd.Flags().Lookup("kms-type")) + viper.BindPFlag("aws-region", cmd.Flags().Lookup("aws-region")) + viper.BindPFlag("azure-tenant-id", cmd.Flags().Lookup("azure-tenant-id")) + viper.BindPFlag("vault-token", cmd.Flags().Lookup("vault-token")) + viper.BindPFlag("vault-address", cmd.Flags().Lookup("vault-address")) + viper.BindPFlag("root-key-id", cmd.Flags().Lookup("root-key-id")) + viper.BindPFlag("leaf-key-id", cmd.Flags().Lookup("leaf-key-id")) + viper.BindPFlag("root-template", cmd.Flags().Lookup("root-template")) + viper.BindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template")) + + cmd.SetArgs(tt.args) + err := cmd.Execute() + + if tt.wantError { + require.Error(t, err) + if tt.errorString != "" { + assert.Contains(t, err.Error(), tt.errorString) + } + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go index fa7c95eb..98616002 100644 --- a/pkg/certmaker/certmaker.go +++ b/pkg/certmaker/certmaker.go @@ -432,10 +432,14 @@ func ValidateKMSConfig(config KMSConfig) error { if keyID == "" { return nil } - if strings.Contains(keyID, "/") { - return fmt.Errorf("hashivault %s must be a simple key name without path separators", keyType) + parts := strings.Split(keyID, "/") + if len(parts) < 3 { + return fmt.Errorf("hashivault %s must be in format: transit/keys/keyname", keyType) } - if strings.TrimSpace(keyID) == "" { + if parts[0] != "transit" || parts[1] != "keys" { + return fmt.Errorf("hashivault %s must start with 'transit/keys/'", keyType) + } + if parts[2] == "" { return fmt.Errorf("key name cannot be empty for %s", keyType) } return nil diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index c7611cb2..c94ea90c 100644 --- a/pkg/certmaker/certmaker_test.go +++ b/pkg/certmaker/certmaker_test.go @@ -23,6 +23,7 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/json" "fmt" "io" "math/big" @@ -37,12 +38,19 @@ import ( "github.com/stretchr/testify/require" ) +var ( + originalInitKMS = InitKMS + testKey *ecdsa.PrivateKey +) + // mockSignerVerifier implements signature.SignerVerifier for testing type mockSignerVerifier struct { - key crypto.PrivateKey - err error - publicKeyFunc func() (crypto.PublicKey, error) - signMessageFunc func(message io.Reader) ([]byte, error) + key crypto.PrivateKey + err error + publicKeyFunc func() (crypto.PublicKey, error) + signMessageFunc func(message io.Reader) ([]byte, error) + cryptoSignerFunc func(context.Context, func(error)) (crypto.Signer, crypto.SignerOpts, error) + signFunc func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) } func (m *mockSignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) { @@ -74,18 +82,15 @@ func (m *mockSignerVerifier) SignMessage(message io.Reader, _ ...signature.SignO return nil, fmt.Errorf("no key set") } - // Read the message msgBytes, err := io.ReadAll(message) if err != nil { return nil, fmt.Errorf("error reading message: %w", err) } - // Hash the message with SHA256 h := crypto.SHA256.New() h.Write(msgBytes) digest := h.Sum(nil) - // Use the private key to sign switch k := m.key.(type) { case *ecdsa.PrivateKey: return ecdsa.SignASN1(rand.Reader, k, digest) @@ -118,8 +123,12 @@ func (m *mockSignerVerifier) Status() error { return nil } -// CryptoSigner implements the CryptoSignerVerifier interface func (m *mockSignerVerifier) CryptoSigner(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { + if m.cryptoSignerFunc != nil { + return m.cryptoSignerFunc(context.Background(), func(err error) { + m.err = err + }) + } if m.err != nil { return nil, nil, m.err } @@ -134,11 +143,33 @@ func (m *mockSignerVerifier) CryptoSigner(_ context.Context, _ func(error)) (cry } } -// At package level -var ( - // Store the original function - originalInitKMS = InitKMS // Changed from initKMS to InitKMS -) +func init() { + var err error + testKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(fmt.Sprintf("failed to generate test key: %v", err)) + } + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.Options != nil && config.Options["mock-leaf-error"] == "true" { + return nil, fmt.Errorf("leaf signing error") + } + return &mockSignerVerifier{ + key: testKey, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, testKey, digest) + }, + }, nil + } +} + +func TestMain(m *testing.M) { + code := m.Run() + + InitKMS = originalInitKMS + + os.Exit(code) +} func TestValidateKMSConfig(t *testing.T) { tests := []struct { @@ -200,9 +231,9 @@ func TestValidateKMSConfig(t *testing.T) { name: "valid_HashiVault_KMS_config", config: KMSConfig{ Type: "hashivault", - RootKeyID: "test-key", - IntermediateKeyID: "test-intermediate-key", - LeafKeyID: "test-leaf-key", + RootKeyID: "transit/keys/test-key", + IntermediateKeyID: "transit/keys/test-intermediate-key", + LeafKeyID: "transit/keys/test-leaf-key", Options: map[string]string{"vault-token": "test-token", "vault-address": "http://localhost:8200"}, }, }, @@ -276,17 +307,60 @@ func TestValidateTemplatePath(t *testing.T) { } func TestCreateCertificates(t *testing.T) { - // Save original and restore after test - defer func() { InitKMS = originalInitKMS }() // Changed from initKMS to InitKMS + tmpDir, err := os.MkdirTemp("", "cert-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) - // Create a mock key key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - mockSV := &mockSignerVerifier{ + mockSigner := &mockSignerVerifier{ key: key, publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil + return &key.PublicKey, nil }, signMessageFunc: func(message io.Reader) ([]byte, error) { msgBytes, err := io.ReadAll(message) @@ -298,689 +372,383 @@ func TestCreateCertificates(t *testing.T) { digest := h.Sum(nil) return ecdsa.SignASN1(rand.Reader, key, digest) }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, } - // Replace initKMS with mock version - InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { - return mockSV, nil + config := KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, } tests := []struct { name string - setup func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) + rootTmpl string + leafTmpl string + rootPath string + leafPath string + signer signature.SignerVerifier + config KMSConfig wantError string }{ { - name: "successful_certificate_creation", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - 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) - require.NoError(t, err) - - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - - intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{ - "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - IntermediateKeyID: "alias/intermediate-key", - LeafKeyID: "alias/leaf-key", - Options: map[string]string{"region": "us-west-2"}, - }, mockSV - }, + name: "successful_certificate_creation", + rootTmpl: rootTmplPath, + leafTmpl: leafTmplPath, + rootPath: rootCertPath, + leafPath: leafCertPath, + signer: mockSigner, + config: config, }, { - name: "invalid_template_path", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, mockSV - }, - wantError: "error parsing root template", + name: "invalid_template_path", + rootTmpl: "nonexistent.json", + leafTmpl: leafTmplPath, + rootPath: rootCertPath, + leafPath: leafCertPath, + signer: mockSigner, + config: config, + wantError: "error parsing root template: error reading template file", }, { - name: "invalid_root_template_content", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - - rootTemplate := filepath.Join(tmpDir, "root.json") - err = os.WriteFile(rootTemplate, []byte(`{invalid json`), 0600) - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, mockSV + name: "invalid_root_template_content", + rootTmpl: rootTmplPath, + leafTmpl: leafTmplPath, + rootPath: rootCertPath, + leafPath: leafCertPath, + signer: &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("unsupported key type") + }, }, - wantError: "error parsing root template", + config: config, + wantError: "error getting root public key: unsupported key type", }, { - name: "signer_error", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, 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) - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - errorMockSV := &mockSignerVerifier{ - key: key, - err: fmt.Errorf("signer error"), - } - - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, errorMockSV + name: "signer_error", + rootTmpl: rootTmplPath, + leafTmpl: leafTmplPath, + rootPath: rootCertPath, + leafPath: leafCertPath, + signer: &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("error getting root public key") + }, }, + config: config, wantError: "error getting root public key", }, { - name: "invalid_intermediate_template", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) + name: "invalid_leaf_template", + rootTmpl: rootTmplPath, + leafTmpl: leafTmplPath, + rootPath: rootCertPath, + leafPath: leafCertPath, + signer: mockSigner, + config: KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "invalid-key", + Options: map[string]string{"aws-region": "us-west-2", "mock-leaf-error": "true"}, + }, + wantError: "leaf signing error", + }, + } - 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) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CreateCertificates(tt.signer, tt.config, tt.rootTmpl, tt.leafTmpl, tt.rootPath, tt.leafPath, "", "", "") + if tt.wantError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + } else { require.NoError(t, err) + } + }) + } +} - intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{invalid json`), 0600) - require.NoError(t, err) +func TestCreateCertificatesWithoutIntermediate(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - IntermediateKeyID: "alias/intermediate-key", - LeafKeyID: "alias/leaf-key", - }, mockSV - }, - wantError: "error parsing intermediate template", + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" }, - { - name: "invalid_leaf_template", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - 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) - require.NoError(t, err) + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{invalid json`), 0600) - require.NoError(t, err) + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, mockSV - }, - wantError: "error parsing leaf template", + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil }, - { - name: "root_cert_write_error", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } - 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) - require.NoError(t, err) + config := KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, + } - // Create a directory where a file should be to cause a write error - rootCertDir := filepath.Join(tmpDir, "out", "root.crt") - require.NoError(t, os.MkdirAll(rootCertDir, 0755)) + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.NoError(t, err) +} - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, mockSV - }, - wantError: "error writing root certificate", - }, - { - name: "successful_certificate_creation_without_intermediate", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - 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) - require.NoError(t, err) +func TestCreateCertificatesLeafErrors(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - Options: map[string]string{"region": "us-west-2"}, - }, mockSV - }, + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" }, - { - name: "successful_certificate_creation_with_intermediate", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) + leafTemplate := `invalid json` - // Create root template - 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) - require.NoError(t, err) + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) - // Create intermediate template - intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{ - "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) - // Create leaf template - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - IntermediateKeyID: "alias/intermediate-key", - LeafKeyID: "alias/leaf-key", - Options: map[string]string{"region": "us-west-2"}, - }, mockSV - }, + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil }, - { - name: "intermediate_cert_creation_error", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - // Create root template - 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) - require.NoError(t, err) - - // Create invalid intermediate template - intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{ - "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["digitalSignature"], - "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "alias/root-key", - IntermediateKeyID: "alias/intermediate-key", - LeafKeyID: "alias/leaf-key", - }, &mockSignerVerifier{key: key} - }, - wantError: "template validation error: CA certificate must have certSign key usage", + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir, config, sv := tt.setup(t) - defer os.RemoveAll(tmpDir) - - var intermediateKeyID, intermediateTemplate, intermediateCert string - if strings.Contains(tt.name, "intermediate") { - intermediateKeyID = config.IntermediateKeyID - intermediateTemplate = filepath.Join(tmpDir, "intermediate.json") - intermediateCert = filepath.Join(tmpDir, "out", "intermediate.crt") - } - - err := CreateCertificates(sv, config, - filepath.Join(tmpDir, "root.json"), - filepath.Join(tmpDir, "leaf.json"), - filepath.Join(tmpDir, "out", "root.crt"), - filepath.Join(tmpDir, "out", "leaf.crt"), - intermediateKeyID, - intermediateTemplate, - intermediateCert) - - if tt.wantError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - } else { - require.NoError(t, err) - // Verify certificates were created - rootCertPath := filepath.Join(tmpDir, "out", "root.crt") - leafCertPath := filepath.Join(tmpDir, "out", "leaf.crt") - require.FileExists(t, rootCertPath) - require.FileExists(t, leafCertPath) - } - }) + config := KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, } -} -func TestInitKMS(t *testing.T) { - // Create a temporary credentials file for GCP tests - tmpDir := t.TempDir() - gcpCredsFile := filepath.Join(tmpDir, "gcp-credentials.json") - err := os.WriteFile(gcpCredsFile, []byte(`{ - "type": "service_account", - "project_id": "test-project", - "private_key_id": "test-key-id", - "private_key": "test-private-key", - "client_email": "test@test-project.iam.gserviceaccount.com", - "client_id": "test-client-id", - "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" - }`), 0600) - require.NoError(t, err) + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), "error parsing leaf template: leaf template error: invalid JSON after template execution: invalid character 'i' looking for beginning of value") +} +func TestCreateCertificatesWithErrors(t *testing.T) { tests := []struct { - name string - config KMSConfig - wantErr string + name string + rootTmpl string + leafTmpl string + signer signature.SignerVerifier + config KMSConfig + wantError string }{ { - name: "aws_kms_valid_config", - config: KMSConfig{ - Type: "awskms", - RootKeyID: "alias/test-root-key", - IntermediateKeyID: "alias/test-intermediate-key", - LeafKeyID: "alias/test-leaf-key", - Options: map[string]string{"aws-region": "us-west-2"}, - }, - }, - { - name: "azure_kms_valid_config", + name: "root_cert_creation_error", + rootTmpl: `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "invalid", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + leafTmpl: `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extensions": [ + { + "id": "2.5.29.37", + "critical": true, + "value": "MCQwIgYDVR0lBBswGQYIKwYBBQUHAwgGDSsGAQQBgjcUAgICAf8=" + } + ] + }`, + signer: &mockSignerVerifier{}, config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=test-root-key;vault=test-vault", - IntermediateKeyID: "azurekms:name=test-intermediate-key;vault=test-vault", - LeafKeyID: "azurekms:name=test-leaf-key;vault=test-vault", - Options: map[string]string{"azure-tenant-id": "test-tenant"}, + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "alias/test-leaf-key", }, + wantError: "error parsing root template: template validation error: invalid certLifetime format: time: invalid duration \"invalid\"", }, { - name: "gcp_kms_valid_config", - config: KMSConfig{ - Type: "gcpkms", - RootKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", - IntermediateKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-intermediate-key/cryptoKeyVersions/1", - LeafKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-leaf-key/cryptoKeyVersions/1", - Options: map[string]string{"gcp-credentials-file": gcpCredsFile}, + name: "root_cert_sign_error", + rootTmpl: `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + leafTmpl: `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }`, + signer: &mockSignerVerifier{ + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("signing error") + }, }, - }, - { - name: "hashivault_kms_valid_config", config: KMSConfig{ - Type: "hashivault", - RootKeyID: "test-key", - IntermediateKeyID: "test-intermediate-key", - LeafKeyID: "test-leaf-key", - Options: map[string]string{"vault-token": "test-token", "vault-address": "http://localhost:8200"}, + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, }, + wantError: "error getting root public key: signing error", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - _, err := InitKMS(ctx, tt.config) - if tt.wantErr != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.wantErr) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestCreateCertificatesWithoutIntermediate(t *testing.T) { - // Create temp dir for test files - tmpDir := t.TempDir() - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - // Create test templates - rootTemplate := `{ - "subject": { - "commonName": "Test Root CA" - }, - "issuer": { - "commonName": "Test Root CA" - }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "keyUsage": ["certSign"], - "basicConstraints": { - "isCA": true - } - }` - - leafTemplate := `{ - "subject": { - "commonName": "Test Leaf" - }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "keyUsage": ["digitalSignature"] - }` - - rootTemplatePath := filepath.Join(tmpDir, "root.json") - leafTemplatePath := filepath.Join(tmpDir, "leaf.json") - require.NoError(t, os.WriteFile(rootTemplatePath, []byte(rootTemplate), 0644)) - require.NoError(t, os.WriteFile(leafTemplatePath, []byte(leafTemplate), 0644)) - - rootCertPath := filepath.Join(outDir, "root.crt") - leafCertPath := filepath.Join(outDir, "leaf.crt") - - // Create mock signer - mockSigner := &mockSignerVerifier{ - err: fmt.Errorf("error getting root public key: getting public key: operation error KMS: GetPublicKey"), - } - - config := KMSConfig{ - Type: "awskms", - RootKeyID: "alias/test-root-key", - IntermediateKeyID: "alias/test-intermediate-key", - LeafKeyID: "alias/test-leaf-key", - Options: map[string]string{"region": "us-west-2"}, - } - - err := CreateCertificates(mockSigner, config, rootTemplatePath, leafTemplatePath, rootCertPath, leafCertPath, "", "", "") - require.Error(t, err) - require.Contains(t, err.Error(), "error getting root public key: getting public key: operation error KMS: GetPublicKey") -} - -func TestCreateCertificatesLeafErrors(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, 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) - require.NoError(t, err) - - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{invalid json`), 0600) - require.NoError(t, err) + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) + err = os.WriteFile(rootTmplPath, []byte(tt.rootTmpl), 0644) + require.NoError(t, err) - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(tt.leafTmpl), 0644) + require.NoError(t, err) - config := KMSConfig{ - Type: "awskms", - RootKeyID: "awskms:///arn:aws:kms:us-west-2:123456789012:key/root-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - Options: map[string]string{"region": "us-west-2"}, + err = CreateCertificates(tt.signer, tt.config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantError) + }) } - - err = CreateCertificates(&mockSignerVerifier{key: key}, config, - rootTemplate, - leafTemplate, - filepath.Join(outDir, "root.crt"), - filepath.Join(outDir, "leaf.crt"), - "", // No intermediate key ID - "", // No intermediate template - "") // No intermediate cert path - - require.Error(t, err) - assert.Contains(t, err.Error(), "error parsing leaf template") } -func TestCreateCertificatesWithErrors(t *testing.T) { +func TestWriteCertificateToFileWithErrors(t *testing.T) { tests := []struct { name string - setup func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) + setup func(t *testing.T) (*x509.Certificate, string) wantError string }{ { - name: "root_cert_creation_error", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, 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": "invalid-time", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "awskms:///arn:aws:kms:us-west-2:123456789012:key/root-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - Options: map[string]string{"region": "us-west-2"}, - }, &mockSignerVerifier{key: key} - }, - wantError: "error parsing root template: template validation error: invalid notBefore time format", - }, - { - name: "root_cert_sign_error", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, 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) - require.NoError(t, err) - - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) - - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "awskms:///arn:aws:kms:us-west-2:123456789012:key/root-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - Options: map[string]string{"region": "us-west-2"}, - }, &mockSignerVerifier{key: key, err: fmt.Errorf("signing error")} - }, - wantError: "error getting root public key: signing error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir, config, sv := tt.setup(t) - defer os.RemoveAll(tmpDir) - - err := CreateCertificates(sv, config, - filepath.Join(tmpDir, "root.json"), - filepath.Join(tmpDir, "leaf.json"), - filepath.Join(tmpDir, "out", "root.crt"), - filepath.Join(tmpDir, "out", "leaf.crt"), - "", - "", - "") - - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - }) - } -} - -func TestWriteCertificateToFileWithErrors(t *testing.T) { - tests := []struct { - name string - setup func(t *testing.T) (*x509.Certificate, string) - wantError string - }{ - { - name: "file_write_error", - setup: func(t *testing.T) (*x509.Certificate, string) { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + name: "file_write_error", + setup: func(t *testing.T) (*x509.Certificate, string) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) template := &x509.Certificate{ @@ -1001,7 +769,6 @@ func TestWriteCertificateToFileWithErrors(t *testing.T) { parsedCert, err := x509.ParseCertificate(cert) require.NoError(t, err) - // Create a read-only directory to cause a write error tmpDir, err := os.MkdirTemp("", "cert-test-*") require.NoError(t, err) require.NoError(t, os.Chmod(tmpDir, 0500)) @@ -1060,359 +827,391 @@ func TestValidateTemplateWithInvalidExtKeyUsage(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(tmpDir) - templateFile := filepath.Join(tmpDir, "template.json") - err = os.WriteFile(templateFile, []byte(`{ - "subject": {"commonName": "Test TSA"}, - "issuer": {"commonName": "Test TSA"}, - "keyUsage": ["digitalSignature"], - "basicConstraints": {"isCA": true}, - "extensions": [ - { - "id": "2.5.29.37", - "critical": true, - "value": "MCQwIgYDVR0lBBswGQYIKwYBBQUHAwgGDSsGAQQBgjcUAgICAf8=" - } - ], - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") - parent := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Parent CA", + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - } - - template, err := ParseTemplate(templateFile, parent) - require.Error(t, err) - assert.Contains(t, err.Error(), "CA certificate must have certSign key usage") - assert.Nil(t, template) -} - -func TestCreateCertificatesWithInvalidIntermediateKey(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - // Create valid root template - 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) - require.NoError(t, err) + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - // Create valid leaf template - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + "extKeyUsage": ["serverAuth"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) require.NoError(t, err) - // Create valid intermediate template - intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{ - "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) require.NoError(t, err) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - // Test with invalid intermediate key ID format - err = CreateCertificates( - &mockSignerVerifier{key: key}, - KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - IntermediateKeyID: "invalid-intermediate-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - Options: map[string]string{"aws-region": "us-west-2"}, - }, - rootTemplate, - leafTemplate, - filepath.Join(tmpDir, "root.crt"), - filepath.Join(tmpDir, "leaf.crt"), - "invalid-intermediate-key", - intermediateTemplate, - filepath.Join(tmpDir, "intermediate.crt"), - ) + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "alias/test-leaf-key", + } + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") require.Error(t, err) - assert.Contains(t, err.Error(), "error initializing intermediate KMS: invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") + require.Contains(t, err.Error(), "CA certificate must have certSign key usage") } -func TestCreateCertificatesWithInvalidLeafKey(t *testing.T) { +func TestCreateCertificatesWithInvalidIntermediateKey(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.IntermediateKeyID != "" && !strings.HasPrefix(config.IntermediateKeyID, "arn:aws:kms:") && !strings.HasPrefix(config.IntermediateKeyID, "alias/") { + return nil, fmt.Errorf("invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + return &mockSignerVerifier{ + key: key, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + }, nil + } + tmpDir, err := os.MkdirTemp("", "cert-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) - // Create valid root template - rootTemplate := filepath.Join(tmpDir, "root.json") - err = os.WriteFile(rootTemplate, []byte(`{ - "subject": {"commonName": "Test Root CA"}, - "issuer": {"commonName": "Test Root CA"}, + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.crt") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 1}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - // Create valid leaf template - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, + intermediateTemplate := `{ + "subject": { + "commonName": "Test Intermediate CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Intermediate CA" + }, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Test with invalid leaf key ID format - err = CreateCertificates( - &mockSignerVerifier{key: key}, - KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - LeafKeyID: "invalid-leaf-key", - }, - rootTemplate, - leafTemplate, - filepath.Join(tmpDir, "root.crt"), - filepath.Join(tmpDir, "leaf.crt"), - "", - "", - "", - ) - - require.Error(t, err) - assert.Contains(t, err.Error(), "error initializing leaf KMS") -} + "basicConstraints": { + "isCA": false + } + }` -func TestCreateCertificatesWithInvalidRootCert(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) require.NoError(t, err) - defer os.RemoveAll(tmpDir) - // Create invalid root template (missing required fields) - rootTemplate := filepath.Join(tmpDir, "root.json") - err = os.WriteFile(rootTemplate, []byte(`{ - "subject": {}, - "issuer": {}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + err = os.WriteFile(intermediateTmplPath, []byte(intermediateTemplate), 0644) require.NoError(t, err) - // Create valid leaf template - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) require.NoError(t, err) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - err = CreateCertificates( - &mockSignerVerifier{key: key}, - KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - }, - rootTemplate, - leafTemplate, - filepath.Join(tmpDir, "root.crt"), - filepath.Join(tmpDir, "leaf.crt"), - "", - "", - "", - ) + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + config := KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-root-key", + IntermediateKeyID: "invalid-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, config.IntermediateKeyID, intermediateTmplPath, intermediateCertPath) require.Error(t, err) - assert.Contains(t, err.Error(), "subject.commonName cannot be empty") + require.Contains(t, err.Error(), "invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") } -func TestCreateCertificatesWithInvalidCertPath(t *testing.T) { +func TestCreateCertificatesWithInvalidLeafKey(t *testing.T) { + oldInitKMS := InitKMS + defer func() { InitKMS = oldInitKMS }() + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.LeafKeyID == "invalid-key" { + return nil, fmt.Errorf("invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + return &mockSignerVerifier{ + key: key, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + }, nil + } + tmpDir, err := os.MkdirTemp("", "cert-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) - // Create valid templates - rootTemplate := filepath.Join(tmpDir, "root.json") - err = os.WriteFile(rootTemplate, []byte(`{ - "subject": {"commonName": "Test Root CA"}, - "issuer": {"commonName": "Test Root CA"}, + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 1}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // Create a directory where a file should be and make it read-only - invalidPath := filepath.Join(tmpDir, "invalid") - err = os.MkdirAll(invalidPath, 0444) // Changed permissions to read-only - require.NoError(t, err) - - err = CreateCertificates( - &mockSignerVerifier{key: key}, - KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - }, - rootTemplate, - leafTemplate, - filepath.Join(invalidPath, "root.crt"), - filepath.Join(invalidPath, "leaf.crt"), - "", - "", - "", - ) - - require.Error(t, err) - assert.Contains(t, err.Error(), "error writing root certificate") -} + "basicConstraints": { + "isCA": false + } + }` -func TestWriteCertificateToFileWithPEMError(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) require.NoError(t, err) - defer os.RemoveAll(tmpDir) - // Create a directory where a file should be to cause a write error - certPath := filepath.Join(tmpDir, "cert.pem") - err = os.MkdirAll(certPath, 0755) // Create a directory instead of a file + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) require.NoError(t, err) - // Create a valid certificate key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: "Test", + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour), } - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) - require.NoError(t, err) - - cert, err := x509.ParseCertificate(certBytes) - require.NoError(t, err) + config := KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "invalid-key", + Options: map[string]string{"aws-region": "us-west-2"}, + } - // Try to write to a path that is a directory, which should fail - err = WriteCertificateToFile(cert, certPath) + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") require.Error(t, err) - assert.Contains(t, err.Error(), "failed to create file") + require.Contains(t, err.Error(), "invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") } func TestCreateCertificatesWithInvalidRootKey(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) + tests := []struct { + name string + setup func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) + wantError string + }{ + { + name: "signing_error", + setup: func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) - // Create valid templates - 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) - require.NoError(t, err) + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - // Test with signing error - err = CreateCertificates( - &mockSignerVerifier{key: nil, err: fmt.Errorf("signing error")}, - KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - }, - rootTemplate, - leafTemplate, - filepath.Join(tmpDir, "root.crt"), - filepath.Join(tmpDir, "leaf.crt"), - "", - "", - "", - ) + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` - require.Error(t, err) - assert.Contains(t, err.Error(), "error getting root public key: signing error") -} + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) -func TestCreateCertificatesWithInvalidLeafTemplate(t *testing.T) { - tests := []struct { - name string - setup func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) - wantError string - }{ + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("signing error") + }, + signMessageFunc: func(_ io.Reader) ([]byte, error) { + return nil, fmt.Errorf("signing error") + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + return rootTmplPath, rootCertPath, leafTmplPath, leafCertPath, config, mockSigner + }, + wantError: "error getting root public key: signing error", + }, { - name: "missing_timeStamping_extKeyUsage", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { + name: "invalid_root_key", + setup: func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) { tmpDir := t.TempDir() - // Create test templates + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") + rootTemplate := `{ "subject": { "commonName": "Test Root CA" @@ -1420,109 +1219,177 @@ func TestCreateCertificatesWithInvalidLeafTemplate(t *testing.T) { "issuer": { "commonName": "Test Root CA" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "keyUsage": ["certSign"], + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], "basicConstraints": { - "isCA": true + "isCA": true, + "maxPathLen": 1 } }` leafTemplate := `{ "subject": { - "commonName": "Test Leaf" + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "keyUsage": ["digitalSignature"] + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } }` - rootTemplatePath := filepath.Join(tmpDir, "root.json") - leafTemplatePath := filepath.Join(tmpDir, "leaf.json") - require.NoError(t, os.WriteFile(rootTemplatePath, []byte(rootTemplate), 0644)) - require.NoError(t, os.WriteFile(leafTemplatePath, []byte(leafTemplate), 0644)) + err := os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - IntermediateKeyID: "arn:aws:kms:us-west-2:123456789012:key/intermediate-key", - LeafKeyID: "invalid-key", - Options: map[string]string{"aws-region": "us-west-2"}, - }, &mockSignerVerifier{key: key} + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") + }, + signMessageFunc: func(_ io.Reader) ([]byte, error) { + return nil, fmt.Errorf("invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'") + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "invalid-key", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + return rootTmplPath, rootCertPath, leafTmplPath, leafCertPath, config, mockSigner }, - wantError: "error initializing leaf KMS: invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'", + wantError: "error getting root public key: invalid KMS configuration: awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tmpDir, config, signer := tt.setup(t) - rootCertPath := filepath.Join(tmpDir, "root.crt") - err := CreateCertificates(signer, config, filepath.Join(tmpDir, "root.json"), filepath.Join(tmpDir, "leaf.json"), rootCertPath, "", "", "", "") + rootTmpl, rootCert, leafTmpl, leafCert, config, signer := tt.setup(t) + err := CreateCertificates(signer, config, rootTmpl, leafTmpl, rootCert, leafCert, "", "", "") require.Error(t, err) require.Contains(t, err.Error(), tt.wantError) }) } } -func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { +func TestCreateCertificatesWithInvalidLeafTemplate(t *testing.T) { tests := []struct { name string - setup func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) + setup func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) wantError string }{ { - name: "intermediate_template_parse_error", - setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { - tmpDir, err := os.MkdirTemp("", "cert-test-*") - require.NoError(t, err) + name: "missing_timeStamping_extKeyUsage", + setup: func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) { + tmpDir := t.TempDir() - outDir := filepath.Join(tmpDir, "out") - require.NoError(t, os.MkdirAll(outDir, 0755)) + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") - rootTemplate := filepath.Join(tmpDir, "root.json") - err = os.WriteFile(rootTemplate, []byte(`{ - "subject": {"commonName": "Test Root CA"}, - "issuer": {"commonName": "Test Root CA"}, + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 1}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) + "basicConstraints": { + "isCA": false + } + }` - intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{invalid json`), 0600) + err := os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) require.NoError(t, err) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - return tmpDir, KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/root-key", - IntermediateKeyID: "arn:aws:kms:us-west-2:123456789012:key/intermediate-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - }, &mockSignerVerifier{key: key} + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-root-key", + LeafKeyID: "invalid-key", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + return rootTmplPath, rootCertPath, leafTmplPath, leafCertPath, config, mockSigner }, - wantError: "error parsing intermediate template", + wantError: "error parsing leaf template: template validation error: timestamp authority certificate must have TimeStamping extended key usage", }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootTmpl, rootCert, leafTmpl, leafCert, config, signer := tt.setup(t) + err := CreateCertificates(signer, config, rootTmpl, leafTmpl, rootCert, leafCert, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) + wantError string + }{ { - name: "intermediate_cert_write_error", + name: "intermediate_template_parse_error", setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { tmpDir, err := os.MkdirTemp("", "cert-test-*") require.NoError(t, err) @@ -1536,50 +1403,53 @@ func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { "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" + "certLifetime": "8760h" }`), 0600) require.NoError(t, err) leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["TimeStamping"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + err = os.WriteFile(leafTemplate, []byte(`{invalid json`), 0600) require.NoError(t, err) intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") - err = os.WriteFile(intermediateTemplate, []byte(`{ - "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + err = os.WriteFile(intermediateTemplate, []byte(`{invalid json`), 0600) require.NoError(t, err) - // Create a directory where the intermediate cert file should be - intermediateCertDir := filepath.Join(outDir, "intermediate.crt") - require.NoError(t, os.MkdirAll(intermediateCertDir, 0755)) - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + return tmpDir, KMSConfig{ Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/root-key", - IntermediateKeyID: "arn:aws:kms:us-west-2:123456789012:key/intermediate-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - }, &mockSignerVerifier{key: key} + RootKeyID: "alias/test-root-key", + IntermediateKeyID: "alias/test-leaf-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + }, mockSigner }, - wantError: "error initializing intermediate KMS", + wantError: "error parsing intermediate template: leaf template error: invalid JSON after template execution: invalid character 'i' looking for beginning of object key string", }, { - name: "leaf_cert_with_intermediate_error", + name: "invalid_intermediate_template_validation", setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { tmpDir, err := os.MkdirTemp("", "cert-test-*") require.NoError(t, err) @@ -1593,8 +1463,7 @@ func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { "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" + "certLifetime": "8760h" }`), 0600) require.NoError(t, err) @@ -1604,35 +1473,54 @@ func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { "keyUsage": ["digitalSignature"], "extKeyUsage": ["TimeStamping"], "basicConstraints": {"isCA": false}, - "notBefore": "invalid-time", - "notAfter": "2025-01-01T00:00:00Z" + "certLifetime": "8760h" }`), 0600) require.NoError(t, err) intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") err = os.WriteFile(intermediateTemplate, []byte(`{ "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["certSign", "crlSign"], + "keyUsage": ["digitalSignature"], "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" + "certLifetime": "43800h" }`), 0600) require.NoError(t, err) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + return tmpDir, KMSConfig{ Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/root-key", - IntermediateKeyID: "arn:aws:kms:us-west-2:123456789012:key/intermediate-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - }, &mockSignerVerifier{key: key} + RootKeyID: "alias/test-root-key", + IntermediateKeyID: "alias/test-leaf-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + }, mockSigner }, - wantError: "error initializing intermediate KMS", + wantError: "error parsing intermediate template: template validation error: CA certificate must have certSign key usage", }, { - name: "invalid_intermediate_template_validation", + name: "invalid_intermediate_lifetime", setup: func(t *testing.T) (string, KMSConfig, signature.SignerVerifier) { tmpDir, err := os.MkdirTemp("", "cert-test-*") require.NoError(t, err) @@ -1646,8 +1534,7 @@ func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { "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" + "certLifetime": "8760h" }`), 0600) require.NoError(t, err) @@ -1657,41 +1544,60 @@ func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { "keyUsage": ["digitalSignature"], "extKeyUsage": ["TimeStamping"], "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" + "certLifetime": "8760h" }`), 0600) require.NoError(t, err) intermediateTemplate := filepath.Join(tmpDir, "intermediate.json") err = os.WriteFile(intermediateTemplate, []byte(`{ "subject": {"commonName": "Test Intermediate CA"}, - "keyUsage": ["digitalSignature"], + "keyUsage": ["certSign", "crlSign"], "basicConstraints": {"isCA": true, "maxPathLen": 0}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" + "certLifetime": "invalid-time" }`), 0600) require.NoError(t, err) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + return tmpDir, KMSConfig{ Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/root-key", - IntermediateKeyID: "arn:aws:kms:us-west-2:123456789012:key/intermediate-key", - LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/leaf-key", - }, &mockSignerVerifier{key: key} + RootKeyID: "alias/test-root-key", + IntermediateKeyID: "alias/test-leaf-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + }, mockSigner }, - wantError: "error parsing intermediate template: template validation error: CA certificate must have certSign key usage", + wantError: "error parsing intermediate template: template validation error: invalid certLifetime format: time: invalid duration \"invalid-time\"", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tmpDir, config, sv := tt.setup(t) + tmpDir, config, signer := tt.setup(t) defer os.RemoveAll(tmpDir) - err := CreateCertificates(sv, config, + err := CreateCertificates(signer, config, filepath.Join(tmpDir, "root.json"), filepath.Join(tmpDir, "leaf.json"), filepath.Join(tmpDir, "out", "root.crt"), @@ -1701,7 +1607,977 @@ func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { filepath.Join(tmpDir, "out", "intermediate.crt")) require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) + require.Contains(t, err.Error(), tt.wantError) }) } } + +func TestValidateKMSConfig_AdditionalCases(t *testing.T) { + tests := []struct { + name string + config KMSConfig + wantErr string + }{ + { + name: "invalid_aws_kms_key_format", + config: KMSConfig{ + Type: "awskms", + RootKeyID: "invalid-format", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, + }, + wantErr: "awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'", + }, + { + name: "invalid_gcp_kms_key_format", + config: KMSConfig{ + Type: "gcpkms", + RootKeyID: "invalid-format", + LeafKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", + }, + wantErr: "gcpkms RootKeyID must start with 'projects/'", + }, + { + name: "invalid_azure_kms_key_format", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "invalid-format", + LeafKeyID: "azurekms:name=test-key;vault=test-vault", + Options: map[string]string{"azure-tenant-id": "test-tenant"}, + }, + wantErr: "azurekms RootKeyID must start with 'azurekms:name='", + }, + { + name: "missing_azure_tenant_id", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + LeafKeyID: "azurekms:name=test-key;vault=test-vault", + }, + wantErr: "options map is required for Azure KMS", + }, + { + name: "missing_hashivault_options", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "test-key", + LeafKeyID: "test-key", + }, + wantErr: "options map is required for HashiVault KMS", + }, + { + name: "missing_hashivault_token", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "test-key", + LeafKeyID: "test-key", + Options: map[string]string{"vault-address": "http://localhost:8200"}, + }, + wantErr: "vault-token is required for HashiVault KMS", + }, + { + name: "missing_hashivault_address", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "test-key", + LeafKeyID: "test-key", + Options: map[string]string{"vault-token": "test-token"}, + }, + wantErr: "vault-address is required for HashiVault KMS", + }, + { + name: "invalid_gcp_intermediate_key", + config: KMSConfig{ + Type: "gcpkms", + RootKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", + IntermediateKeyID: "invalid-format", + LeafKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", + }, + wantErr: "gcpkms IntermediateKeyID must start with 'projects/'", + }, + { + name: "invalid_azure_leaf_key", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + LeafKeyID: "invalid-format", + Options: map[string]string{"azure-tenant-id": "test-tenant"}, + }, + wantErr: "azurekms LeafKeyID must start with 'azurekms:name='", + }, + { + name: "unsupported_kms_type", + config: KMSConfig{ + Type: "unsupported", + RootKeyID: "test-key", + LeafKeyID: "test-key", + }, + wantErr: "unsupported KMS type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateKMSConfig(tt.config) + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + }) + } +} + +func TestCreateCertificates_CryptoSignerErrors(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + rootCertPath := filepath.Join(tmpDir, "root.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + t.Run("signer_not_implementing_cryptosigner", func(t *testing.T) { + mockSigner := &mockSignerVerifier{ + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, nil + }, + cryptoSignerFunc: func(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { + return nil, nil, fmt.Errorf("signer does not implement CryptoSigner") + }, + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + err := CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), "error getting root crypto signer: signer does not implement CryptoSigner") + }) + + t.Run("cryptosigner_error", func(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + cryptoSignerFunc: func(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { + return nil, nil, fmt.Errorf("crypto signer error") + }, + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + LeafKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + certErr := CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, certErr) + require.Contains(t, certErr.Error(), "error getting root crypto signer: crypto signer error") + }) +} + +func TestValidateTemplate_AdditionalCases(t *testing.T) { + tests := []struct { + name string + template string + parent *x509.Certificate + wantError string + }{ + { + name: "missing_key_usage", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + wantError: "CA certificate must specify at least one key usage", + }, + { + name: "invalid_key_usage_combination", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature", "keyEncipherment"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + wantError: "CA certificate must have certSign key usage", + }, + { + name: "invalid_basic_constraints", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": false, + "maxPathLen": 1 + } + }`, + wantError: "CA certificate must have isCA set to true", + }, + { + name: "invalid_ext_key_usage_for_leaf", + template: `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["serverAuth"], + "basicConstraints": { + "isCA": false + } + }`, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test CA", + }, + }, + wantError: "timestamp authority certificate must have TimeStamping extended key usage", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "template-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + err = os.WriteFile(tmpFile.Name(), []byte(tt.template), 0644) + require.NoError(t, err) + + var certTmpl CertificateTemplate + err = json.Unmarshal([]byte(tt.template), &certTmpl) + require.NoError(t, err) + + err = ValidateTemplate(&certTmpl, tt.parent) + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestCreateCertificateFromTemplate(t *testing.T) { + tests := []struct { + name string + template *CertificateTemplate + parent *x509.Certificate + wantError string + }{ + { + name: "invalid_cert_lifetime_format", + template: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{ + CommonName: "Test CA", + }, + CertLifetime: "invalid", + KeyUsage: []string{"certSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: true, + MaxPathLen: 1, + }, + }, + wantError: "invalid certLifetime format", + }, + { + name: "invalid_extension_oid", + template: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{ + CommonName: "Test CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: true, + MaxPathLen: 1, + }, + Extensions: []struct { + ID string `json:"id"` + Critical bool `json:"critical"` + Value string `json:"value"` + }{ + { + ID: "invalid.oid", + Critical: true, + Value: "AQID", + }, + }, + }, + wantError: "invalid OID in extension", + }, + { + name: "invalid_extension_value", + template: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{ + CommonName: "Test CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: true, + MaxPathLen: 1, + }, + Extensions: []struct { + ID string `json:"id"` + Critical bool `json:"critical"` + Value string `json:"value"` + }{ + { + ID: "2.5.29.37", + Critical: true, + Value: "invalid-base64", + }, + }, + }, + wantError: "error decoding extension value", + }, + { + name: "successful_non_ca_cert", + template: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{ + CommonName: "Test Leaf", + Country: []string{"US"}, + Organization: []string{"Test Org"}, + OrganizationalUnit: []string{"Test Unit"}, + }, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"TimeStamping"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: false, + }, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test CA", + }, + }, + }, + { + name: "successful_ca_cert_with_extensions", + template: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{ + CommonName: "Test CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign", "digitalSignature"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: true, + MaxPathLen: 0, + }, + Extensions: []struct { + ID string `json:"id"` + Critical bool `json:"critical"` + Value string `json:"value"` + }{ + { + ID: "2.5.29.37", + Critical: true, + Value: "MCQwIgYDVR0lBBswGQYIKwYBBQUHAwgGDSsGAQQBgjcUAgICAf8=", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cert, err := CreateCertificateFromTemplate(tt.template, tt.parent) + if tt.wantError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantError) + } else { + require.NoError(t, err) + require.NotNil(t, cert) + + if tt.parent != nil { + require.Equal(t, tt.parent.Subject, cert.Issuer) + } else { + require.Equal(t, tt.template.Issuer.CommonName, cert.Issuer.CommonName) + } + + require.Equal(t, tt.template.Subject.CommonName, cert.Subject.CommonName) + require.Equal(t, tt.template.Subject.Country, cert.Subject.Country) + require.Equal(t, tt.template.Subject.Organization, cert.Subject.Organization) + require.Equal(t, tt.template.Subject.OrganizationalUnit, cert.Subject.OrganizationalUnit) + require.Equal(t, tt.template.BasicConstraints.IsCA, cert.IsCA) + + if tt.template.BasicConstraints.IsCA { + require.Equal(t, tt.template.BasicConstraints.MaxPathLen, cert.MaxPathLen) + require.Equal(t, tt.template.BasicConstraints.MaxPathLen == 0, cert.MaxPathLenZero) + } + + if len(tt.template.KeyUsage) > 0 { + for _, usage := range tt.template.KeyUsage { + switch usage { + case "certSign": + require.True(t, cert.KeyUsage&x509.KeyUsageCertSign != 0) + case "crlSign": + require.True(t, cert.KeyUsage&x509.KeyUsageCRLSign != 0) + case "digitalSignature": + require.True(t, cert.KeyUsage&x509.KeyUsageDigitalSignature != 0) + } + } + } + + // Verify extended key usages for non-CA certs + if !tt.template.BasicConstraints.IsCA && len(tt.template.ExtKeyUsage) > 0 { + for _, usage := range tt.template.ExtKeyUsage { + if usage == "TimeStamping" { + require.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageTimeStamping) + } + } + } + } + }) + } +} + +func TestCreateCertificatesWithWriteErrors(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + return &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + }, nil + } + + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + } + + config := KMSConfig{ + Type: "mock", + RootKeyID: "test-root-key", + LeafKeyID: "test-leaf-key", + } + + readOnlyDir := filepath.Join(tmpDir, "readonly") + err = os.MkdirAll(readOnlyDir, 0755) + require.NoError(t, err) + err = os.Chmod(readOnlyDir, 0500) + require.NoError(t, err) + + tests := []struct { + name string + rootPath string + leafPath string + wantError string + }{ + { + name: "root_cert_write_error", + rootPath: filepath.Join(readOnlyDir, "root.crt"), + leafPath: filepath.Join(tmpDir, "leaf.crt"), + wantError: "error writing root certificate: failed to create file", + }, + { + name: "leaf_cert_write_error", + rootPath: filepath.Join(tmpDir, "root.crt"), + leafPath: filepath.Join(readOnlyDir, "leaf.crt"), + wantError: "error writing leaf certificate: failed to create file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, tt.rootPath, tt.leafPath, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestCreateCertificatesWithSigningErrors(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.LeafKeyID == "test-leaf-key" { + return nil, fmt.Errorf("leaf signing error") + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + return &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + }, nil + } + + tests := []struct { + name string + setup func() (*mockSignerVerifier, *mockSignerVerifier) + wantError string + }{ + { + name: "leaf_signing_error", + setup: func() (*mockSignerVerifier, *mockSignerVerifier) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + return &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return &key.PublicKey, nil + }, + signMessageFunc: func(message io.Reader) ([]byte, error) { + msgBytes, err := io.ReadAll(message) + if err != nil { + return nil, err + } + h := crypto.SHA256.New() + h.Write(msgBytes) + digest := h.Sum(nil) + return ecdsa.SignASN1(rand.Reader, key, digest) + }, + signFunc: func(rand io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) { + return ecdsa.SignASN1(rand, key, digest) + }, + }, nil + }, + wantError: "leaf signing error", + }, + { + name: "root_signing_error", + setup: func() (*mockSignerVerifier, *mockSignerVerifier) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + rootSigner := &mockSignerVerifier{ + key: key, + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("root signing error") + }, + } + return rootSigner, nil + }, + wantError: "error getting root public key: root signing error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + rootSigner, _ := tt.setup() + + err = CreateCertificates(rootSigner, KMSConfig{ + Type: "mock", + RootKeyID: "test-root-key", + LeafKeyID: "test-leaf-key", + }, + rootTmplPath, + leafTmplPath, + filepath.Join(tmpDir, "root.crt"), + filepath.Join(tmpDir, "leaf.crt"), + "", "", "") + + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestInitKMSWithDifferentProviders(t *testing.T) { + tests := []struct { + name string + config KMSConfig + wantError string + }{ + { + name: "AWS KMS missing region", + config: KMSConfig{ + Type: "awskms", + RootKeyID: "alias/test-key", + LeafKeyID: "alias/test-key", + Options: map[string]string{}, + }, + wantError: "aws-region is required", + }, + { + name: "GCP KMS invalid key format", + config: KMSConfig{ + Type: "gcpkms", + RootKeyID: "invalid-key-format", + LeafKeyID: "invalid-key-format", + Options: map[string]string{ + "gcp-credentials-file": "/path/to/creds.json", + }, + }, + wantError: "must start with 'projects/'", + }, + { + name: "Azure KMS missing tenant ID", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + LeafKeyID: "azurekms:name=test-key;vault=test-vault", + Options: map[string]string{}, + }, + wantError: "azure-tenant-id is required", + }, + { + name: "HashiVault KMS missing token", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "test-key", + LeafKeyID: "test-key", + Options: map[string]string{ + "vault-address": "http://vault:8200", + }, + }, + wantError: "vault-token is required", + }, + { + name: "Unsupported KMS type", + config: KMSConfig{ + Type: "unsupported", + RootKeyID: "test-key", + LeafKeyID: "test-key", + Options: map[string]string{}, + }, + 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) + }) + } +} + +func TestWriteCertificateToFileErrors(t *testing.T) { + cert := &x509.Certificate{ + Raw: []byte("test certificate"), + } + + tests := []struct { + name string + filename string + wantError string + }{ + { + name: "invalid path", + filename: "/nonexistent/directory/cert.pem", + wantError: "failed to create file", + }, + { + name: "empty filename", + filename: "", + wantError: "failed to create file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := WriteCertificateToFile(cert, tt.filename) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestCreateCertificatesWithMockSigner(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + rootTemplate := filepath.Join(tmpDir, "root-template.json") + leafTemplate := filepath.Join(tmpDir, "leaf-template.json") + rootCert := filepath.Join(tmpDir, "root.pem") + leafCert := filepath.Join(tmpDir, "leaf.pem") + + rootTmplContent := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTmplContent := `{ + "subject": { + "commonName": "Test Leaf" + }, + "certLifetime": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTemplate, []byte(rootTmplContent), 0600) + require.NoError(t, err) + err = os.WriteFile(leafTemplate, []byte(leafTmplContent), 0600) + require.NoError(t, err) + + mockSV := &mockSignerVerifier{ + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("mock public key error") + }, + } + + config := KMSConfig{ + Type: "mock", + RootKeyID: "test-root-key", + LeafKeyID: "test-leaf-key", + } + + err = CreateCertificates(mockSV, config, rootTemplate, leafTemplate, rootCert, leafCert, "", "", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "mock public key error") +} diff --git a/pkg/certmaker/template.go b/pkg/certmaker/template.go index 1141d451..7a015bed 100644 --- a/pkg/certmaker/template.go +++ b/pkg/certmaker/template.go @@ -45,9 +45,9 @@ type CertificateTemplate struct { Issuer struct { CommonName string `json:"commonName"` } `json:"issuer"` - NotBefore string `json:"notBefore"` - NotAfter string `json:"notAfter"` + CertLifetime string `json:"certLifetime"` // Duration string e.g. "8760h" for 1 year KeyUsage []string `json:"keyUsage"` + ExtKeyUsage []string `json:"extKeyUsage,omitempty"` BasicConstraints struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -101,18 +101,37 @@ func ParseTemplate(filename string, parent *x509.Certificate) (*x509.Certificate // ValidateTemplate performs validation checks on the certificate template. func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error { - if tmpl.NotBefore == "" { - return fmt.Errorf("notBefore time must be specified") - } - if tmpl.NotAfter == "" { - return fmt.Errorf("notAfter time must be specified") - } - if _, err := time.Parse(time.RFC3339, tmpl.NotBefore); err != nil { - return fmt.Errorf("invalid notBefore time format: %w", err) + var notBefore, notAfter time.Time + var err error + + if tmpl.CertLifetime != "" { + duration, err := time.ParseDuration(tmpl.CertLifetime) + if err != nil { + return fmt.Errorf("invalid certLifetime format: %w", err) + } + if duration <= 0 { + return fmt.Errorf("certLifetime must be positive") + } + notBefore = time.Now().UTC() + notAfter = notBefore.Add(duration) + } else { + if tmpl.CertLifetime == "" { + return fmt.Errorf("certLifetime must be specified") + } + notBefore, err = time.Parse(time.RFC3339, tmpl.CertLifetime) + if err != nil { + return fmt.Errorf("invalid certLifetime format: %w", err) + } + notAfter, err = time.Parse(time.RFC3339, tmpl.CertLifetime) + if err != nil { + return fmt.Errorf("invalid certLifetime format: %w", err) + } } - if _, err := time.Parse(time.RFC3339, tmpl.NotAfter); err != nil { - return fmt.Errorf("invalid notAfter time format: %w", err) + + if notBefore.After(notAfter) { + return fmt.Errorf("NotBefore time must be before NotAfter time") } + if tmpl.Subject.CommonName == "" { return fmt.Errorf("template subject.commonName cannot be empty") } @@ -120,18 +139,25 @@ func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error return fmt.Errorf("template issuer.commonName cannot be empty for root certificate") } + // Check if the certificate has certSign key usage + hasKeyUsageCertSign := false + for _, usage := range tmpl.KeyUsage { + if usage == "certSign" { + hasKeyUsageCertSign = true + break + } + } + + // For certificates with certSign key usage, they must be CA certificates + if hasKeyUsageCertSign && !tmpl.BasicConstraints.IsCA { + return fmt.Errorf("CA certificate must have isCA set to true") + } + // For CA certs if tmpl.BasicConstraints.IsCA { if len(tmpl.KeyUsage) == 0 { return fmt.Errorf("CA certificate must specify at least one key usage") } - hasKeyUsageCertSign := false - for _, usage := range tmpl.KeyUsage { - if usage == "certSign" { - hasKeyUsageCertSign = true - break - } - } if !hasKeyUsageCertSign { return fmt.Errorf("CA certificate must have certSign key usage") } @@ -168,6 +194,41 @@ func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error if !hasDigitalSignature { return fmt.Errorf("timestamp authority certificate must have digitalSignature key usage") } + + // Check for TimeStamping extended key usage + hasTimeStamping := false + + // Check extKeyUsage field first + if len(tmpl.ExtKeyUsage) > 0 { + for _, usage := range tmpl.ExtKeyUsage { + if usage == "TimeStamping" { + hasTimeStamping = true + break + } + } + } + + // If not found in extKeyUsage, check Extensions + if !hasTimeStamping { + for _, ext := range tmpl.Extensions { + if ext.ID == "2.5.29.37" { // Extended Key Usage OID + // Decode the base64 value + value, err := base64.StdEncoding.DecodeString(ext.Value) + if err != nil { + return fmt.Errorf("error decoding extension value: %w", err) + } + // Check if the value contains the TimeStamping OID (1.3.6.1.5.5.7.3.8) + if bytes.Contains(value, []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x08}) { + hasTimeStamping = true + break + } + } + } + } + + if !hasTimeStamping { + return fmt.Errorf("timestamp authority certificate must have TimeStamping extended key usage") + } } // Validate extensions @@ -183,25 +244,34 @@ func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error } } - notBefore, _ := time.Parse(time.RFC3339, tmpl.NotBefore) - notAfter, _ := time.Parse(time.RFC3339, tmpl.NotAfter) - if notBefore.After(notAfter) { - return fmt.Errorf("NotBefore time must be before NotAfter time") - } - return nil } // CreateCertificateFromTemplate creates an x509.Certificate from the provided template func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) (*x509.Certificate, error) { - notBefore, err := time.Parse(time.RFC3339, tmpl.NotBefore) - if err != nil { - return nil, fmt.Errorf("invalid notBefore time format: %w", err) + var notBefore, notAfter time.Time + var err error + + if tmpl.CertLifetime != "" { + duration, err := time.ParseDuration(tmpl.CertLifetime) + if err != nil { + return nil, fmt.Errorf("invalid certLifetime format: %w", err) + } + notBefore = time.Now().UTC() + notAfter = notBefore.Add(duration) + } else { + notBefore, err = time.Parse(time.RFC3339, tmpl.CertLifetime) + if err != nil { + return nil, fmt.Errorf("invalid certLifetime format: %w", err) + } + notAfter, err = time.Parse(time.RFC3339, tmpl.CertLifetime) + if err != nil { + return nil, fmt.Errorf("invalid certLifetime format: %w", err) + } } - notAfter, err := time.Parse(time.RFC3339, tmpl.NotAfter) - if err != nil { - return nil, fmt.Errorf("invalid notAfter time format: %w", err) + if notBefore.After(notAfter) { + return nil, fmt.Errorf("NotBefore time must be before NotAfter time") } cert := &x509.Certificate{ @@ -228,9 +298,16 @@ func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certi if tmpl.BasicConstraints.IsCA { cert.MaxPathLen = tmpl.BasicConstraints.MaxPathLen cert.MaxPathLenZero = tmpl.BasicConstraints.MaxPathLen == 0 + } else { + // For non-CA certificates, set the extended key usage + for _, usage := range tmpl.ExtKeyUsage { + if usage == "TimeStamping" { + cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageTimeStamping) + } + } } - SetKeyUsages(cert, tmpl.KeyUsage) + SetCertificateUsages(cert, tmpl.KeyUsage, tmpl.ExtKeyUsage) // Sets extensions (e.g. Timestamping) for _, ext := range tmpl.Extensions { @@ -260,10 +337,12 @@ func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certi return cert, nil } -// SetKeyUsages applies the specified key usage to cert(s) -// supporting certSign, crlSign, and digitalSignature usages. -func SetKeyUsages(cert *x509.Certificate, usages []string) { - for _, usage := range usages { +// SetCertificateUsages applies both basic key usages and extended key usages to the certificate. +// Supports basic usages: certSign, crlSign, digitalSignature +// Supports extended usages: CodeSigning, TimeStamping +func SetCertificateUsages(cert *x509.Certificate, keyUsages []string, extKeyUsages []string) { + // Set basic key usages + for _, usage := range keyUsages { switch usage { case "certSign": cert.KeyUsage |= x509.KeyUsageCertSign @@ -273,4 +352,14 @@ func SetKeyUsages(cert *x509.Certificate, usages []string) { cert.KeyUsage |= x509.KeyUsageDigitalSignature } } + + // Set extended key usages + for _, usage := range extKeyUsages { + switch usage { + case "CodeSigning": + cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) + case "TimeStamping": + cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageTimeStamping) + } + } } diff --git a/pkg/certmaker/template_test.go b/pkg/certmaker/template_test.go index 075fb778..f3d62cc4 100644 --- a/pkg/certmaker/template_test.go +++ b/pkg/certmaker/template_test.go @@ -47,85 +47,95 @@ func TestParseTemplate(t *testing.T) { tests := []struct { name string - content string - parent *x509.Certificate + template string wantError string }{ { - name: "valid template", - content: `{ - "subject": { - "commonName": "Test TSA" - }, - "issuer": { - "commonName": "Test TSA" - }, - "keyUsage": [ - "digitalSignature" - ], - "basicConstraints": { - "isCA": false - }, - "extensions": [ - { - "id": "2.5.29.37", - "critical": true, - "value": "MCQwIgYDVR0lBBswGQYIKwYBBQUHAwgGDSsGAQQBgjcUAgICAf8=" - } - ], - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`, - parent: parent, + name: "valid template with duration-based validity", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Parent CA" + }, + "certLifetime": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + wantError: "", }, { - name: "missing required fields", - content: `{ - "issuer": {"commonName": "Test TSA"}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`, - wantError: "subject.commonName cannot be empty", + name: "invalid certLifetime format", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Parent CA" + }, + "certLifetime": "invalid", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + wantError: "invalid certLifetime format", }, { - name: "invalid time format", - content: `{ - "subject": {"commonName": "Test TSA"}, - "issuer": {"commonName": "Test TSA"}, - "notBefore": "invalid", - "notAfter": "2025-01-01T00:00:00Z" - }`, - wantError: "invalid notBefore time format", + name: "missing certLifetime", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Parent CA" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + wantError: "certLifetime must be specified", }, { - name: "missing digital signature usage", - content: `{ - "subject": {"commonName": "Test TSA"}, - "issuer": {"commonName": "Test TSA"}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z", - "keyUsage": ["certSign"], - "basicConstraints": {"isCA": false} - }`, - wantError: "timestamp authority certificate must have digitalSignature key usage", + name: "negative certLifetime", + template: `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Parent CA" + }, + "certLifetime": "-8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }`, + wantError: "certLifetime must be positive", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tmpFile := filepath.Join(tmpDir, "template.json") - err := os.WriteFile(tmpFile, []byte(tt.content), 0600) + templateFile := filepath.Join(tmpDir, "template.json") + err := os.WriteFile(templateFile, []byte(tt.template), 0600) require.NoError(t, err) - cert, err := ParseTemplate(tmpFile, tt.parent) + cert, err := ParseTemplate(templateFile, parent) if tt.wantError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantError) } else { require.NoError(t, err) - if cert == nil { - t.Error("Expected non-nil certificate") - } + assert.NotNil(t, cert) } }) } @@ -139,6 +149,7 @@ func TestParseTemplateWithInvalidExtensions(t *testing.T) { content := `{ "subject": {"commonName": "Test TSA"}, "issuer": {"commonName": "Test TSA"}, + "certLifetime": "8760h", "keyUsage": ["digitalSignature"], "basicConstraints": {"isCA": false}, "extensions": [ @@ -147,9 +158,7 @@ func TestParseTemplateWithInvalidExtensions(t *testing.T) { "critical": true, "value": "invalid-base64" } - ], - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" + ] }` tmpFile := filepath.Join(tmpDir, "template.json") @@ -169,115 +178,118 @@ func TestParseTemplateWithInvalidExtensions(t *testing.T) { } func TestValidateTemplate(t *testing.T) { - parent := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Parent CA", - }, - } - tests := []struct { name string - tmpl *CertificateTemplate + template *CertificateTemplate parent *x509.Certificate wantError string }{ { - name: "valid TSA template", - tmpl: &CertificateTemplate{ + name: "valid root CA template", + template: &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", + CommonName: "Test Root CA", }, Issuer: struct { CommonName string `json:"commonName"` }{ - CommonName: "Test TSA", + CommonName: "Test Root CA", }, - 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"` + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` }{ - { - ID: "2.5.29.37", - Critical: true, - Value: base64.StdEncoding.EncodeToString([]byte{0x30, 0x24, 0x30, 0x22, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x08}), - }, + IsCA: true, + MaxPathLen: 1, }, }, - parent: parent, }, { - name: "empty notBefore time", - tmpl: &CertificateTemplate{ + name: "valid intermediate CA template", + template: &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", + CommonName: "Test Intermediate CA", }, Issuer: struct { CommonName string `json:"commonName"` }{ - CommonName: "Test TSA", + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: true, + MaxPathLen: 0, + }, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", }, - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, }, - wantError: "notBefore time must be specified", }, { - name: "empty notAfter time", - tmpl: &CertificateTemplate{ + name: "valid leaf template", + template: &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", + CommonName: "Test Leaf", }, Issuer: struct { CommonName string `json:"commonName"` }{ - CommonName: "Test TSA", + CommonName: "Test Intermediate CA", }, - NotBefore: "2024-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, - }, - wantError: "notAfter time must be specified", - }, - { - name: "invalid notBefore format", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` }{ - CommonName: "Test TSA", + IsCA: false, + }, + Extensions: []struct { + ID string `json:"id"` + Critical bool `json:"critical"` + Value string `json:"value"` + }{ + { + ID: "2.5.29.37", + Critical: true, + Value: "MCQwIgYDVR0lBBswGQYIKwYBBQUHAwgGDSsGAQQBgjcUAgICAf8=", + }, + }, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Intermediate CA", }, - NotBefore: "invalid", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, }, - wantError: "invalid notBefore time format", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateTemplate(tt.tmpl, tt.parent) + err := ValidateTemplate(tt.template, tt.parent) if tt.wantError != "" { if err == nil { t.Error("Expected error but got none") @@ -307,14 +319,14 @@ func TestValidateTemplateWithMockKMS(t *testing.T) { tests := []struct { name string - tmpl *CertificateTemplate + template *CertificateTemplate parent *x509.Certificate signer signature.SignerVerifier wantError string }{ { - name: "valid TSA template with mock KMS", - tmpl: &CertificateTemplate{ + name: "valid template", + template: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` Organization []string `json:"organization,omitempty"` @@ -328,9 +340,8 @@ func TestValidateTemplateWithMockKMS(t *testing.T) { }{ CommonName: "Test TSA", }, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, Extensions: []struct { ID string `json:"id"` Critical bool `json:"critical"` @@ -343,12 +354,16 @@ func TestValidateTemplateWithMockKMS(t *testing.T) { }, }, }, - parent: parent, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test TSA", + }, + }, signer: mockSigner, }, { - name: "invalid TSA template with mock KMS", - tmpl: &CertificateTemplate{ + name: "missing certLifetime", + template: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` Organization []string `json:"organization,omitempty"` @@ -357,19 +372,22 @@ func TestValidateTemplateWithMockKMS(t *testing.T) { }{ CommonName: "Test TSA", }, - NotBefore: "invalid", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{ + CommonName: "Test TSA", + }, + KeyUsage: []string{"digitalSignature"}, }, parent: parent, signer: mockSigner, - wantError: "invalid notBefore time format", + wantError: "certLifetime must be specified", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateTemplate(tt.tmpl, tt.parent) + err := ValidateTemplate(tt.template, tt.parent) if tt.wantError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantError) @@ -379,3 +397,137 @@ func TestValidateTemplateWithMockKMS(t *testing.T) { }) } } + +func TestParseTemplateWithInvalidJSON(t *testing.T) { + tests := []struct { + name string + content string + wantError string + }{ + { + name: "invalid JSON structure", + content: `{ + "subject": { + "commonName": "Test" + }, + "keyUsage": ["certSign", // missing closing bracket + "certLifetime": "8760h" + }`, + wantError: "invalid character", + }, + { + name: "empty template", + content: `{}`, + wantError: "certLifetime must be specified", + }, + { + name: "missing required fields", + content: `{ + "subject": {}, + "certLifetime": "8760h" + }`, + wantError: "subject.commonName cannot be empty", + }, + { + name: "invalid key usage", + content: `{ + "subject": { + "commonName": "Test" + }, + "issuer": { + "commonName": "Test" + }, + "certLifetime": "8760h", + "keyUsage": ["invalidUsage"], + "basicConstraints": { + "isCA": false + } + }`, + wantError: "timestamp authority certificate must have digitalSignature key usage", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "cert-template-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + err = os.WriteFile(tmpFile.Name(), []byte(tt.content), 0600) + require.NoError(t, err) + + _, err = ParseTemplate(tmpFile.Name(), nil) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestSetCertificateUsagesComprehensive(t *testing.T) { + tests := []struct { + name string + keyUsages []string + extKeyUsages []string + expectedKeyUsage x509.KeyUsage + expectedExtUsages []x509.ExtKeyUsage + }{ + { + name: "empty usages", + keyUsages: []string{}, + extKeyUsages: []string{}, + expectedKeyUsage: 0, + expectedExtUsages: nil, + }, + { + name: "single key usage", + keyUsages: []string{"certSign"}, + extKeyUsages: []string{}, + expectedKeyUsage: x509.KeyUsageCertSign, + expectedExtUsages: nil, + }, + { + name: "single ext usage", + keyUsages: []string{}, + extKeyUsages: []string{"CodeSigning"}, + expectedKeyUsage: 0, + expectedExtUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }, + { + name: "multiple key usages", + keyUsages: []string{"certSign", "crlSign", "digitalSignature"}, + extKeyUsages: []string{}, + expectedKeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, + expectedExtUsages: nil, + }, + { + name: "multiple ext usages", + keyUsages: []string{}, + extKeyUsages: []string{"CodeSigning", "TimeStamping"}, + expectedKeyUsage: 0, + expectedExtUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageTimeStamping}, + }, + { + name: "both key and ext usages", + keyUsages: []string{"certSign", "digitalSignature"}, + extKeyUsages: []string{"CodeSigning", "TimeStamping"}, + expectedKeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + expectedExtUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageTimeStamping}, + }, + { + name: "invalid usages", + keyUsages: []string{"invalidKeyUsage"}, + extKeyUsages: []string{"invalidExtUsage"}, + expectedKeyUsage: 0, + expectedExtUsages: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cert := &x509.Certificate{} + SetCertificateUsages(cert, tt.keyUsages, tt.extKeyUsages) + assert.Equal(t, tt.expectedKeyUsage, cert.KeyUsage) + assert.Equal(t, tt.expectedExtUsages, cert.ExtKeyUsage) + }) + } +} diff --git a/pkg/certmaker/templates/intermediate-template.json b/pkg/certmaker/templates/intermediate-template.json index 5b032f7e..681a394f 100644 --- a/pkg/certmaker/templates/intermediate-template.json +++ b/pkg/certmaker/templates/intermediate-template.json @@ -14,8 +14,7 @@ "issuer": { "commonName": "" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", + "certLifetime": "43800h", "basicConstraints": { "isCA": true, "maxPathLen": 0 diff --git a/pkg/certmaker/templates/leaf-template.json b/pkg/certmaker/templates/leaf-template.json index 1756def9..7dac179f 100644 --- a/pkg/certmaker/templates/leaf-template.json +++ b/pkg/certmaker/templates/leaf-template.json @@ -14,8 +14,7 @@ "issuer": { "commonName": "" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", + "certLifetime": "8760h", "basicConstraints": { "isCA": false }, diff --git a/pkg/certmaker/templates/root-template.json b/pkg/certmaker/templates/root-template.json index 32464de1..8ece8da4 100644 --- a/pkg/certmaker/templates/root-template.json +++ b/pkg/certmaker/templates/root-template.json @@ -14,8 +14,7 @@ "issuer": { "commonName": "" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", + "certLifetime": "87600h", "basicConstraints": { "isCA": true, "maxPathLen": 1