diff --git a/cmd/certificate_maker/certificate_maker.go b/cmd/certificate_maker/certificate_maker.go index 5ae959e43..f11a00100 100644 --- a/cmd/certificate_maker/certificate_maker.go +++ b/cmd/certificate_maker/certificate_maker.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "os" + "strings" "time" "github.com/sigstore/fulcio/pkg/certmaker" @@ -65,6 +66,20 @@ func mustBindEnv(key, envVar string) { func init() { log.ConfigureLogger("prod") + viper.AutomaticEnv() + viper.SetEnvPrefix("") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + mustBindEnv("kms-type", "KMS_TYPE") + mustBindEnv("aws-region", "AWS_REGION") + mustBindEnv("azure-tenant-id", "AZURE_TENANT_ID") + mustBindEnv("gcp-credentials-file", "GCP_CREDENTIALS_FILE") + mustBindEnv("vault-token", "VAULT_TOKEN") + mustBindEnv("vault-address", "VAULT_ADDR") + mustBindEnv("root-key-id", "KMS_ROOT_KEY_ID") + mustBindEnv("intermediate-key-id", "KMS_INTERMEDIATE_KEY_ID") + mustBindEnv("leaf-key-id", "KMS_LEAF_KEY_ID") + rootCmd.AddCommand(createCmd) // KMS provider flags @@ -90,7 +105,6 @@ func init() { createCmd.Flags().String("leaf-template", "pkg/certmaker/templates/leaf-template.json", "Path to leaf certificate template") createCmd.Flags().String("leaf-cert", "leaf.pem", "Output path for leaf certificate") - // Bind flags to viper mustBindPFlag("kms-type", createCmd.Flags().Lookup("kms-type")) mustBindPFlag("aws-region", createCmd.Flags().Lookup("aws-region")) mustBindPFlag("azure-tenant-id", createCmd.Flags().Lookup("azure-tenant-id")) @@ -106,17 +120,6 @@ func init() { mustBindPFlag("leaf-key-id", createCmd.Flags().Lookup("leaf-key-id")) mustBindPFlag("leaf-template", createCmd.Flags().Lookup("leaf-template")) mustBindPFlag("leaf-cert", createCmd.Flags().Lookup("leaf-cert")) - - // Bind environment variables - mustBindEnv("kms-type", "KMS_TYPE") - mustBindEnv("aws-region", "AWS_REGION") - mustBindEnv("azure-tenant-id", "AZURE_TENANT_ID") - mustBindEnv("gcp-credentials-file", "GOOGLE_APPLICATION_CREDENTIALS") - mustBindEnv("vault-token", "VAULT_TOKEN") - mustBindEnv("vault-address", "VAULT_ADDR") - mustBindEnv("root-key-id", "KMS_ROOT_KEY_ID") - mustBindEnv("intermediate-key-id", "KMS_INTERMEDIATE_KEY_ID") - mustBindEnv("leaf-key-id", "KMS_LEAF_KEY_ID") } func runCreate(_ *cobra.Command, _ []string) error { diff --git a/cmd/certificate_maker/certificate_maker_test.go b/cmd/certificate_maker/certificate_maker_test.go index d57566208..7da89843f 100644 --- a/cmd/certificate_maker/certificate_maker_test.go +++ b/cmd/certificate_maker/certificate_maker_test.go @@ -18,6 +18,7 @@ package main import ( "os" "path/filepath" + "strings" "testing" "github.com/sigstore/fulcio/pkg/log" @@ -28,7 +29,6 @@ import ( ) func TestGetConfigValue(t *testing.T) { - // KMS provider flags var ( kmsType string awsKMSRegion string @@ -37,17 +37,14 @@ func TestGetConfigValue(t *testing.T) { hashiVaultToken string hashiVaultAddr string - // Root certificate flags rootKeyID string rootTemplatePath string rootCertPath string - // Intermediate certificate flags intermediateKeyID string intermediateTemplatePath string intermediateCertPath string - // Leaf certificate flags leafKeyID string leafTemplatePath string leafCertPath string @@ -58,7 +55,6 @@ func TestGetConfigValue(t *testing.T) { }, } - // KMS provider flags cmd.Flags().StringVar(&kmsType, "kms-type", "", "KMS provider type") cmd.Flags().StringVar(&awsKMSRegion, "aws-region", "", "AWS KMS region") cmd.Flags().StringVar(&azureKMSTenantID, "azure-tenant-id", "", "Azure KMS tenant ID") @@ -66,17 +62,14 @@ func TestGetConfigValue(t *testing.T) { cmd.Flags().StringVar(&hashiVaultToken, "vault-token", "", "HashiVault token") cmd.Flags().StringVar(&hashiVaultAddr, "vault-address", "", "HashiVault server address") - // Root certificate flags cmd.Flags().StringVar(&rootKeyID, "root-key-id", "", "KMS key identifier for root certificate") cmd.Flags().StringVar(&rootTemplatePath, "root-template", "", "Path to root certificate template") cmd.Flags().StringVar(&rootCertPath, "root-cert", "root.pem", "Output path for root certificate") - // Intermediate certificate flags cmd.Flags().StringVar(&intermediateKeyID, "intermediate-key-id", "", "KMS key identifier for intermediate certificate") cmd.Flags().StringVar(&intermediateTemplatePath, "intermediate-template", "", "Path to intermediate certificate template") cmd.Flags().StringVar(&intermediateCertPath, "intermediate-cert", "intermediate.pem", "Output path for intermediate certificate") - // Leaf certificate flags cmd.Flags().StringVar(&leafKeyID, "leaf-key-id", "", "KMS key identifier for leaf certificate") cmd.Flags().StringVar(&leafTemplatePath, "leaf-template", "", "Path to leaf certificate template") cmd.Flags().StringVar(&leafCertPath, "leaf-cert", "leaf.pem", "Output path for leaf certificate") @@ -160,7 +153,8 @@ func TestRunCreate(t *testing.T) { "basicConstraints": { "isCA": true, "maxPathLen": 1 - } + }, + "certLife": "8760h" }` leafTemplate := `{ @@ -173,7 +167,8 @@ func TestRunCreate(t *testing.T) { "extKeyUsage": ["CodeSigning"], "basicConstraints": { "isCA": false - } + }, + "certLife": "8760h" }` rootTmplPath := filepath.Join(tmpDir, "root-template.json") @@ -315,7 +310,6 @@ func TestRunCreate(t *testing.T) { RunE: runCreate, } - // KMS provider flags cmd.Flags().String("kms-type", "", "KMS provider type") cmd.Flags().String("aws-region", "", "AWS KMS region") cmd.Flags().String("azure-tenant-id", "", "Azure KMS tenant ID") @@ -323,17 +317,14 @@ func TestRunCreate(t *testing.T) { cmd.Flags().String("vault-token", "", "HashiVault token") cmd.Flags().String("vault-address", "", "HashiVault server address") - // Root certificate flags cmd.Flags().String("root-key-id", "", "KMS key identifier for root certificate") cmd.Flags().String("root-template", "", "Path to root certificate template") cmd.Flags().String("root-cert", "root.pem", "Output path for root certificate") - // Intermediate certificate flags cmd.Flags().String("intermediate-key-id", "", "KMS key identifier for intermediate certificate") cmd.Flags().String("intermediate-template", "", "Path to intermediate certificate template") cmd.Flags().String("intermediate-cert", "intermediate.pem", "Output path for intermediate certificate") - // Leaf certificate flags cmd.Flags().String("leaf-key-id", "", "KMS key identifier for leaf certificate") cmd.Flags().String("leaf-template", "", "Path to leaf certificate template") cmd.Flags().String("leaf-cert", "leaf.pem", "Output path for leaf certificate") @@ -400,7 +391,15 @@ func TestRunCreate(t *testing.T) { if tt.wantError { require.Error(t, err) - assert.Contains(t, err.Error(), tt.errMsg) + if tt.name == "AWS KMS test" { + assert.True(t, + strings.Contains(err.Error(), "get identity: get credentials: failed to refresh cached credentials, no EC2 IMDS role found") || + strings.Contains(err.Error(), "NotFoundException: Alias arn:aws:kms:us-west-2:") || + strings.Contains(err.Error(), "operation error KMS: GetPublicKey"), + "Expected AWS credentials or key not found error, got: %v", err) + } else { + assert.Contains(t, err.Error(), tt.errMsg) + } } else { require.NoError(t, err) } @@ -453,3 +452,259 @@ func TestRootCommand(t *testing.T) { err = rootCmd.Execute() require.Error(t, err) } + +func setupTestCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "test", + RunE: func(_ *cobra.Command, _ []string) error { + return nil + }, + } + + cmd.Flags().String("kms-type", "", "KMS type") + cmd.Flags().String("aws-region", "", "AWS KMS region") + cmd.Flags().String("azure-tenant-id", "", "Azure KMS 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", "", "Root template path") + cmd.Flags().String("leaf-template", "", "Leaf template path") + + return cmd +} + +func TestEnvironmentVariableOverrides(t *testing.T) { + oldEnv := make(map[string]string) + envVars := []string{ + "KMS_TYPE", + "AWS_REGION", + "AZURE_TENANT_ID", + "GCP_CREDENTIALS_FILE", + "VAULT_TOKEN", + "VAULT_ADDR", + "KMS_ROOT_KEY_ID", + "KMS_INTERMEDIATE_KEY_ID", + "KMS_LEAF_KEY_ID", + } + for _, env := range envVars { + oldEnv[env] = os.Getenv(env) + } + + defer func() { + for env, value := range oldEnv { + if value == "" { + os.Unsetenv(env) + } else { + os.Setenv(env, value) + } + } + }() + + tests := []struct { + name string + envVars map[string]string + flags map[string]string + wantValues map[string]string + }{ + { + name: "environment_overrides_flags", + envVars: map[string]string{ + "KMS_TYPE": "awskms", + "AWS_REGION": "us-east-1", + "KMS_ROOT_KEY_ID": "env-root-key", + "KMS_LEAF_KEY_ID": "env-leaf-key", + }, + flags: map[string]string{ + "kms-type": "gcpkms", + "aws-region": "us-west-2", + "root-key-id": "flag-root-key", + "leaf-key-id": "flag-leaf-key", + }, + wantValues: map[string]string{ + "kms-type": "awskms", + "aws-region": "us-east-1", + "root-key-id": "env-root-key", + "leaf-key-id": "env-leaf-key", + }, + }, + { + name: "azure_kms_environment", + envVars: map[string]string{ + "KMS_TYPE": "azurekms", + "AZURE_TENANT_ID": "env-tenant-id", + "KMS_ROOT_KEY_ID": "azurekms:name=env-key;vault=env-vault", + "KMS_LEAF_KEY_ID": "azurekms:name=env-leaf;vault=env-vault", + }, + flags: map[string]string{ + "azure-tenant-id": "flag-tenant-id", + "root-key-id": "flag-root-key", + "leaf-key-id": "flag-leaf-key", + }, + wantValues: map[string]string{ + "kms-type": "azurekms", + "azure-tenant-id": "env-tenant-id", + "root-key-id": "azurekms:name=env-key;vault=env-vault", + "leaf-key-id": "azurekms:name=env-leaf;vault=env-vault", + }, + }, + { + name: "hashivault_kms_environment", + envVars: map[string]string{ + "KMS_TYPE": "hashivault", + "VAULT_TOKEN": "env-token", + "VAULT_ADDR": "http://env-vault:8200", + "KMS_ROOT_KEY_ID": "transit/keys/env-key", + "KMS_LEAF_KEY_ID": "transit/keys/env-leaf", + }, + flags: map[string]string{ + "vault-token": "flag-token", + "vault-address": "http://flag-vault:8200", + "root-key-id": "flag-root-key", + "leaf-key-id": "flag-leaf-key", + }, + wantValues: map[string]string{ + "kms-type": "hashivault", + "vault-token": "env-token", + "vault-address": "http://env-vault:8200", + "root-key-id": "transit/keys/env-key", + "leaf-key-id": "transit/keys/env-leaf", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, env := range envVars { + os.Unsetenv(env) + } + viper.Reset() + + viper.AutomaticEnv() + viper.SetEnvPrefix("") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + mustBindEnv("kms-type", "KMS_TYPE") + mustBindEnv("aws-region", "AWS_REGION") + mustBindEnv("azure-tenant-id", "AZURE_TENANT_ID") + mustBindEnv("vault-token", "VAULT_TOKEN") + mustBindEnv("vault-address", "VAULT_ADDR") + mustBindEnv("root-key-id", "KMS_ROOT_KEY_ID") + mustBindEnv("leaf-key-id", "KMS_LEAF_KEY_ID") + + for k, v := range tt.envVars { + os.Setenv(k, v) + } + + for k, v := range tt.flags { + viper.SetDefault(k, v) + } + + for k, want := range tt.wantValues { + got := viper.GetString(k) + assert.Equal(t, want, got, "key %s", k) + } + }) + } +} + +func TestTemplateValidationInRunCreate(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "template-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + validTemplate := `{ + "subject": { + "commonName": "Test CA" + }, + "issuer": { + "commonName": "Test CA" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + invalidTemplate := `{ + "invalid": json, + }` + + validPath := filepath.Join(tmpDir, "valid.json") + invalidPath := filepath.Join(tmpDir, "invalid.json") + nonexistentPath := filepath.Join(tmpDir, "nonexistent.json") + + err = os.WriteFile(validPath, []byte(validTemplate), 0600) + require.NoError(t, err) + err = os.WriteFile(invalidPath, []byte(invalidTemplate), 0600) + require.NoError(t, err) + + tests := []struct { + name string + flags []string + wantError string + }{ + { + name: "valid_template_paths", + flags: []string{ + "--kms-type", "awskms", + "--aws-region", "us-west-2", + "--root-key-id", "alias/test-key", + "--leaf-key-id", "alias/test-leaf-key", + "--root-template", validPath, + "--leaf-template", validPath, + }, + wantError: "error getting root public key", + }, + { + name: "nonexistent_root_template", + flags: []string{ + "--kms-type", "awskms", + "--aws-region", "us-west-2", + "--root-key-id", "alias/test-key", + "--leaf-key-id", "alias/test-leaf-key", + "--root-template", nonexistentPath, + "--leaf-template", validPath, + }, + wantError: "template not found", + }, + { + name: "invalid_root_template_json", + flags: []string{ + "--kms-type", "awskms", + "--aws-region", "us-west-2", + "--root-key-id", "alias/test-key", + "--leaf-key-id", "alias/test-leaf-key", + "--root-template", invalidPath, + "--leaf-template", validPath, + }, + wantError: "invalid JSON", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + viper.AutomaticEnv() + viper.SetEnvPrefix("") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + cmd := setupTestCommand() + cmd.RunE = runCreate + + mustBindPFlag("kms-type", cmd.Flags().Lookup("kms-type")) + mustBindPFlag("aws-region", cmd.Flags().Lookup("aws-region")) + mustBindPFlag("root-key-id", cmd.Flags().Lookup("root-key-id")) + mustBindPFlag("leaf-key-id", cmd.Flags().Lookup("leaf-key-id")) + mustBindPFlag("root-template", cmd.Flags().Lookup("root-template")) + mustBindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template")) + + cmd.SetArgs(tt.flags) + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + }) + } +} diff --git a/docs/certificate-maker.md b/docs/certificate-maker.md index cc3e944bc..57a233b16 100644 --- a/docs/certificate-maker.md +++ b/docs/certificate-maker.md @@ -5,6 +5,8 @@ This tool creates root, intermediate (optional), and leaf certificates for Fulci - Two-level chain (root -> leaf) - Three-level chain (root -> intermediate -> leaf) +Relies on [x509util](https://pkg.go.dev/go.step.sm/crypto/x509util) which builds X.509 certificates from JSON templates. + ## Requirements - Access to one of the supported KMS providers (AWS, Google Cloud, Azure, HashiCorp Vault) @@ -46,12 +48,17 @@ Available flags: ### Environment Variables - `KMS_TYPE`: KMS provider type ("awskms", "gcpkms", "azurekms", "hashivault") + - `ROOT_KEY_ID`: Key identifier for root certificate - `KMS_INTERMEDIATE_KEY_ID`: Key identifier for intermediate certificate - `LEAF_KEY_ID`: Key identifier for leaf certificate + - `AWS_REGION`: AWS Region (required for AWS KMS) + - `AZURE_TENANT_ID`: Azure tenant ID + - `GCP_CREDENTIALS_FILE`: Path to credentials file (for Google Cloud KMS) + - `VAULT_ADDR`: HashiCorp Vault address - `VAULT_TOKEN`: HashiCorp Vault token diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go index 745c2408f..8d0f526eb 100644 --- a/pkg/certmaker/certmaker.go +++ b/pkg/certmaker/certmaker.go @@ -79,7 +79,7 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif case "gcpkms": ref := fmt.Sprintf("gcpkms://%s", config.RootKeyID) if gcpCredsFile := config.Options["gcp-credentials-file"]; gcpCredsFile != "" { - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", gcpCredsFile) + os.Setenv("GCP_CREDENTIALS_FILE", gcpCredsFile) } sv, err = kms.Get(ctx, ref, crypto.SHA256) if err != nil { @@ -270,6 +270,12 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, // WriteCertificateToFile writes an X.509 certificate to a PEM-encoded file func WriteCertificateToFile(cert *x509.Certificate, filename string) error { + if cert == nil { + return fmt.Errorf("certificate cannot be nil") + } + if len(cert.Raw) == 0 { + return fmt.Errorf("certificate has no raw data") + } certPEM := &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index 26318645a..b54f17db9 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" + "errors" "fmt" "io" "math/big" @@ -39,10 +40,11 @@ import ( // mockSignerVerifier implements signature.SignerVerifier for testing type mockSignerVerifier struct { - key crypto.PrivateKey - err error - publicKeyFunc func() (crypto.PublicKey, error) - signMessageFunc func(message io.Reader, opts ...signature.SignOption) ([]byte, error) + key crypto.PrivateKey + err error + publicKeyFunc func() (crypto.PublicKey, error) + signMessageFunc func(message io.Reader, opts ...signature.SignOption) ([]byte, error) + cryptoSignerFunc func(ctx context.Context, errHandler func(error)) (crypto.Signer, crypto.SignerOpts, error) } func (m *mockSignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) { @@ -65,7 +67,7 @@ func (m *mockSignerVerifier) SignMessage(message io.Reader, opts ...signature.Si } func (m *mockSignerVerifier) VerifySignature(_, _ io.Reader, _ ...signature.VerifyOption) error { - return nil + return m.err } func (m *mockSignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) { @@ -75,39 +77,37 @@ func (m *mockSignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.P if m.err != nil { return nil, m.err } + if m.key == nil { + return nil, fmt.Errorf("no key available") + } switch k := m.key.(type) { case *ecdsa.PrivateKey: - return k.Public(), nil + return &k.PublicKey, nil default: return nil, fmt.Errorf("unsupported key type") } } -func (m *mockSignerVerifier) Close() error { - return nil -} +func (m *mockSignerVerifier) Close() error { return nil } -func (m *mockSignerVerifier) DefaultHashFunction() crypto.Hash { - return crypto.SHA256 -} +func (m *mockSignerVerifier) DefaultHashFunction() crypto.Hash { return crypto.SHA256 } -func (m *mockSignerVerifier) Bytes() ([]byte, error) { - return nil, fmt.Errorf("not implemented") -} +func (m *mockSignerVerifier) Bytes() ([]byte, error) { return nil, nil } -func (m *mockSignerVerifier) KeyID() (string, error) { - return "mock-key-id", nil -} +func (m *mockSignerVerifier) KeyID() (string, error) { return "", nil } -func (m *mockSignerVerifier) Status() error { - return nil -} +func (m *mockSignerVerifier) Status() error { return nil } -func (m *mockSignerVerifier) CryptoSigner(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { +func (m *mockSignerVerifier) CryptoSigner(ctx context.Context, errHandler func(error)) (crypto.Signer, crypto.SignerOpts, error) { + if m.cryptoSignerFunc != nil { + return m.cryptoSignerFunc(ctx, errHandler) + } if m.err != nil { return nil, nil, m.err } - + if m.key == nil { + return nil, nil, fmt.Errorf("no key available") + } switch k := m.key.(type) { case *ecdsa.PrivateKey: return k, crypto.SHA256, nil @@ -314,1027 +314,519 @@ func TestValidateTemplatePath(t *testing.T) { } func TestCreateCertificates(t *testing.T) { - defer func() { originalInitKMS = InitKMS }() + 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}, nil + } + + 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" + }, + "certLife": "", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0600) + require.NoError(t, err) + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "4380h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning", "TimeStamping"] + }` + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0600) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{} + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + + require.Error(t, err) + assert.Contains(t, err.Error(), "error parsing root template: certLife must be specified") +} +func TestInitKMS(t *testing.T) { tests := []struct { - name string - config KMSConfig - rootTemplatePath string - leafTemplatePath string - rootCertPath string - leafCertPath string - intermediateKeyID string - intermediateTemplatePath string - intermediateCertPath string - setupMockSigner func() signature.SignerVerifier - wantError string + name string + config KMSConfig + wantError bool }{ { - name: "leaf_key_initialization_error", + name: "empty_KMS_type", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "invalid-key", + RootKeyID: "test-key", }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - originalKMS := InitKMS - InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { - if strings.Contains(config.LeafKeyID, "invalid-key") { - return nil, fmt.Errorf("error initializing leaf KMS") - } - return &mockSignerVerifier{key: key}, nil - } - t.Cleanup(func() { - InitKMS = originalKMS - }) - return &mockSignerVerifier{key: key} + wantError: true, + }, + { + name: "missing_key_IDs", + config: KMSConfig{ + Type: "awskms", + Options: map[string]string{"region": "us-west-2"}, }, - wantError: "error initializing leaf KMS", + wantError: true, }, { - name: "leaf_public_key_error", + name: "AWS_KMS_missing_region", config: KMSConfig{ Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - originalKMS := InitKMS - InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { - if strings.Contains(config.LeafKeyID, "leaf-key") { - return &mockSignerVerifier{ - key: key, - err: fmt.Errorf("error getting leaf public key"), - }, nil - } - return &mockSignerVerifier{key: key}, nil - } - t.Cleanup(func() { - InitKMS = originalKMS - }) - return &mockSignerVerifier{key: key} + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", }, - wantError: "error getting leaf public key", + wantError: true, }, { - name: "intermediate_key_initialization_error", + name: "Azure_KMS_missing_tenant_ID", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - IntermediateKeyID: "invalid-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - intermediateTemplatePath: func(t *testing.T) string { - intermediateTemplate := filepath.Join(t.TempDir(), "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 intermediateTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - intermediateKeyID: "invalid-key", - intermediateCertPath: filepath.Join(t.TempDir(), "intermediate.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - originalKMS := InitKMS - InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { - if strings.Contains(config.IntermediateKeyID, "invalid-key") { - return nil, fmt.Errorf("error initializing intermediate KMS") - } - return &mockSignerVerifier{key: key}, nil - } - t.Cleanup(func() { - InitKMS = originalKMS - }) - return &mockSignerVerifier{key: key} + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + Options: map[string]string{}, }, - wantError: "error initializing intermediate KMS", + wantError: true, }, { - name: "intermediate_public_key_error", + name: "Azure_KMS_missing_vault_parameter", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - IntermediateKeyID: "alias/intermediate-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - intermediateTemplatePath: func(t *testing.T) string { - intermediateTemplate := filepath.Join(t.TempDir(), "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 intermediateTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - intermediateKeyID: "alias/intermediate-key", - intermediateCertPath: filepath.Join(t.TempDir(), "intermediate.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - originalKMS := InitKMS - InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { - if strings.Contains(config.IntermediateKeyID, "intermediate-key") { - return &mockSignerVerifier{ - key: key, - err: fmt.Errorf("error getting intermediate public key"), - }, nil - } - return &mockSignerVerifier{key: key}, nil - } - t.Cleanup(func() { - InitKMS = originalKMS - }) - return &mockSignerVerifier{key: key} + Type: "azurekms", + RootKeyID: "azurekms:name=test-key", + Options: map[string]string{ + "azure-tenant-id": "test-tenant", + }, }, - wantError: "error getting intermediate public key", + wantError: true, }, { - name: "invalid_leaf_template", + name: "unsupported_KMS_type", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: "/nonexistent/leaf.json", - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return &mockSignerVerifier{key: key} + Type: "unsupported", + RootKeyID: "test-key", }, - wantError: "error parsing leaf template: error reading template file", + wantError: true, }, { - name: "successful_certificate_creation", + name: "aws_kms_valid_config", config: KMSConfig{ Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", + Options: map[string]string{"aws-region": "us-west-2"}, }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return &mockSignerVerifier{ - key: key, - err: fmt.Errorf("error getting leaf public key: getting public key: operation error KMS: GetPublicKey, get identity: get credentials: failed to refresh cached credentials, no EC2 IMDS role found"), - publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil - }, - } + wantError: true, + }, + { + name: "azure_kms_valid_config", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + LeafKeyID: "azurekms:name=test-leaf-key;vault=test-vault", + Options: map[string]string{ + "azure-tenant-id": "test-tenant", + }, }, - wantError: "error getting leaf public key: getting public key: operation error KMS: GetPublicKey, get identity: get credentials: failed to refresh cached credentials, no EC2 IMDS role found", + wantError: false, }, { - name: "invalid_template_path", + name: "gcp_kms_valid_config", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "awskms:///arn:aws:kms:us-west-2:123456789012:key/root-key", - LeafKeyID: "awskms:///arn:aws:kms:us-west-2:123456789012:key/leaf-key", + Type: "gcpkms", + RootKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", + LeafKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-leaf-key/cryptoKeyVersions/1", }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: "/nonexistent/leaf.json", - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return &mockSignerVerifier{key: key} + wantError: false, + }, + { + name: "hashivault_kms_valid_config", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "transit/keys/my-key", + Options: map[string]string{ + "vault-token": "test-token", + "vault-address": "http://vault:8200", + }, }, - wantError: "error parsing leaf template: error reading template file", + wantError: true, }, { - name: "invalid_root_template_path", + name: "aws_kms_nil_signer", config: KMSConfig{ Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: "/nonexistent/root.json", - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - return &mockSignerVerifier{ - key: nil, - err: fmt.Errorf("no such file or directory"), - } + Options: map[string]string{"aws-region": "us-west-2"}, + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", }, - wantError: "error parsing root template: error reading template file", + wantError: true, }, { - name: "root_cert_write_error", + name: "aws_kms_with_endpoint", config: KMSConfig{ Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: "/nonexistent/directory/root.crt", - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return &mockSignerVerifier{ - key: key, - publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil - }, - } + RootKeyID: "alias/test-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, }, - wantError: "failed to create file", + wantError: false, }, { - name: "leaf_cert_write_error", + name: "aws_kms_with_alias", config: KMSConfig{ Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: "/nonexistent/directory/leaf.crt", - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return &mockSignerVerifier{ - key: key, - publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil - }, - } + RootKeyID: "alias/test-key", + LeafKeyID: "alias/test-leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, }, - wantError: "failed to create file", + wantError: false, }, { - name: "signing_error", + name: "aws_kms_with_arn", config: KMSConfig{ Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - return &mockSignerVerifier{ - key: nil, - err: fmt.Errorf("signing error"), - publicKeyFunc: func() (crypto.PublicKey, error) { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return key.Public(), nil - }, - } + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", + Options: map[string]string{"aws-region": "us-west-2"}, }, - wantError: "signing error", + wantError: true, }, { - name: "intermediate_cert_write_error", + name: "gcp_kms_with_cryptoKeyVersions", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - IntermediateKeyID: "alias/intermediate-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - intermediateTemplatePath: func(t *testing.T) string { - intermediateTemplate := filepath.Join(t.TempDir(), "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 intermediateTemplate - }(t), - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - intermediateKeyID: "alias/intermediate-key", - intermediateCertPath: filepath.Join(t.TempDir(), "intermediate.crt"), - setupMockSigner: func() signature.SignerVerifier { - return &mockSignerVerifier{ - key: nil, - publicKeyFunc: func() (crypto.PublicKey, error) { - return nil, fmt.Errorf("error writing intermediate certificate") - }, - } + Type: "gcpkms", + RootKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", + LeafKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-leaf-key/cryptoKeyVersions/1", }, - wantError: "error writing intermediate certificate", + wantError: false, }, { - name: "invalid_cert_path", + name: "hashivault_kms_with_transit_keys", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", - }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: func(t *testing.T) string { - leafTemplate := filepath.Join(t.TempDir(), "leaf.json") - err := os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["CodeSigning"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) - require.NoError(t, err) - return leafTemplate - }(t), - rootCertPath: "/nonexistent/directory/root.crt", - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - return &mockSignerVerifier{key: key} + Type: "hashivault", + RootKeyID: "transit/keys/test-key", + Options: map[string]string{ + "vault-token": "test-token", + "vault-address": "http://vault:8200", + }, }, - wantError: "failed to create file", + wantError: true, }, { - name: "invalid_leaf_template_path#01", + name: "gcp_kms_with_uri", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", + Type: "gcpkms", + RootKeyID: "gcpkms://projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", }, - rootTemplatePath: func(t *testing.T) string { - rootTemplate := filepath.Join(t.TempDir(), "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) - return rootTemplate - }(t), - leafTemplatePath: "/nonexistent/leaf.json", - rootCertPath: filepath.Join(t.TempDir(), "root.crt"), - leafCertPath: filepath.Join(t.TempDir(), "leaf.crt"), - setupMockSigner: func() signature.SignerVerifier { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - originalKMS := InitKMS - InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { - if strings.Contains(config.LeafKeyID, "leaf-key") { - return &mockSignerVerifier{ - key: key, - publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil - }, - signMessageFunc: func(message io.Reader, _ ...signature.SignOption) ([]byte, error) { - digest := make([]byte, 32) - if _, err := message.Read(digest); err != nil { - return nil, err - } - return key.Sign(rand.Reader, digest, crypto.SHA256) - }, - }, nil - } - return &mockSignerVerifier{ - key: key, - publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil - }, - signMessageFunc: func(message io.Reader, _ ...signature.SignOption) ([]byte, error) { - digest := make([]byte, 32) - if _, err := message.Read(digest); err != nil { - return nil, err - } - return key.Sign(rand.Reader, digest, crypto.SHA256) - }, - }, nil - } - t.Cleanup(func() { - InitKMS = originalKMS - }) - return &mockSignerVerifier{ - key: key, - publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil - }, - signMessageFunc: func(message io.Reader, _ ...signature.SignOption) ([]byte, error) { - digest := make([]byte, 32) - if _, err := message.Read(digest); err != nil { - return nil, err - } - return key.Sign(rand.Reader, digest, crypto.SHA256) - }, - } + wantError: true, + }, + { + name: "hashivault_kms_with_uri", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "hashivault://transit/keys/test-key", + Options: map[string]string{ + "vault-token": "test-token", + "vault-address": "http://vault:8200", + }, }, - wantError: "error parsing leaf template: error reading template file", + wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := CreateCertificates(tt.setupMockSigner(), tt.config, - tt.rootTemplatePath, tt.leafTemplatePath, - tt.rootCertPath, tt.leafCertPath, - tt.intermediateKeyID, tt.intermediateTemplatePath, tt.intermediateCertPath) - - if tt.wantError != "" { + _, err := InitKMS(context.Background(), tt.config) + if tt.wantError { require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) } else { require.NoError(t, err) - _, err = os.Stat(tt.rootCertPath) - require.NoError(t, err) - _, err = os.Stat(tt.leafCertPath) - require.NoError(t, err) - if tt.intermediateKeyID != "" { - _, err = os.Stat(tt.intermediateCertPath) - require.NoError(t, err) - } } }) } } -func TestInitKMS(t *testing.T) { +type Subject struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` +} + +type Issuer struct { + CommonName string `json:"commonName"` +} + +type BasicConstraints struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` +} + +func TestValidateTemplate(t *testing.T) { tests := []struct { name string - config KMSConfig - wantError bool + template *CertificateTemplate + parent *x509.Certificate + certType string + wantError string }{ { - name: "empty_KMS_type", - config: KMSConfig{ - RootKeyID: "test-key", - }, - wantError: true, - }, - { - name: "missing_key_IDs", - config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"region": "us-west-2"}, - }, - wantError: true, - }, - { - name: "AWS_KMS_missing_region", - config: KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - }, - wantError: true, - }, - { - name: "Azure_KMS_missing_tenant_ID", - config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=test-key;vault=test-vault", - Options: map[string]string{}, - }, - wantError: true, - }, - { - name: "Azure_KMS_missing_vault_parameter", - config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=test-key", - Options: map[string]string{ - "azure-tenant-id": "test-tenant", + name: "valid_template_with_duration-based_validity", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Root CA", }, - }, - wantError: true, - }, - { - name: "unsupported_KMS_type", - config: KMSConfig{ - Type: "unsupported", - RootKeyID: "test-key", - }, - wantError: true, - }, - { - name: "aws_kms_valid_config", - config: KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - Options: map[string]string{"aws-region": "us-west-2"}, - }, - wantError: true, - }, - { - name: "azure_kms_valid_config", - config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=test-key;vault=test-vault", - LeafKeyID: "azurekms:name=test-leaf-key;vault=test-vault", - Options: map[string]string{ - "azure-tenant-id": "test-tenant", + Issuer: Issuer{ + CommonName: "Test Root CA", }, - }, - wantError: false, - }, - { - name: "gcp_kms_valid_config", - config: KMSConfig{ - Type: "gcpkms", - RootKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", - LeafKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-leaf-key/cryptoKeyVersions/1", - }, - wantError: false, - }, - { - name: "hashivault_kms_valid_config", - config: KMSConfig{ - Type: "hashivault", - RootKeyID: "transit/keys/my-key", - Options: map[string]string{ - "vault-token": "test-token", - "vault-address": "http://vault:8200", + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: BasicConstraints{ + IsCA: true, + MaxPathLen: 1, }, }, - wantError: true, - }, - { - name: "aws_kms_nil_signer", - config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"aws-region": "us-west-2"}, - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - }, - wantError: true, - }, - { - name: "aws_kms_with_endpoint", - config: KMSConfig{ - Type: "awskms", - RootKeyID: "alias/test-key", - LeafKeyID: "alias/test-leaf-key", - Options: map[string]string{"aws-region": "us-west-2"}, - }, - wantError: false, + parent: nil, + certType: "root", + wantError: "", }, { - name: "aws_kms_with_alias", - config: KMSConfig{ - Type: "awskms", - RootKeyID: "alias/test-key", - LeafKeyID: "alias/test-leaf-key", - Options: map[string]string{"aws-region": "us-west-2"}, + name: "invalid_extended_key_usage", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"invalid"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, }, - wantError: false, - }, - { - name: "aws_kms_with_arn", - config: KMSConfig{ - Type: "awskms", - RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/test-key", - Options: map[string]string{"aws-region": "us-west-2"}, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), }, - wantError: true, + certType: "leaf", + wantError: "Fulcio leaf certificates must have codeSign extended key usage", }, { - name: "gcp_kms_with_cryptoKeyVersions", - config: KMSConfig{ - Type: "gcpkms", - RootKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", - LeafKeyID: "projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-leaf-key/cryptoKeyVersions/1", + name: "invalid_duration_format", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "invalid", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, }, - wantError: false, - }, - { - name: "hashivault_kms_with_transit_keys", - config: KMSConfig{ - Type: "hashivault", - RootKeyID: "transit/keys/test-key", - Options: map[string]string{ - "vault-token": "test-token", - "vault-address": "http://vault:8200", + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), }, - wantError: true, + certType: "leaf", + wantError: "invalid certLife format: time: invalid duration \"invalid\"", }, { - name: "gcp_kms_with_uri", - config: KMSConfig{ - Type: "gcpkms", - RootKeyID: "gcpkms://projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", + name: "negative_duration", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "-8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, }, - wantError: true, - }, - { - name: "hashivault_kms_with_uri", - config: KMSConfig{ - Type: "hashivault", - RootKeyID: "hashivault://transit/keys/test-key", - Options: map[string]string{ - "vault-token": "test-token", - "vault-address": "http://vault:8200", + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), }, - wantError: true, + certType: "leaf", + wantError: "certLife must be positive", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := InitKMS(context.Background(), tt.config) - if tt.wantError { - require.Error(t, err) - } else { + err := ValidateTemplate(tt.template, tt.parent, tt.certType) + if tt.wantError == "" { require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) } }) } } -func TestValidateTemplateWithExtKeyUsage(t *testing.T) { +func TestValidateTemplateWithValidFields(t *testing.T) { 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", - }, - Issuer: struct { - CommonName string `json:"commonName"` - }{ - CommonName: "Test CA", - }, - KeyUsage: []string{"certSign", "crlSign"}, - ExtKeyUsage: []string{"serverAuth", "clientAuth"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{ + Subject: Subject{ + CommonName: "Test Root CA", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: BasicConstraints{ IsCA: true, MaxPathLen: 1, }, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - } - - parent := &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Parent CA", - }, - NotBefore: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), } - err := ValidateTemplate(template, parent, "root") + err := ValidateTemplate(template, nil, "root") require.NoError(t, err) } -func TestValidateTemplateWithInvalidExtKeyUsage(t *testing.T) { +func TestValidateTemplateWithDurationBasedValidity(t *testing.T) { 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", + Subject: Subject{ + CommonName: "Test Root CA", }, - Issuer: struct { - CommonName string `json:"commonName"` - }{ - CommonName: "Test CA", - }, - KeyUsage: []string{"digitalSignature"}, - ExtKeyUsage: []string{"nonExistentUsage", "anotherInvalidUsage"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{ + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: BasicConstraints{ + IsCA: true, + MaxPathLen: 1, + }, + } + + err := ValidateTemplate(template, nil, "root") + require.NoError(t, err) +} + +func TestValidateTemplateWithInvalidExtKeyUsage(t *testing.T) { + template := &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"invalid"}, + BasicConstraints: BasicConstraints{ IsCA: false, }, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", } parent := &x509.Certificate{ Subject: pkix.Name{ - CommonName: "Parent CA", + CommonName: "Test Root CA", }, - NotBefore: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), } err := ValidateTemplate(template, parent, "leaf") require.Error(t, err) - assert.Contains(t, err.Error(), "must have codeSign extended key usage") + assert.Contains(t, err.Error(), "Fulcio leaf certificates must have codeSign extended key usage") +} - template.ExtKeyUsage = append(template.ExtKeyUsage, "CodeSigning") - err = ValidateTemplate(template, parent, "leaf") - require.NoError(t, err) +func TestValidateTemplateWithInvalidTimestamps(t *testing.T) { + template := &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "invalid", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, + } + + parent := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + } + + err := ValidateTemplate(template, parent, "leaf") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid certLife format: time: invalid duration \"invalid\"") +} + +func TestValidateTemplateWithInvalidTimestampOrder(t *testing.T) { + template := &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "-8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, + } + + parent := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + } + + err := ValidateTemplate(template, parent, "leaf") + require.Error(t, err) + assert.Contains(t, err.Error(), "certLife must be positive") } func TestWriteCertificateToFile(t *testing.T) { @@ -1346,197 +838,1170 @@ func TestWriteCertificateToFile(t *testing.T) { wantType string }{ { - name: "write_to_nonexistent_directory", - cert: &x509.Certificate{ - Raw: []byte("test"), - Subject: pkix.Name{ - CommonName: "Test CA", + name: "write_to_nonexistent_directory", + cert: &x509.Certificate{ + Raw: []byte("test"), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + IsCA: true, + }, + path: "/nonexistent/directory/cert.crt", + wantError: "failed to create file", + }, + { + name: "write_to_readonly_directory", + cert: &x509.Certificate{ + Raw: []byte("test"), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + IsCA: true, + }, + path: filepath.Join(os.TempDir(), "readonly", "cert.crt"), + wantError: "failed to create file", + }, + { + name: "write_root_certificate", + cert: &x509.Certificate{ + Raw: []byte("test"), + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + IsCA: true, + MaxPathLen: 1, + }, + path: filepath.Join(os.TempDir(), "root.crt"), + wantType: "root", + }, + { + name: "write_intermediate_certificate", + cert: &x509.Certificate{ + Raw: []byte("test"), + Subject: pkix.Name{ + CommonName: "Test Intermediate CA", + }, + IsCA: true, + MaxPathLen: 0, + }, + path: filepath.Join(os.TempDir(), "intermediate.crt"), + wantType: "intermediate", + }, + { + name: "write_leaf_certificate", + cert: &x509.Certificate{ + Raw: []byte("test"), + Subject: pkix.Name{ + CommonName: "Test Leaf", + }, + IsCA: false, + }, + path: filepath.Join(os.TempDir(), "leaf.crt"), + wantType: "leaf", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if strings.Contains(tt.name, "readonly") { + dir := filepath.Dir(tt.path) + err := os.MkdirAll(dir, 0444) + require.NoError(t, err) + defer os.RemoveAll(dir) + } + + err := WriteCertificateToFile(tt.cert, tt.path) + if tt.wantError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + } else { + require.NoError(t, err) + _, err = os.Stat(tt.path) + require.NoError(t, err) + if tt.wantType != "" { + assert.Contains(t, tt.path, tt.wantType) + } + os.Remove(tt.path) + } + }) + } +} + +func TestWriteCertificateToFileErrors(t *testing.T) { + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test Cert", + }, + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + cert, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + parsedCert, err := x509.ParseCertificate(cert) + require.NoError(t, err) + + tests := []struct { + name string + setup func(t *testing.T) string + wantError string + }{ + { + name: "directory_exists_as_file", + setup: func(t *testing.T) string { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "cert.crt") + err := os.MkdirAll(path, 0755) + require.NoError(t, err) + return path + }, + wantError: "failed to create file", + }, + { + name: "permission_denied", + setup: func(t *testing.T) string { + tmpDir := t.TempDir() + err := os.Chmod(tmpDir, 0000) + require.NoError(t, err) + return filepath.Join(tmpDir, "cert.crt") + }, + wantError: "permission denied", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := tt.setup(t) + err := WriteCertificateToFile(parsedCert, path) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + }) + } +} + +func TestCreateCertificatesTemplateValidation(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") + rootCertPath := filepath.Join(tmpDir, "root.pem") + leafCertPath := filepath.Join(tmpDir, "leaf.pem") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.pem") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0600) + require.NoError(t, err) + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "certLife": "4380h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning", "TimeStamping"] + }` + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0600) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{} + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", intermediateTmplPath, intermediateCertPath) + + require.Error(t, err) + assert.Contains(t, err.Error(), "error parsing root template: certLife must be specified") +} + +func TestCreateCertificatesWithInvalidLeafTemplate(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) + wantError string + }{ + { + name: "missing_timeStamping_extKeyUsage", + setup: func(t *testing.T) (string, string, string, string, KMSConfig, signature.SignerVerifier) { + tmpDir := t.TempDir() + + 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" + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z", + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2024-12-31T23:59:59Z", + "certLife": "8760h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning"] + }` + + 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, _ ...signature.SignOption) ([]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) + }, + } + + 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: "certificate notAfter time cannot be after parent's notAfter time", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + return &mockSignerVerifier{err: errors.New("test error")}, nil + } + + 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 TestCreateCertificatesWithInvalidIntermediateKey(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if strings.Contains(config.IntermediateKeyID, "invalid-key") { + return nil, fmt.Errorf("test error") + } + _, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + return &mockSignerVerifier{err: errors.New("test error")}, nil + } + + 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") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.crt") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + intermediateTemplate := `{ + "subject": { + "commonName": "Test Intermediate CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Intermediate CA" + }, + "certLife": "8760h", + "keyUsage": ["digitalSignature"], + "extensions": [ + { + "id": "2.5.29.37", + "critical": true, + "value": "MCQwIgYDVR0lBBswGQYIKwYBBQUHAwgGDSsGAQQBgjcUAgICAf8=" + } + ] + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(intermediateTmplPath, []byte(intermediateTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + _, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + err: errors.New("test error"), + } + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + IntermediateKeyID: "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"}, + } + + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "invalid-key", intermediateTmplPath, intermediateCertPath) + require.Error(t, err) + require.Contains(t, err.Error(), "error getting root public key: test error") +} + +func TestCreateCertificatesWithIntermediateErrors(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.pem") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + intermediateTemplate := `{ + "subject": { + "commonName": "Test Intermediate CA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(intermediateTmplPath, []byte(intermediateTemplate), 0644) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + err: errors.New("test error"), + } + + kmsConfig := KMSConfig{ + Type: "mock", + RootKeyID: "root-key", + IntermediateKeyID: "intermediate-key", + LeafKeyID: "leaf-key", + } + + err = CreateCertificates(mockSigner, kmsConfig, rootTmplPath, "", "", "", "intermediate-key", intermediateTmplPath, intermediateCertPath) + require.Error(t, err) + assert.Contains(t, err.Error(), "test error") +} + +func TestValidateTemplateWithDurationAndExtKeyUsage(t *testing.T) { + tests := []struct { + name string + template *CertificateTemplate + parent *x509.Certificate + certType string + wantError string + }{ + { + name: "valid_template_with_duration-based_validity", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Root CA", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: BasicConstraints{ + IsCA: true, + MaxPathLen: 1, + }, + }, + parent: nil, + certType: "root", + wantError: "", + }, + { + name: "invalid_extended_key_usage", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"invalid"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + }, + certType: "leaf", + wantError: "Fulcio leaf certificates must have codeSign extended key usage", + }, + { + name: "invalid_duration_format", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "invalid", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + }, + certType: "leaf", + wantError: "invalid certLife format: time: invalid duration \"invalid\"", + }, + { + name: "negative_duration", + template: &CertificateTemplate{ + Subject: Subject{ + CommonName: "Test Leaf", + }, + Issuer: Issuer{ + CommonName: "Test Root CA", + }, + CertLifetime: "-8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: BasicConstraints{ + IsCA: false, + }, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + }, + certType: "leaf", + wantError: "certLife must be positive", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateTemplate(tt.template, tt.parent, tt.certType) + if tt.wantError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + } + }) + } +} + +func TestCreateCertificatesWithoutIntermediate(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + return &mockSignerVerifier{err: errors.New("test error")}, nil + } + + 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" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test TSA" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "2190h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning", "TimeStamping"] + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + _, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + err: errors.New("test 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"}, + } + + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), "test error") +} + +func TestCreateCertificatesLeafErrors(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + return &mockSignerVerifier{err: errors.New("test error")}, nil + } + + 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" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte("invalid json"), 0644) + require.NoError(t, err) + + _, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ + err: errors.New("test 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"}, + } + + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + require.Contains(t, err.Error(), "test error") +} + +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-leaf-key", + }, + wantError: "aws-region is required for AWS KMS", + }, + { + name: "gcp_kms_invalid_key_format", + config: KMSConfig{ + Type: "gcpkms", + RootKeyID: "invalid-key-format", + LeafKeyID: "projects/test/locations/global/keyRings/test/cryptoKeys/test/cryptoKeyVersions/1", + Options: map[string]string{ + "gcp-credentials-file": "/path/to/creds.json", }, - IsCA: true, }, - path: "/nonexistent/directory/cert.crt", - wantError: "failed to create file", + wantError: "gcpkms RootKeyID must start with 'projects/'", }, { - name: "write_to_readonly_directory", - cert: &x509.Certificate{ - Raw: []byte("test"), - Subject: pkix.Name{ - CommonName: "Test CA", - }, - IsCA: true, + name: "azure_kms_missing_tenant_id", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=test-key;vault=test-vault", + LeafKeyID: "azurekms:name=test-leaf-key;vault=test-vault", + Options: map[string]string{}, }, - path: filepath.Join(os.TempDir(), "readonly", "cert.crt"), - wantError: "failed to create file", + wantError: "azure-tenant-id is required for Azure KMS", }, { - name: "write_root_certificate", - cert: &x509.Certificate{ - Raw: []byte("test"), - Subject: pkix.Name{ - CommonName: "Test Root CA", + name: "hashivault_kms_missing_token", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "transit/keys/test-key", + LeafKeyID: "transit/keys/test-leaf-key", + Options: map[string]string{ + "vault-address": "http://vault:8200", }, - IsCA: true, - MaxPathLen: 1, }, - path: filepath.Join(os.TempDir(), "root.crt"), - wantType: "root", + wantError: "vault-token is required for HashiVault KMS", }, { - name: "write_intermediate_certificate", - cert: &x509.Certificate{ - Raw: []byte("test"), - Subject: pkix.Name{ - CommonName: "Test Intermediate CA", + name: "unsupported_kms_type", + config: KMSConfig{ + Type: "unsupported", + RootKeyID: "test-key", + LeafKeyID: "test-leaf-key", + }, + wantError: "unsupported KMS type: unsupported", + }, + { + name: "azure_kms_invalid_key_format", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "invalid-format", + LeafKeyID: "azurekms:name=test-leaf-key;vault=test-vault", + Options: map[string]string{ + "azure-tenant-id": "test-tenant", }, - IsCA: true, - MaxPathLen: 0, }, - path: filepath.Join(os.TempDir(), "intermediate.crt"), - wantType: "intermediate", + wantError: "azurekms RootKeyID must start with 'azurekms:name='", }, { - name: "write_leaf_certificate", - cert: &x509.Certificate{ - Raw: []byte("test"), - Subject: pkix.Name{ - CommonName: "Test Leaf", + name: "hashivault_kms_invalid_key_format", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "invalid/format", + LeafKeyID: "transit/keys/test-leaf-key", + Options: map[string]string{ + "vault-token": "test-token", + "vault-address": "http://vault:8200", }, - IsCA: false, }, - path: filepath.Join(os.TempDir(), "leaf.crt"), - wantType: "leaf", + wantError: "hashivault RootKeyID must be in format: transit/keys/keyname", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if strings.Contains(tt.name, "readonly") { - dir := filepath.Dir(tt.path) - err := os.MkdirAll(dir, 0444) - require.NoError(t, err) - defer os.RemoveAll(dir) - } - - err := WriteCertificateToFile(tt.cert, tt.path) - if tt.wantError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - } else { - require.NoError(t, err) - _, err = os.Stat(tt.path) - require.NoError(t, err) - if tt.wantType != "" { - assert.Contains(t, tt.path, tt.wantType) - } - os.Remove(tt.path) - } + err := ValidateKMSConfig(tt.config) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) }) } } -func TestWriteCertificateToFileErrors(t *testing.T) { - template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: "Test Cert", - }, - } - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - cert, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) - require.NoError(t, err) - - parsedCert, err := x509.ParseCertificate(cert) - require.NoError(t, err) - +func TestWriteCertificateToFileAdditionalErrors(t *testing.T) { tests := []struct { name string - setup func(t *testing.T) string + cert *x509.Certificate + filename string wantError string }{ { - name: "directory_exists_as_file", - setup: func(t *testing.T) string { - tmpDir := t.TempDir() - path := filepath.Join(tmpDir, "cert.crt") - err := os.MkdirAll(path, 0755) - require.NoError(t, err) - return path + name: "invalid_directory", + cert: &x509.Certificate{ + Raw: []byte("test"), + IsCA: true, }, + filename: "/nonexistent/directory/cert.pem", wantError: "failed to create file", }, { - name: "permission_denied", - setup: func(t *testing.T) string { - tmpDir := t.TempDir() - err := os.Chmod(tmpDir, 0000) - require.NoError(t, err) - return filepath.Join(tmpDir, "cert.crt") + name: "nil_certificate", + cert: nil, + filename: "test.pem", + wantError: "certificate cannot be nil", + }, + { + name: "empty_raw_data", + cert: &x509.Certificate{ + IsCA: true, }, - wantError: "permission denied", + filename: "test.pem", + wantError: "certificate has no raw data", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - path := tt.setup(t) - err := WriteCertificateToFile(parsedCert, path) + err := WriteCertificateToFile(tt.cert, tt.filename) require.Error(t, err) assert.Contains(t, err.Error(), tt.wantError) }) } } -func TestCreateCertificatesTemplateValidation(t *testing.T) { - tmpDir := t.TempDir() +func TestCreateCertificatesCreationFailure(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + 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") + }, + } + + 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" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "24h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning", "TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` - rootTemplate := filepath.Join(tmpDir, "root.json") - err := os.WriteFile(rootTemplate, []byte(`{ - "subject": {"commonName": "Test Root CA"}, - "issuer": {"commonName": "Test Root CA"}, + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "error getting root crypto signer: crypto signer error") +} + +func TestCreateCertificatesSuccessPath(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + 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 key, crypto.SHA256, nil + }, + } + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.RootKeyID == "root-key" { + return mockSigner, nil + } + if config.IntermediateKeyID == "intermediate-key" { + return mockSigner, nil + } + if config.LeafKeyID == "leaf-key" { + return mockSigner, nil + } + return nil, fmt.Errorf("unexpected key ID") + } + + 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") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.crt") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + leafCertPath := filepath.Join(tmpDir, "leaf.crt") + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA", + "organization": ["Test Org"], + "country": ["US"] + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + intermediateTemplate := `{ + "subject": { + "commonName": "Test Intermediate CA", + "organization": ["Test Org"], + "country": ["US"] + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "4380h", "keyUsage": ["certSign", "crlSign"], - "basicConstraints": {"isCA": true, "maxPathLen": 1}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf", + "organization": ["Test Org"], + "country": ["US"] + }, + "issuer": { + "commonName": "Test Intermediate CA" + }, + "certLife": "24h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning", "TimeStamping"], + "basicConstraints": { + "isCA": false + } + }` + + err = os.WriteFile(rootTmplPath, []byte(rootTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(intermediateTmplPath, []byte(intermediateTemplate), 0644) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0644) + require.NoError(t, err) + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "root-key", + IntermediateKeyID: "intermediate-key", + LeafKeyID: "leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + } + + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "intermediate-key", intermediateTmplPath, intermediateCertPath) + require.NoError(t, err) + + _, err = os.Stat(rootCertPath) + require.NoError(t, err) + _, err = os.Stat(intermediateCertPath) + require.NoError(t, err) + _, err = os.Stat(leafCertPath) + require.NoError(t, err) +} + +func TestCreateCertificatesInvalidSigner(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + mockSigner := &mockSignerVerifier{ + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("signer does not implement CryptoSigner") + }, + cryptoSignerFunc: nil, + } + + 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" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` - leafTemplate := filepath.Join(tmpDir, "leaf.json") - err = os.WriteFile(leafTemplate, []byte(`{ - "subject": {"commonName": "Test Leaf"}, + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "24h", "keyUsage": ["digitalSignature"], - "basicConstraints": {"isCA": false}, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`), 0600) + "extKeyUsage": ["CodeSigning", "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) config := KMSConfig{ Type: "awskms", - RootKeyID: "alias/root-key", - LeafKeyID: "alias/leaf-key", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", Options: map[string]string{"aws-region": "us-west-2"}, } - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - sv := &mockSignerVerifier{ + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "signer does not implement CryptoSigner") +} + +func TestCreateCertificatesCryptoSignerFailure(t *testing.T) { + defer func() { InitKMS = originalInitKMS }() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner := &mockSignerVerifier{ key: key, publicKeyFunc: func() (crypto.PublicKey, error) { - return key.Public(), nil + return &key.PublicKey, nil + }, + cryptoSignerFunc: func(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { + return nil, nil, fmt.Errorf("crypto signer error") }, } - err = CreateCertificates(sv, config, - rootTemplate, - leafTemplate, - filepath.Join(tmpDir, "root.crt"), - filepath.Join(tmpDir, "leaf.crt"), - "", - "", - "") + 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" + }, + "certLife": "8760h", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "certLife": "24h", + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning", "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) + + config := KMSConfig{ + Type: "awskms", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + Options: map[string]string{"aws-region": "us-west-2"}, + } + err = CreateCertificates(mockSigner, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "") require.Error(t, err) - assert.Contains(t, err.Error(), "error parsing leaf template: Fulcio leaf certificates must have codeSign extended key usage") + assert.Contains(t, err.Error(), "error getting root crypto signer: crypto signer error") } diff --git a/pkg/certmaker/template.go b/pkg/certmaker/template.go index 24c1dc9de..625e92fb5 100644 --- a/pkg/certmaker/template.go +++ b/pkg/certmaker/template.go @@ -38,8 +38,7 @@ type CertificateTemplate struct { Issuer struct { CommonName string `json:"commonName"` } `json:"issuer"` - NotBefore string `json:"notBefore"` - NotAfter string `json:"notAfter"` + CertLifetime string `json:"certLife"` // e.g. "8760h" for 1 year KeyUsage []string `json:"keyUsage"` BasicConstraints struct { IsCA bool `json:"isCA"` @@ -83,21 +82,21 @@ func ParseTemplate(filename string, parent *x509.Certificate) (*x509.Certificate // ValidateTemplate performs validation checks on the certificate template. func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate, certType string) error { - if tmpl.NotBefore == "" || tmpl.NotAfter == "" { - return fmt.Errorf("notBefore and notAfter times must be specified") - } + var notBefore, notAfter time.Time - notBefore, err := time.Parse(time.RFC3339, tmpl.NotBefore) - if err != nil { - return fmt.Errorf("invalid notBefore time format: %w", err) + if tmpl.CertLifetime == "" { + return fmt.Errorf("certLife must be specified") } - notAfter, err := time.Parse(time.RFC3339, tmpl.NotAfter) + + duration, err := time.ParseDuration(tmpl.CertLifetime) if err != nil { - return fmt.Errorf("invalid notAfter time format: %w", err) + return fmt.Errorf("invalid certLife format: %w", err) } - if notBefore.After(notAfter) { - return fmt.Errorf("NotBefore time must be before NotAfter time") + if duration <= 0 { + return fmt.Errorf("certLife must be positive") } + notBefore = time.Now().UTC() + notAfter = notBefore.Add(duration) if tmpl.Subject.CommonName == "" { return fmt.Errorf("template subject.commonName cannot be empty") @@ -111,6 +110,9 @@ func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate, certT if tmpl.Issuer.CommonName == "" { return fmt.Errorf("template issuer.commonName cannot be empty for root certificate") } + if len(tmpl.ExtKeyUsage) > 0 { + return fmt.Errorf("root certificates should not have extended key usage") + } // For root certificates, the SKID and AKID should match if len(tmpl.Extensions) > 0 { var hasAKID, hasSKID bool @@ -188,21 +190,40 @@ func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate, certT 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) +// SetCertificateUsages applies both basic key usages and extended key usages to the certificate. +// Supports basic usages: certSign, crlSign, digitalSignature +// Supports extended usages: CodeSigning +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 + case "crlSign": + cert.KeyUsage |= x509.KeyUsageCRLSign + case "digitalSignature": + cert.KeyUsage |= x509.KeyUsageDigitalSignature + } } - notAfter, err := time.Parse(time.RFC3339, tmpl.NotAfter) - if err != nil { - return nil, fmt.Errorf("invalid notAfter time format: %w", err) + // Set extended key usages + for _, usage := range extKeyUsages { + if usage == "CodeSigning" { + cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) + } } +} - if notBefore.After(notAfter) { - return nil, fmt.Errorf("NotBefore time must be before NotAfter time") +// CreateCertificateFromTemplate creates an x509.Certificate from the provided template +func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) (*x509.Certificate, error) { + var notBefore, notAfter time.Time + + duration, err := time.ParseDuration(tmpl.CertLifetime) + if err != nil { + return nil, fmt.Errorf("invalid certLife format: %w", err) } + notBefore = time.Now().UTC() + notAfter = notBefore.Add(duration) cert := &x509.Certificate{ Subject: pkix.Name{ @@ -229,37 +250,11 @@ func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certi cert.MaxPathLenZero = tmpl.BasicConstraints.MaxPathLen == 0 } - SetKeyUsages(cert, tmpl.KeyUsage) - SetExtKeyUsages(cert, tmpl.ExtKeyUsage) + SetCertificateUsages(cert, tmpl.KeyUsage, tmpl.ExtKeyUsage) 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 { - switch usage { - case "certSign": - cert.KeyUsage |= x509.KeyUsageCertSign - case "crlSign": - cert.KeyUsage |= x509.KeyUsageCRLSign - case "digitalSignature": - cert.KeyUsage |= x509.KeyUsageDigitalSignature - } - } -} - -// SetExtKeyUsages applies the specified extended key usage flags to the cert(s). -// Currently only supports CodeSigning usage. -func SetExtKeyUsages(cert *x509.Certificate, usages []string) { - for _, usage := range usages { - if usage == "CodeSigning" { - cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) - } - } -} - // Helper function to check if a key usage is present func containsKeyUsage(usages []string, target string) bool { for _, usage := range usages { diff --git a/pkg/certmaker/template_test.go b/pkg/certmaker/template_test.go index c3a5f5632..cb6faaf6f 100644 --- a/pkg/certmaker/template_test.go +++ b/pkg/certmaker/template_test.go @@ -36,7 +36,7 @@ func TestValidateTemplateFields(t *testing.T) { wantError string }{ { - name: "valid root CA", + name: "valid_root_CA", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -47,9 +47,8 @@ func TestValidateTemplateFields(t *testing.T) { Issuer: struct { CommonName string `json:"commonName"` }{CommonName: "Test CA"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"certSign"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign"}, BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -58,7 +57,7 @@ func TestValidateTemplateFields(t *testing.T) { certType: "root", }, { - name: "missing subject common name", + name: "missing_subject_common_name", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -66,14 +65,13 @@ func TestValidateTemplateFields(t *testing.T) { OrganizationalUnit []string `json:"organizationalUnit,omitempty"` CommonName string `json:"commonName"` }{}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", + CertLifetime: "8760h", }, certType: "root", wantError: "subject.commonName cannot be empty", }, { - name: "missing issuer common name for root", + name: "missing_issuer_common_name_for_root", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -81,8 +79,7 @@ func TestValidateTemplateFields(t *testing.T) { OrganizationalUnit []string `json:"organizationalUnit,omitempty"` CommonName string `json:"commonName"` }{CommonName: "Test CA"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", + CertLifetime: "8760h", BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -92,7 +89,7 @@ func TestValidateTemplateFields(t *testing.T) { wantError: "issuer.commonName cannot be empty for root certificate", }, { - name: "CA without key usage", + name: "CA_without_key_usage", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -108,14 +105,13 @@ func TestValidateTemplateFields(t *testing.T) { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` }{IsCA: true}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", + CertLifetime: "8760h", }, certType: "root", wantError: "CA certificate must specify at least one key usage", }, { - name: "CA without certSign usage", + name: "CA_without_certSign_usage", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -131,14 +127,13 @@ func TestValidateTemplateFields(t *testing.T) { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` }{IsCA: true}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", + CertLifetime: "8760h", }, certType: "root", wantError: "CA certificate must have certSign key usage", }, { - name: "leaf with certSign usage", + name: "leaf_with_certSign_usage", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -149,27 +144,26 @@ func TestValidateTemplateFields(t *testing.T) { Issuer: struct { CommonName string `json:"commonName"` }{CommonName: "Test CA"}, - KeyUsage: []string{"certSign", "digitalSignature"}, - ExtKeyUsage: []string{"CodeSigning"}, + KeyUsage: []string{"certSign", "digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + CertLifetime: "8760h", BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` }{IsCA: false}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", }, parent: &x509.Certificate{ Subject: pkix.Name{ CommonName: "Test CA", }, - NotBefore: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour * 24 * 365), }, certType: "leaf", wantError: "leaf certificate cannot have certSign key usage", }, { - name: "invalid_notBefore_format", + name: "invalid_certLife_format", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -177,52 +171,13 @@ func TestValidateTemplateFields(t *testing.T) { OrganizationalUnit []string `json:"organizationalUnit,omitempty"` CommonName string `json:"commonName"` }{CommonName: "Test"}, - NotBefore: "invalid", - NotAfter: "2024-01-01T00:00:00Z", + CertLifetime: "1y", }, certType: "root", - wantError: "invalid notBefore time format", + wantError: "invalid certLife format", }, { - name: "invalid_notAfter_format", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - }{CommonName: "Test"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "invalid", - }, - certType: "root", - wantError: "invalid notAfter time format", - }, - { - name: "NotBefore after NotAfter", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - }{CommonName: "Test CA"}, - Issuer: struct { - CommonName string `json:"commonName"` - }{CommonName: "Test CA"}, - NotBefore: "2025-01-01T00:00:00Z", - NotAfter: "2024-01-01T00:00:00Z", - KeyUsage: []string{"certSign", "crlSign"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{IsCA: true}, - }, - certType: "root", - wantError: "NotBefore time must be before NotAfter time", - }, - { - name: "leaf without CodeSigning usage", + name: "leaf_without_CodeSigning_usage", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -233,26 +188,25 @@ func TestValidateTemplateFields(t *testing.T) { Issuer: struct { CommonName string `json:"commonName"` }{CommonName: "Test CA"}, - KeyUsage: []string{"digitalSignature"}, + KeyUsage: []string{"digitalSignature"}, + CertLifetime: "8760h", BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` }{IsCA: false}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", }, parent: &x509.Certificate{ Subject: pkix.Name{ CommonName: "Test CA", }, - NotBefore: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour * 24 * 365), }, certType: "leaf", wantError: "Fulcio leaf certificates must have codeSign extended key usage", }, { - name: "valid intermediate CA", + name: "valid_intermediate_CA", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -263,9 +217,8 @@ func TestValidateTemplateFields(t *testing.T) { Issuer: struct { CommonName string `json:"commonName"` }{CommonName: "Test Root CA"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"certSign", "crlSign"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -278,13 +231,13 @@ func TestValidateTemplateFields(t *testing.T) { Subject: pkix.Name{ CommonName: "Test Root CA", }, - NotBefore: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 2), // 2 years }, certType: "intermediate", }, { - name: "intermediate with wrong MaxPathLen", + name: "intermediate_with_wrong_MaxPathLen", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -295,9 +248,8 @@ func TestValidateTemplateFields(t *testing.T) { Issuer: struct { CommonName string `json:"commonName"` }{CommonName: "Test Root CA"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"certSign", "crlSign"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -310,14 +262,14 @@ func TestValidateTemplateFields(t *testing.T) { Subject: pkix.Name{ CommonName: "Test Root CA", }, - NotBefore: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour * 24 * 365), }, certType: "intermediate", wantError: "intermediate CA MaxPathLen must be 0", }, { - name: "leaf with invalid time constraints", + name: "leaf_with_invalid_time_constraints", tmpl: &CertificateTemplate{ Subject: struct { Country []string `json:"country,omitempty"` @@ -325,10 +277,9 @@ func TestValidateTemplateFields(t *testing.T) { OrganizationalUnit []string `json:"organizationalUnit,omitempty"` CommonName string `json:"commonName"` }{CommonName: "Test Leaf"}, - NotBefore: "2023-01-01T00:00:00Z", - NotAfter: "2026-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, - ExtKeyUsage: []string{"CodeSigning"}, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -338,8 +289,8 @@ func TestValidateTemplateFields(t *testing.T) { Subject: pkix.Name{ CommonName: "Test CA", }, - NotBefore: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now().Add(time.Hour), // Parent's NotBefore is in the future + NotAfter: time.Now().Add(time.Hour * 24 * 365), }, certType: "leaf", wantError: "certificate notBefore time cannot be before parent's notBefore time", @@ -361,8 +312,7 @@ func TestValidateTemplateFields(t *testing.T) { func TestParseTemplateErrors(t *testing.T) { tests := []struct { - name string - + name string content string wantError string }{ @@ -372,13 +322,15 @@ func TestParseTemplateErrors(t *testing.T) { wantError: "invalid character", }, { - name: "missing time fields", + name: "missing_time_fields", content: `{ "subject": { - "commonName": "Test" - } + "commonName": "Test CA" + }, + "certLife": "", + "keyUsage": ["certSign"] }`, - wantError: "notBefore and notAfter times must be specified", + wantError: "certLife must be specified", }, { name: "invalid time format", @@ -386,10 +338,9 @@ func TestParseTemplateErrors(t *testing.T) { "subject": { "commonName": "Test" }, - "notBefore": "invalid", - "notAfter": "2024-01-01T00:00:00Z" + "certLife": "invalid" }`, - wantError: "invalid notBefore time format", + wantError: "invalid certLife format", }, } @@ -424,8 +375,7 @@ func TestInvalidCertificateType(t *testing.T) { OrganizationalUnit []string `json:"organizationalUnit,omitempty"` CommonName string `json:"commonName"` }{CommonName: "Test"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", + CertLifetime: "8760h", } err := ValidateTemplate(tmpl, nil, "invalid") @@ -470,10 +420,9 @@ func TestCreateCertificateFromTemplate(t *testing.T) { OrganizationalUnit: []string{"Test Unit"}, CommonName: "Test Leaf", }, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"digitalSignature"}, - ExtKeyUsage: []string{"CodeSigning"}, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -483,8 +432,8 @@ func TestCreateCertificateFromTemplate(t *testing.T) { Subject: pkix.Name{ CommonName: "Test CA", }, - NotBefore: time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour * 24 * 365), }, wantError: false, }, @@ -500,9 +449,8 @@ func TestCreateCertificateFromTemplate(t *testing.T) { Issuer: struct { CommonName string `json:"commonName"` }{CommonName: "Test Root"}, - NotBefore: "2024-01-01T00:00:00Z", - NotAfter: "2025-01-01T00:00:00Z", - KeyUsage: []string{"certSign", "crlSign"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, BasicConstraints: struct { IsCA bool `json:"isCA"` MaxPathLen int `json:"maxPathLen"` @@ -522,8 +470,44 @@ func TestCreateCertificateFromTemplate(t *testing.T) { OrganizationalUnit []string `json:"organizationalUnit,omitempty"` CommonName string `json:"commonName"` }{CommonName: "Test"}, - NotBefore: "invalid", - NotAfter: "2025-01-01T00:00:00Z", + CertLifetime: "1y", + }, + wantError: true, + }, + { + name: "valid_duration_based_template", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test Root"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test Root"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{ + IsCA: true, + MaxPathLen: 1, + }, + }, + wantError: false, + }, + { + name: "invalid_duration_format", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test"}, + CertLifetime: "1y", }, wantError: true, }, @@ -533,58 +517,24 @@ func TestCreateCertificateFromTemplate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cert, err := CreateCertificateFromTemplate(tt.tmpl, tt.parent) if tt.wantError { - if err == nil { - t.Errorf("expected error, got nil") - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if cert.Subject.CommonName != tt.tmpl.Subject.CommonName { - t.Errorf("CommonName got %v, want %v", cert.Subject.CommonName, tt.tmpl.Subject.CommonName) - } - - for _, usage := range tt.tmpl.KeyUsage { - switch usage { - case "certSign": - if cert.KeyUsage&x509.KeyUsageCertSign == 0 { - t.Error("expected KeyUsageCertSign to be set") - } - case "crlSign": - if cert.KeyUsage&x509.KeyUsageCRLSign == 0 { - t.Error("expected KeyUsageCRLSign to be set") - } - case "digitalSignature": - if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { - t.Error("expected KeyUsageDigitalSignature to be set") - } - } - } - - for _, usage := range tt.tmpl.ExtKeyUsage { - if usage == "CodeSigning" { - found := false - for _, certUsage := range cert.ExtKeyUsage { - if certUsage == x509.ExtKeyUsageCodeSigning { - found = true - break - } - } - if !found { - t.Error("expected ExtKeyUsageCodeSigning to be set") - } + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, cert) + if tt.tmpl.CertLifetime != "" { + duration, _ := time.ParseDuration(tt.tmpl.CertLifetime) + require.WithinDuration(t, time.Now().UTC(), cert.NotBefore, time.Second*5) + require.WithinDuration(t, time.Now().UTC().Add(duration), cert.NotAfter, time.Second*5) } } }) } } -func TestSetKeyUsagesAndExtKeyUsages(t *testing.T) { +func TestSetCertificateUsages(t *testing.T) { cert := &x509.Certificate{} - SetKeyUsages(cert, []string{"certSign", "crlSign", "digitalSignature"}) + SetCertificateUsages(cert, []string{"certSign", "crlSign", "digitalSignature"}, nil) if cert.KeyUsage&x509.KeyUsageCertSign == 0 { t.Error("expected KeyUsageCertSign to be set") } @@ -595,25 +545,350 @@ func TestSetKeyUsagesAndExtKeyUsages(t *testing.T) { t.Error("expected KeyUsageDigitalSignature to be set") } - SetExtKeyUsages(cert, []string{"CodeSigning"}) - found := false - for _, usage := range cert.ExtKeyUsage { - if usage == x509.ExtKeyUsageCodeSigning { - found = true - break - } - } - if !found { - t.Error("expected ExtKeyUsageCodeSigning to be set") - } - newCert := &x509.Certificate{} - SetKeyUsages(newCert, nil) - SetExtKeyUsages(newCert, nil) + SetCertificateUsages(newCert, nil, nil) if newCert.KeyUsage != x509.KeyUsage(0) { - t.Error("expected KeyUsage to be cleared") + t.Error("expected no key usages to be set") } if len(newCert.ExtKeyUsage) != 0 { - t.Error("expected ExtKeyUsage to be cleared") + t.Error("expected no extended key usages to be set") + } + + // Test extended key usages + SetCertificateUsages(newCert, nil, []string{"CodeSigning"}) + if len(newCert.ExtKeyUsage) != 1 { + t.Error("expected one extended key usage to be set") + } + if newCert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { + t.Error("expected CodeSigning extended key usage to be set") + } +} + +func TestValidateTemplateWithDurationAndTimestamps(t *testing.T) { + tests := []struct { + name string + tmpl *CertificateTemplate + parent *x509.Certificate + certType string + wantError string + }{ + { + name: "valid_duration_based_template", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "8760h", // 1 year + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true}, + }, + certType: "root", + }, + { + name: "invalid_duration_format", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "1y", // invalid format + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true}, + }, + certType: "root", + wantError: "invalid certLife format", + }, + { + name: "negative_duration", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "-8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true}, + }, + certType: "root", + wantError: "certLife must be positive", + }, + { + name: "mixed_time_specifications", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true}, + }, + certType: "root", + }, + { + name: "duration_based_leaf_with_parent", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test Leaf"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: false}, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 2), // 2 years + }, + certType: "leaf", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateTemplate(tt.tmpl, tt.parent, tt.certType) + if tt.wantError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateTemplateWithExtendedKeyUsage(t *testing.T) { + tests := []struct { + name string + template *CertificateTemplate + parent *x509.Certificate + certType string + wantError string + }{ + { + name: "valid_leaf_with_code_signing", + 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"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "24h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning", "TimeStamping"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: false, MaxPathLen: 0}, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(48 * time.Hour), + IsCA: true, + }, + certType: "leaf", + wantError: "", + }, + { + name: "leaf_with_multiple_ext_key_usages", + 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"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "24h", + KeyUsage: []string{"digitalSignature"}, + ExtKeyUsage: []string{"CodeSigning", "TimeStamping", "ServerAuth"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: false, MaxPathLen: 0}, + }, + parent: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(48 * time.Hour), + IsCA: true, + }, + certType: "leaf", + wantError: "", + }, + { + name: "root_with_ext_key_usage", + 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 Root CA"}, + Issuer: struct { + CommonName string `json:"commonName"` + }{CommonName: "Test Root CA"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign", "crlSign"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true, MaxPathLen: 1}, + }, + parent: nil, + certType: "root", + wantError: "root certificates should not have extended key usage", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateTemplate(tt.template, tt.parent, tt.certType) + if tt.wantError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) + } + }) + } +} + +func TestCreateCertificateFromTemplateWithExtendedFields(t *testing.T) { + tests := []struct { + name string + tmpl *CertificateTemplate + parent *x509.Certificate + wantError bool + checkFunc func(*testing.T, *x509.Certificate) + }{ + { + name: "full_subject_fields", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{ + Country: []string{"US", "CA"}, + Organization: []string{"Test Org", "Another Org"}, + OrganizationalUnit: []string{"Unit 1", "Unit 2"}, + CommonName: "Test Cert", + }, + CertLifetime: "8760h", + KeyUsage: []string{"digitalSignature", "certSign"}, + ExtKeyUsage: []string{"CodeSigning"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true, MaxPathLen: 1}, + }, + checkFunc: func(t *testing.T, cert *x509.Certificate) { + assert.Equal(t, []string{"US", "CA"}, cert.Subject.Country) + assert.Equal(t, []string{"Test Org", "Another Org"}, cert.Subject.Organization) + assert.Equal(t, []string{"Unit 1", "Unit 2"}, cert.Subject.OrganizationalUnit) + assert.Equal(t, "Test Cert", cert.Subject.CommonName) + assert.True(t, cert.IsCA) + assert.Equal(t, 1, cert.MaxPathLen) + assert.True(t, cert.KeyUsage&x509.KeyUsageDigitalSignature != 0) + assert.True(t, cert.KeyUsage&x509.KeyUsageCertSign != 0) + assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) + }, + }, + { + name: "zero_max_path_len", + tmpl: &CertificateTemplate{ + Subject: struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizationalUnit,omitempty"` + CommonName string `json:"commonName"` + }{CommonName: "Test CA"}, + CertLifetime: "8760h", + KeyUsage: []string{"certSign"}, + BasicConstraints: struct { + IsCA bool `json:"isCA"` + MaxPathLen int `json:"maxPathLen"` + }{IsCA: true, MaxPathLen: 0}, + }, + checkFunc: func(t *testing.T, cert *x509.Certificate) { + assert.True(t, cert.IsCA) + assert.Equal(t, 0, cert.MaxPathLen) + assert.True(t, cert.MaxPathLenZero) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cert, err := CreateCertificateFromTemplate(tt.tmpl, tt.parent) + if tt.wantError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, cert) + if tt.checkFunc != nil { + tt.checkFunc(t, cert) + } + } + }) } } diff --git a/pkg/certmaker/templates/intermediate-template.json b/pkg/certmaker/templates/intermediate-template.json index 64d33b80c..4ea1094a9 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", + "certLife": "43800h", "basicConstraints": { "isCA": true, "maxPathLen": 0 diff --git a/pkg/certmaker/templates/leaf-template.json b/pkg/certmaker/templates/leaf-template.json index 79406ac17..f93a0a03e 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", + "certLife": "8760h", "basicConstraints": { "isCA": false }, diff --git a/pkg/certmaker/templates/root-template.json b/pkg/certmaker/templates/root-template.json index 32464de16..fdfb070c4 100644 --- a/pkg/certmaker/templates/root-template.json +++ b/pkg/certmaker/templates/root-template.json @@ -14,14 +14,13 @@ "issuer": { "commonName": "" }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "basicConstraints": { - "isCA": true, - "maxPathLen": 1 - }, + "certLife": "87600h", "keyUsage": [ "certSign", "crlSign" - ] + ], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } } \ No newline at end of file