diff --git a/cmd/certificate_maker/certificate_maker.go b/cmd/certificate_maker/certificate_maker.go index f11a00100..001fe724a 100644 --- a/cmd/certificate_maker/certificate_maker.go +++ b/cmd/certificate_maker/certificate_maker.go @@ -45,9 +45,14 @@ var ( } createCmd = &cobra.Command{ - Use: "create", + Use: "create [common-name]", Short: "Create certificate chain", - RunE: runCreate, + Long: `Create a certificate chain with the specified common name. +The common name will be used as the Subject Common Name for the certificates. +If no common name is provided, the values from the templates will be used. +Example: fulcio-certificate-maker create "https://fulcio.example.com"`, + Args: cobra.RangeArgs(0, 1), + RunE: runCreate, } ) @@ -92,19 +97,24 @@ func init() { // Root certificate flags createCmd.Flags().String("root-key-id", "", "KMS key identifier for root certificate") - createCmd.Flags().String("root-template", "pkg/certmaker/templates/root-template.json", "Path to root certificate template") + createCmd.Flags().String("root-template", "", "Path to root certificate template (optional)") createCmd.Flags().String("root-cert", "root.pem", "Output path for root certificate") // Intermediate certificate flags createCmd.Flags().String("intermediate-key-id", "", "KMS key identifier for intermediate certificate") - createCmd.Flags().String("intermediate-template", "pkg/certmaker/templates/intermediate-template.json", "Path to intermediate certificate template") + createCmd.Flags().String("intermediate-template", "", "Path to intermediate certificate template (optional)") createCmd.Flags().String("intermediate-cert", "intermediate.pem", "Output path for intermediate certificate") // Leaf certificate flags createCmd.Flags().String("leaf-key-id", "", "KMS key identifier for leaf certificate") - createCmd.Flags().String("leaf-template", "pkg/certmaker/templates/leaf-template.json", "Path to leaf certificate template") + createCmd.Flags().String("leaf-template", "", "Path to leaf certificate template (optional)") createCmd.Flags().String("leaf-cert", "leaf.pem", "Output path for leaf certificate") + // Lifetime flags + createCmd.Flags().Duration("root-lifetime", 87600*time.Hour, "Root certificate lifetime") + createCmd.Flags().Duration("intermediate-lifetime", 43800*time.Hour, "Intermediate certificate lifetime") + createCmd.Flags().Duration("leaf-lifetime", 8760*time.Hour, "Leaf certificate lifetime") + 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")) @@ -120,15 +130,25 @@ 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")) + mustBindPFlag("root-lifetime", createCmd.Flags().Lookup("root-lifetime")) + mustBindPFlag("intermediate-lifetime", createCmd.Flags().Lookup("intermediate-lifetime")) + mustBindPFlag("leaf-lifetime", createCmd.Flags().Lookup("leaf-lifetime")) } -func runCreate(_ *cobra.Command, _ []string) error { +func runCreate(_ *cobra.Command, args []string) error { defer func() { rootCmd.SilenceUsage = true }() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // Get common name from args if provided, otherwise templates used + var commonName string + if len(args) > 0 { + commonName = args[0] + } + // Build KMS config from flags and environment config := certmaker.KMSConfig{ + CommonName: commonName, Type: viper.GetString("kms-type"), RootKeyID: viper.GetString("root-key-id"), IntermediateKeyID: viper.GetString("intermediate-key-id"), @@ -140,7 +160,7 @@ func runCreate(_ *cobra.Command, _ []string) error { switch config.Type { case "gcpkms": if gcpCredsFile := viper.GetString("gcp-credentials-file"); gcpCredsFile != "" { - // Check if credentials file exists before trying to use it + // Check if gcp creds exists if _, err := os.Stat(gcpCredsFile); err != nil { if os.IsNotExist(err) { return fmt.Errorf("failed to initialize KMS: credentials file not found: %s", gcpCredsFile) @@ -171,19 +191,32 @@ func runCreate(_ *cobra.Command, _ []string) error { return fmt.Errorf("failed to initialize KMS: %w", err) } - // Validate template paths + // Validate template paths if provided rootTemplate := viper.GetString("root-template") leafTemplate := viper.GetString("leaf-template") - intermediateTemplate := viper.GetString("intermediate-template") - if err := certmaker.ValidateTemplatePath(rootTemplate); err != nil { - return fmt.Errorf("root template error: %w", err) + if rootTemplate != "" { + if err := certmaker.ValidateTemplatePath(rootTemplate); err != nil { + return fmt.Errorf("root template error: %w", err) + } } - if err := certmaker.ValidateTemplatePath(leafTemplate); err != nil { - return fmt.Errorf("leaf template error: %w", err) + if leafTemplate != "" { + if err := certmaker.ValidateTemplatePath(leafTemplate); err != nil { + return fmt.Errorf("leaf template error: %w", err) + } } - return certmaker.CreateCertificates(km, config, rootTemplate, leafTemplate, viper.GetString("root-cert"), viper.GetString("leaf-cert"), viper.GetString("intermediate-key-id"), intermediateTemplate, viper.GetString("intermediate-cert")) + return certmaker.CreateCertificates(km, config, + rootTemplate, + leafTemplate, + viper.GetString("root-cert"), + viper.GetString("leaf-cert"), + viper.GetString("intermediate-key-id"), + viper.GetString("intermediate-template"), + viper.GetString("intermediate-cert"), + viper.GetDuration("root-lifetime"), + viper.GetDuration("intermediate-lifetime"), + viper.GetDuration("leaf-lifetime")) } func main() { diff --git a/cmd/certificate_maker/certificate_maker_test.go b/cmd/certificate_maker/certificate_maker_test.go index 7da89843f..9a676aa1d 100644 --- a/cmd/certificate_maker/certificate_maker_test.go +++ b/cmd/certificate_maker/certificate_maker_test.go @@ -153,8 +153,7 @@ func TestRunCreate(t *testing.T) { "basicConstraints": { "isCA": true, "maxPathLen": 1 - }, - "certLife": "8760h" + } }` leafTemplate := `{ @@ -167,8 +166,7 @@ func TestRunCreate(t *testing.T) { "extKeyUsage": ["CodeSigning"], "basicConstraints": { "isCA": false - }, - "certLife": "8760h" + } }` rootTmplPath := filepath.Join(tmpDir, "root-template.json") @@ -188,6 +186,7 @@ func TestRunCreate(t *testing.T) { { name: "missing KMS type", args: []string{ + "test-cn", "--aws-region", "us-west-2", "--root-key-id", "test-root-key", "--leaf-key-id", "test-leaf-key", @@ -200,6 +199,7 @@ func TestRunCreate(t *testing.T) { { name: "invalid KMS type", args: []string{ + "test-cn", "--kms-type", "invalid", "--aws-region", "us-west-2", "--root-key-id", "test-root-key", @@ -213,6 +213,7 @@ func TestRunCreate(t *testing.T) { { name: "missing root template", args: []string{ + "test-cn", "--kms-type", "awskms", "--aws-region", "us-west-2", "--root-key-id", "alias/test-key", @@ -221,11 +222,12 @@ func TestRunCreate(t *testing.T) { "--leaf-template", leafTmplPath, }, wantError: true, - errMsg: "template not found at nonexistent.json", + errMsg: "root template error: template not found at nonexistent.json", }, { name: "missing leaf template", args: []string{ + "test-cn", "--kms-type", "awskms", "--aws-region", "us-west-2", "--root-key-id", "alias/test-key", @@ -234,11 +236,12 @@ func TestRunCreate(t *testing.T) { "--leaf-template", "nonexistent.json", }, wantError: true, - errMsg: "template not found at nonexistent.json", + errMsg: "leaf template error: template not found at nonexistent.json", }, { name: "GCP KMS with credentials file", args: []string{ + "test-cn", "--kms-type", "gcpkms", "--root-key-id", "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1", "--leaf-key-id", "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/leaf-key/cryptoKeyVersions/1", @@ -252,6 +255,7 @@ func TestRunCreate(t *testing.T) { { name: "Azure KMS without tenant ID", args: []string{ + "test-cn", "--kms-type", "azurekms", "--root-key-id", "azurekms:name=test-key;vault=test-vault", "--leaf-key-id", "azurekms:name=leaf-key;vault=test-vault", @@ -259,11 +263,12 @@ func TestRunCreate(t *testing.T) { "--leaf-template", leafTmplPath, }, wantError: true, - errMsg: "tenant-id is required for Azure KMS", + errMsg: "azure-tenant-id is required for Azure KMS", }, { name: "AWS KMS test", args: []string{ + "test-cn", "--kms-type", "awskms", "--aws-region", "us-west-2", "--root-key-id", "alias/test-key", @@ -272,11 +277,12 @@ func TestRunCreate(t *testing.T) { "--leaf-template", leafTmplPath, }, wantError: true, - errMsg: "error getting root public key: getting public key: operation error KMS: GetPublicKey", + errMsg: "operation error KMS: GetPublicKey", }, { name: "HashiVault KMS without token", args: []string{ + "test-cn", "--kms-type", "hashivault", "--root-key-id", "transit/keys/test-key", "--leaf-key-id", "transit/keys/leaf-key", @@ -285,11 +291,12 @@ func TestRunCreate(t *testing.T) { "--leaf-template", leafTmplPath, }, wantError: true, - errMsg: "token is required for HashiVault KMS", + errMsg: "vault-token is required for HashiVault KMS", }, { name: "HashiVault KMS without address", args: []string{ + "test-cn", "--kms-type", "hashivault", "--root-key-id", "transit/keys/test-key", "--leaf-key-id", "transit/keys/leaf-key", @@ -298,7 +305,7 @@ func TestRunCreate(t *testing.T) { "--leaf-template", leafTmplPath, }, wantError: true, - errMsg: "address is required for HashiVault KMS", + errMsg: "vault-address is required for HashiVault KMS", }, } @@ -342,64 +349,12 @@ func TestRunCreate(t *testing.T) { viper.BindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template")) viper.BindPFlag("intermediate-template", cmd.Flags().Lookup("intermediate-template")) - switch tt.name { - case "invalid KMS type": - viper.Set("root-key-id", "dummy-key") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", leafTmplPath) - case "missing_root_template": - viper.Set("kms-type", "awskms") - viper.Set("root-key-id", "dummy-key") - viper.Set("root-template", "nonexistent.json") - viper.Set("leaf-template", leafTmplPath) - case "missing_leaf_template": - viper.Set("kms-type", "awskms") - viper.Set("leaf-key-id", "dummy-key") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", "nonexistent.json") - case "GCP_KMS_with_credentials_file": - viper.Set("kms-type", "gcpkms") - viper.Set("root-key-id", "dummy-key") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", leafTmplPath) - case "Azure_KMS_without_tenant_ID": - viper.Set("kms-type", "azurekms") - viper.Set("root-key-id", "dummy-key") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", leafTmplPath) - case "AWS_KMS_test": - viper.Set("kms-type", "awskms") - viper.Set("aws-region", "us-west-2") - viper.Set("root-key-id", "dummy-key") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", leafTmplPath) - case "HashiVault_KMS_without_token": - viper.Set("kms-type", "hashivault") - viper.Set("root-key-id", "dummy-key") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", leafTmplPath) - case "HashiVault_KMS_without_address": - viper.Set("kms-type", "hashivault") - viper.Set("root-key-id", "dummy-key") - viper.Set("vault-token", "dummy-token") - viper.Set("root-template", rootTmplPath) - viper.Set("leaf-template", leafTmplPath) - } - cmd.SetArgs(tt.args) err := cmd.Execute() if tt.wantError { require.Error(t, err) - 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) - } + assert.Contains(t, err.Error(), tt.errMsg) } else { require.NoError(t, err) } @@ -620,7 +575,6 @@ func TestTemplateValidationInRunCreate(t *testing.T) { "issuer": { "commonName": "Test CA" }, - "certLife": "8760h", "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, @@ -628,59 +582,82 @@ func TestTemplateValidationInRunCreate(t *testing.T) { } }` - 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) + err = os.WriteFile(invalidPath, []byte("invalid json"), 0600) require.NoError(t, err) tests := []struct { name string - flags []string + args []string wantError string }{ { - name: "valid_template_paths", - flags: []string{ + name: "valid template paths", + args: []string{ + "test-cn", "--kms-type", "awskms", "--aws-region", "us-west-2", "--root-key-id", "alias/test-key", - "--leaf-key-id", "alias/test-leaf-key", + "--leaf-key-id", "alias/test-key", "--root-template", validPath, "--leaf-template", validPath, }, wantError: "error getting root public key", }, { - name: "nonexistent_root_template", - flags: []string{ + name: "nonexistent root template", + args: []string{ + "test-cn", "--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-key-id", "alias/test-key", + "--root-template", "nonexistent.json", "--leaf-template", validPath, }, - wantError: "template not found", + wantError: "template not found at nonexistent.json", }, { - name: "invalid_root_template_json", - flags: []string{ + name: "invalid root template json", + args: []string{ + "test-cn", "--kms-type", "awskms", "--aws-region", "us-west-2", "--root-key-id", "alias/test-key", - "--leaf-key-id", "alias/test-leaf-key", + "--leaf-key-id", "alias/test-key", "--root-template", invalidPath, "--leaf-template", validPath, }, - wantError: "invalid JSON", + wantError: "invalid JSON in template file", + }, + { + name: "nonexistent leaf template", + args: []string{ + "test-cn", + "--kms-type", "awskms", + "--aws-region", "us-west-2", + "--root-key-id", "alias/test-key", + "--leaf-key-id", "alias/test-key", + "--root-template", validPath, + "--leaf-template", "nonexistent.json", + }, + wantError: "template not found at nonexistent.json", + }, + { + name: "invalid leaf template json", + args: []string{ + "test-cn", + "--kms-type", "awskms", + "--aws-region", "us-west-2", + "--root-key-id", "alias/test-key", + "--leaf-key-id", "alias/test-key", + "--root-template", validPath, + "--leaf-template", invalidPath, + }, + wantError: "invalid JSON in template file", }, } @@ -701,7 +678,7 @@ func TestTemplateValidationInRunCreate(t *testing.T) { mustBindPFlag("root-template", cmd.Flags().Lookup("root-template")) mustBindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template")) - cmd.SetArgs(tt.flags) + cmd.SetArgs(tt.args) 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 57a233b16..9fd1e623b 100644 --- a/docs/certificate-maker.md +++ b/docs/certificate-maker.md @@ -5,7 +5,7 @@ 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. +Relies on [x509util](https://pkg.go.dev/go.step.sm/crypto/x509util) which builds X.509 certificates from JSON templates. The tool includes embedded default templates that are compiled into the binary, making it ready to use without external template files. ## Requirements @@ -27,6 +27,14 @@ The tool can be configured using either command-line flags or environment variab ### Command-Line Interface +The `create` command accepts an optional positional argument for the common name: + +```bash +fulcio-certificate-maker create [common-name] +``` + +If no common name is provided, the values from the templates will be used. + Available flags: - `--kms-type`: KMS provider type (awskms, gcpkms, azurekms, hashivault) @@ -44,6 +52,9 @@ Available flags: - `--intermediate-key-id`: KMS key identifier for intermediate certificate - `--intermediate-template`: Path to intermediate certificate template - `--intermediate-cert`: Output path for intermediate certificate +- `--root-lifetime`: Root certificate lifetime (default: 87600h, 10 years) +- `--intermediate-lifetime`: Intermediate certificate lifetime (default: 43800h, 5 years) +- `--leaf-lifetime`: Leaf certificate lifetime (default: 8760h, 1 year) ### Environment Variables @@ -64,13 +75,13 @@ Available flags: ### Certificate Templates -The tool uses JSON templates to define certificate properties: +The embedded templates are located in `pkg/certmaker/templates/` in the source code and are compiled into the binary. You can override these defaults by providing your own template files using: -- `root-template.json`: Defines root CA certificate properties -- `intermediate-template.json`: Defines intermediate CA certificate properties (when using --intermediate-key-id) -- `leaf-template.json`: Defines leaf certificate properties +- `--root-template`: Custom root CA template +- `--intermediate-template`: Custom intermediate CA template +- `--leaf-template`: Custom leaf template -Templates are located in `pkg/certmaker/templates/`. +If no custom templates are provided via flags, the tool will automatically use the embedded defaults which are designed to work with Fulcio's certificate requirements as long as the intended common name is used as a positional argument. ### Provider-Specific Configuration Examples @@ -246,19 +257,21 @@ Certificate: Example with AWS KMS: ```bash -fulcio-certificate-maker create \ +fulcio-certificate-maker create "https://fulcio.example.com" \ --kms-type awskms \ --aws-region us-east-1 \ --root-key-id alias/fulcio-root \ --leaf-key-id alias/fulcio-leaf \ --root-template pkg/certmaker/templates/root-template.json \ - --leaf-template pkg/certmaker/templates/leaf-template.json + --leaf-template pkg/certmaker/templates/leaf-template.json \ + --root-lifetime 87600h \ + --leaf-lifetime 8760h ``` Example with Azure KMS: ```bash -fulcio-certificate-maker create \ +fulcio-certificate-maker create "https://fulcio.example.com" \ --kms-type azurekms \ --azure-tenant-id 1b4a4fed-fed8-4823-a8a0-3d5cea83d122 \ --root-key-id "azurekms:name=sigstore-key;vault=sigstore-key" \ @@ -266,13 +279,16 @@ fulcio-certificate-maker create \ --intermediate-key-id "azurekms:name=sigstore-key-intermediate;vault=sigstore-key" \ --root-cert root.pem \ --leaf-cert leaf.pem \ - --intermediate-cert intermediate.pem + --intermediate-cert intermediate.pem \ + --root-lifetime 87600h \ + --intermediate-lifetime 43800h \ + --leaf-lifetime 8760h ``` Example with GCP KMS: ```bash -fulcio-certificate-maker create \ +fulcio-certificate-maker create "https://fulcio.example.com" \ --kms-type gcpkms \ --gcp-credentials-file ~/.config/gcloud/application_default_credentials.json \ --root-key-id projects//locations//keyRings//cryptoKeys/fulcio-key1/cryptoKeyVersions/ \ @@ -280,13 +296,16 @@ fulcio-certificate-maker create \ --leaf-key-id projects//locations//keyRings//cryptoKeys/fulcio-key1/cryptoKeyVersions/ \ --root-cert root.pem \ --leaf-cert leaf.pem \ - --intermediate-cert intermediate.pem + --intermediate-cert intermediate.pem \ + --root-lifetime 87600h \ + --intermediate-lifetime 43800h \ + --leaf-lifetime 8760h ``` Example with HashiCorp Vault KMS: ```bash -fulcio-certificate-maker create \ +fulcio-certificate-maker create "https://fulcio.example.com" \ --kms-type hashivault \ --vault-address http://vault:8200 \ --vault-token token \ @@ -295,5 +314,8 @@ fulcio-certificate-maker create \ --intermediate-key-id "transit/keys/intermediate-key" \ --root-cert root.pem \ --leaf-cert leaf.pem \ - --intermediate-cert intermediate.pem + --intermediate-cert intermediate.pem \ + --root-lifetime 87600h \ + --intermediate-lifetime 43800h \ + --leaf-lifetime 8760h ``` diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go index 8d0f526eb..decf9a43b 100644 --- a/pkg/certmaker/certmaker.go +++ b/pkg/certmaker/certmaker.go @@ -26,6 +26,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" @@ -49,6 +50,7 @@ type CryptoSignerVerifier interface { // KMSConfig holds config for KMS providers. type KMSConfig struct { + CommonName string Type string RootKeyID string IntermediateKeyID string @@ -145,15 +147,10 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, rootTemplatePath, leafTemplatePath string, rootCertPath, leafCertPath string, - intermediateKeyID, intermediateTemplatePath, intermediateCertPath string) error { + intermediateKeyID, intermediateTemplatePath, intermediateCertPath string, + rootLifetime, intermediateLifetime, leafLifetime time.Duration) error { // Create root cert - rootTmpl, err := ParseTemplate(rootTemplatePath, nil) - if err != nil { - return fmt.Errorf("error parsing root template: %w", err) - } - - // Get public key from signer rootPubKey, err := sv.PublicKey() if err != nil { return fmt.Errorf("error getting root public key: %w", err) @@ -169,6 +166,29 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, return fmt.Errorf("error getting root crypto signer: %w", err) } + // Use default root template if none provided + var rootTemplate interface{} + if rootTemplatePath == "" { + defaultTemplate, err := GetDefaultTemplate("root") + if err != nil { + return fmt.Errorf("error getting default root template: %w", err) + } + rootTemplate = defaultTemplate + } else { + // Read from FS if path is provided + content, err := os.ReadFile(rootTemplatePath) + if err != nil { + return fmt.Errorf("root template error: template not found at %s: %w", rootTemplatePath, err) + } + rootTemplate = string(content) + } + + rootNotAfter := time.Now().UTC().Add(rootLifetime) + rootTmpl, err := ParseTemplate(rootTemplate, nil, rootNotAfter, rootPubKey, config.CommonName) + if err != nil { + return fmt.Errorf("error parsing root template: %w", err) + } + rootCert, err := x509util.CreateCertificate(rootTmpl, rootTmpl, rootPubKey, rootSigner) if err != nil { return fmt.Errorf("error creating root certificate: %w", err) @@ -183,12 +203,6 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, if intermediateKeyID != "" { // Create intermediate cert if key ID is provided - intermediateTmpl, err := ParseTemplate(intermediateTemplatePath, rootCert) - if err != nil { - return fmt.Errorf("error parsing intermediate template: %w", err) - } - - // Initialize new KMS for intermediate key intermediateConfig := config intermediateConfig.RootKeyID = intermediateKeyID intermediateSV, err := InitKMS(context.Background(), intermediateConfig) @@ -201,16 +215,38 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, return fmt.Errorf("error getting intermediate public key: %w", err) } - // Get crypto.Signer for intermediate intermediateCryptoSV, ok := intermediateSV.(CryptoSignerVerifier) if !ok { return fmt.Errorf("intermediate signer does not implement CryptoSigner") } + intermediateSigner, _, err := intermediateCryptoSV.CryptoSigner(context.Background(), nil) if err != nil { return fmt.Errorf("error getting intermediate crypto signer: %w", err) } + var intermediateTemplate interface{} + if intermediateTemplatePath == "" { + defaultTemplate, err := GetDefaultTemplate("intermediate") + if err != nil { + return fmt.Errorf("error getting default intermediate template: %w", err) + } + intermediateTemplate = defaultTemplate + } else { + // Read from FS if path is provided + content, err := os.ReadFile(intermediateTemplatePath) + if err != nil { + return fmt.Errorf("intermediate template error: template not found at %s: %w", intermediateTemplatePath, err) + } + intermediateTemplate = string(content) + } + + intermediateNotAfter := time.Now().UTC().Add(intermediateLifetime) + intermediateTmpl, err := ParseTemplate(intermediateTemplate, rootCert, intermediateNotAfter, intermediatePubKey, config.CommonName) + if err != nil { + return fmt.Errorf("error parsing intermediate template: %w", err) + } + intermediateCert, err := x509util.CreateCertificate(intermediateTmpl, rootCert, intermediatePubKey, rootSigner) if err != nil { return fmt.Errorf("error creating intermediate certificate: %w", err) @@ -228,12 +264,6 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, } // Create leaf cert - leafTmpl, err := ParseTemplate(leafTemplatePath, signingCert) - if err != nil { - return fmt.Errorf("error parsing leaf template: %w", err) - } - - // Initialize new KMS for leaf key leafConfig := config leafConfig.RootKeyID = config.LeafKeyID leafSV, err := InitKMS(context.Background(), leafConfig) @@ -246,16 +276,38 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, return fmt.Errorf("error getting leaf public key: %w", err) } - // Get crypto.Signer for leaf leafCryptoSV, ok := leafSV.(CryptoSignerVerifier) if !ok { return fmt.Errorf("leaf signer does not implement CryptoSigner") } + _, _, err = leafCryptoSV.CryptoSigner(context.Background(), nil) if err != nil { return fmt.Errorf("error getting leaf crypto signer: %w", err) } + var leafTemplate interface{} + if leafTemplatePath == "" { + defaultTemplate, err := GetDefaultTemplate("leaf") + if err != nil { + return fmt.Errorf("error getting default leaf template: %w", err) + } + leafTemplate = defaultTemplate + } else { + // Read from FS if path is provided + content, err := os.ReadFile(leafTemplatePath) + if err != nil { + return fmt.Errorf("leaf template error: template not found at %s: %w", leafTemplatePath, err) + } + leafTemplate = string(content) + } + + leafNotAfter := time.Now().UTC().Add(leafLifetime) + leafTmpl, err := ParseTemplate(leafTemplate, signingCert, leafNotAfter, leafPubKey, config.CommonName) + if err != nil { + return fmt.Errorf("error parsing leaf template: %w", err) + } + leafCert, err := x509util.CreateCertificate(leafTmpl, signingCert, leafPubKey, signingKey) if err != nil { return fmt.Errorf("error creating leaf certificate: %w", err) @@ -268,42 +320,41 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig, return nil } -// WriteCertificateToFile writes an X.509 certificate to a PEM-encoded file +// Writes cert to a PEM-encoded file func WriteCertificateToFile(cert *x509.Certificate, filename string) error { if cert == nil { - return fmt.Errorf("certificate cannot be nil") + return fmt.Errorf("certificate is nil") } if len(cert.Raw) == 0 { return fmt.Errorf("certificate has no raw data") } - certPEM := &pem.Block{ + + block := &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, } - file, err := os.Create(filename) + f, err := os.Create(filename) if err != nil { - return fmt.Errorf("failed to create file %s: %w", filename, err) - } - defer file.Close() - - if err := pem.Encode(file, certPEM); err != nil { - return fmt.Errorf("failed to write certificate to file %s: %w", filename, err) + return err } + defer f.Close() - // Determine cert type - certType := "root" - if !cert.IsCA { - certType = "leaf" - } else if cert.MaxPathLen == 0 { - certType = "intermediate" + // get cert type + certType := "leaf" + if cert.IsCA { + if cert.CheckSignatureFrom(cert) == nil { + certType = "root" + } else { + certType = "intermediate" + } } - fmt.Printf("Your %s certificate has been saved in %s.\n", certType, filename) - return nil + fmt.Printf("Saved %s cert to %s\n", certType, filename) + return pem.Encode(f, block) } -// ValidateKMSConfig ensures all required KMS configuration parameters are present +// Ensures all required KMS config params are present func ValidateKMSConfig(config KMSConfig) error { if config.Type == "" { return fmt.Errorf("KMS type cannot be empty") @@ -349,6 +400,9 @@ func ValidateKMSConfig(config KMSConfig) error { case "gcpkms": // GCP KMS validation + if config.Options == nil || config.Options["gcp-credentials-file"] == "" { + return fmt.Errorf("gcp-credentials-file is required for GCP KMS") + } validateGCPKeyID := func(keyID, keyType string) error { if keyID == "" { return nil diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index b54f17db9..a0f587cf2 100644 --- a/pkg/certmaker/certmaker_test.go +++ b/pkg/certmaker/certmaker_test.go @@ -22,14 +22,11 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" - "crypto/x509/pkix" - "errors" + "encoding/pem" "fmt" "io" - "math/big" "os" "path/filepath" - "strings" "testing" "time" @@ -38,9 +35,9 @@ import ( "github.com/stretchr/testify/require" ) -// mockSignerVerifier implements signature.SignerVerifier for testing +// mockSignerVerifier implements signature.SignerVerifier and CryptoSignerVerifier for testing type mockSignerVerifier struct { - key crypto.PrivateKey + key crypto.Signer err error publicKeyFunc func() (crypto.PublicKey, error) signMessageFunc func(message io.Reader, opts ...signature.SignOption) ([]byte, error) @@ -58,16 +55,11 @@ func (m *mockSignerVerifier) SignMessage(message io.Reader, opts ...signature.Si if _, err := message.Read(digest); err != nil { return nil, err } - switch k := m.key.(type) { - case *ecdsa.PrivateKey: - return k.Sign(rand.Reader, digest, crypto.SHA256) - default: - return nil, fmt.Errorf("unsupported key type") - } + return m.key.Sign(rand.Reader, digest, crypto.SHA256) } func (m *mockSignerVerifier) VerifySignature(_, _ io.Reader, _ ...signature.VerifyOption) error { - return m.err + return nil } func (m *mockSignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) { @@ -77,15 +69,7 @@ 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.PublicKey, nil - default: - return nil, fmt.Errorf("unsupported key type") - } + return m.key.Public(), nil } func (m *mockSignerVerifier) Close() error { return nil } @@ -98,1910 +82,436 @@ func (m *mockSignerVerifier) KeyID() (string, error) { return "", nil } func (m *mockSignerVerifier) Status() error { return nil } -func (m *mockSignerVerifier) CryptoSigner(ctx context.Context, errHandler func(error)) (crypto.Signer, crypto.SignerOpts, error) { +func (m *mockSignerVerifier) CryptoSigner(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { if m.cryptoSignerFunc != nil { - return m.cryptoSignerFunc(ctx, errHandler) + return m.cryptoSignerFunc(context.Background(), nil) } if m.err != nil { return nil, nil, m.err } - if m.key == nil { - return nil, nil, fmt.Errorf("no key available") + return m.key, crypto.SHA256, nil +} + +func TestCreateCertificates(t *testing.T) { + originalInitKMS := InitKMS + defer func() { InitKMS = originalInitKMS }() + + tmpDir := t.TempDir() + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + intermediateTemplate := `{ + "subject": { + "commonName": "Test Intermediate CA" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } + }` + + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning"], + "basicConstraints": { + "isCA": false + } + }` + + rootTmplPath := filepath.Join(tmpDir, "root.json") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate.json") + leafTmplPath := filepath.Join(tmpDir, "leaf.json") + rootCertPath := filepath.Join(tmpDir, "root.pem") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.pem") + leafCertPath := filepath.Join(tmpDir, "leaf.pem") + + require.NoError(t, os.WriteFile(rootTmplPath, []byte(rootTemplate), 0600)) + require.NoError(t, os.WriteFile(intermediateTmplPath, []byte(intermediateTemplate), 0600)) + require.NoError(t, os.WriteFile(leafTmplPath, []byte(leafTemplate), 0600)) + + mockSigner := &mockSignerVerifier{ + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("test error") + }, } - switch k := m.key.(type) { - case *ecdsa.PrivateKey: - return k, crypto.SHA256, nil - default: - return nil, nil, fmt.Errorf("unsupported key type") + + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + return mockSigner, nil } -} -var ( - originalInitKMS = InitKMS -) + err := CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "", + 87600*time.Hour, 43800*time.Hour, 8760*time.Hour) + + require.Error(t, err) + assert.Contains(t, err.Error(), "error getting root public key: test error") + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner = &mockSignerVerifier{ + key: key, + cryptoSignerFunc: func(_ context.Context, _ func(error)) (crypto.Signer, crypto.SignerOpts, error) { + return nil, nil, fmt.Errorf("crypto signer error") + }, + } + + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + return mockSigner, nil + } + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "", + 87600*time.Hour, 43800*time.Hour, 8760*time.Hour) + + require.Error(t, err) + assert.Contains(t, err.Error(), "error getting root crypto signer: crypto signer error") + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockSigner = &mockSignerVerifier{ + key: key, + } + + InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { + return mockSigner, nil + } + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "", "", "", + 87600*time.Hour, 43800*time.Hour, 8760*time.Hour) + + require.NoError(t, err) + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.RootKeyID == "intermediate-key" { + return nil, fmt.Errorf("intermediate KMS error") + } + return mockSigner, nil + } + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "intermediate-key", intermediateTmplPath, intermediateCertPath, + 87600*time.Hour, 43800*time.Hour, 8760*time.Hour) + + require.Error(t, err) + assert.Contains(t, err.Error(), "error initializing intermediate KMS: intermediate KMS error") + + intermediateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + mockIntermediateSigner := &mockSignerVerifier{ + key: intermediateKey, + publicKeyFunc: func() (crypto.PublicKey, error) { + return nil, fmt.Errorf("intermediate public key error") + }, + } + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.RootKeyID == "intermediate-key" { + return mockIntermediateSigner, nil + } + return mockSigner, nil + } + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "intermediate-key", intermediateTmplPath, intermediateCertPath, + 87600*time.Hour, 43800*time.Hour, 8760*time.Hour) + + require.Error(t, err) + assert.Contains(t, err.Error(), "error getting intermediate public key: intermediate public key error") + + mockIntermediateSigner = &mockSignerVerifier{ + key: intermediateKey, + } + + InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) { + if config.RootKeyID == "intermediate-key" { + return mockIntermediateSigner, nil + } + return mockSigner, nil + } + + err = CreateCertificates(mockSigner, KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath, "intermediate-key", intermediateTmplPath, intermediateCertPath, + 87600*time.Hour, 43800*time.Hour, 8760*time.Hour) + + 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 TestValidateKMSConfig(t *testing.T) { tests := []struct { - name string - config KMSConfig - wantErr string + name string + config KMSConfig + wantError string }{ { - name: "empty KMS type", - config: KMSConfig{}, - wantErr: "KMS type cannot be empty", + name: "valid AWS config", + config: KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, + }, + { + name: "valid GCP config", + config: KMSConfig{ + Type: "gcpkms", + RootKeyID: "projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1", + LeafKeyID: "projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1", + Options: map[string]string{"gcp-credentials-file": "/path/to/creds.json"}, + }, }, { - name: "missing key IDs", + name: "valid Azure config", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"aws-region": "us-west-2"}, + Type: "azurekms", + RootKeyID: "azurekms:name=key1;vault=vault1", + LeafKeyID: "azurekms:name=key2;vault=vault1", + Options: map[string]string{"azure-tenant-id": "tenant-id"}, }, - wantErr: "RootKeyID must be specified", }, { - name: "missing leaf key ID", + name: "valid HashiVault config", config: KMSConfig{ - Type: "awskms", - Options: map[string]string{"aws-region": "us-west-2"}, - RootKeyID: "alias/test-key", + Type: "hashivault", + RootKeyID: "transit/keys/root-key", + LeafKeyID: "transit/keys/leaf-key", + Options: map[string]string{ + "vault-token": "token", + "vault-address": "http://localhost:8200", + }, }, - wantErr: "LeafKeyID must be specified", }, { - name: "AWS KMS missing region", + name: "missing AWS region", config: KMSConfig{ Type: "awskms", - RootKeyID: "alias/test-key", - LeafKeyID: "alias/test-leaf-key", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", }, - wantErr: "aws-region is required for AWS KMS", + wantError: "aws-region is required for AWS KMS", }, { - name: "Azure KMS missing tenant ID", + name: "missing Azure tenant ID", config: KMSConfig{ Type: "azurekms", - RootKeyID: "azurekms:name=test-key;vault=test-vault", - Options: map[string]string{}, + RootKeyID: "azurekms:name=key1;vault=vault1", + LeafKeyID: "azurekms:name=key2;vault=vault1", }, - wantErr: "tenant-id is required for Azure KMS", + wantError: "options map is required for Azure KMS", }, { - name: "Azure KMS missing vault parameter", + name: "missing HashiVault token", config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=test-key", - Options: map[string]string{"azure-tenant-id": "test-tenant"}, + Type: "hashivault", + RootKeyID: "transit/keys/root-key", + LeafKeyID: "transit/keys/leaf-key", + Options: map[string]string{ + "vault-address": "http://localhost:8200", + }, + }, + wantError: "vault-token is required for HashiVault KMS", + }, + { + name: "missing HashiVault address", + config: KMSConfig{ + Type: "hashivault", + RootKeyID: "transit/keys/root-key", + LeafKeyID: "transit/keys/leaf-key", + Options: map[string]string{ + "vault-token": "token", + }, }, - wantErr: "azurekms RootKeyID must contain ';vault=' parameter", + wantError: "vault-address is required for HashiVault KMS", }, { name: "unsupported KMS type", config: KMSConfig{ Type: "unsupported", - RootKeyID: "test-key", + RootKeyID: "key1", + LeafKeyID: "key2", }, - wantErr: "unsupported KMS type", + wantError: "unsupported KMS type: unsupported", }, { - name: "GCP KMS missing cryptoKeyVersions", + name: "invalid AWS key ID format", config: KMSConfig{ - Type: "gcpkms", - RootKeyID: "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key", + Type: "awskms", + RootKeyID: "invalid-key-id", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, + wantError: "awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'", + }, + { + name: "AWS key ID region mismatch", + config: KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, }, - wantErr: "gcpkms RootKeyID must contain '/cryptoKeyVersions/'", + wantError: "region in ARN (us-east-1) does not match configured region (us-west-2)", }, { - name: "GCP KMS invalid key format", + name: "invalid GCP key ID format", config: KMSConfig{ Type: "gcpkms", - RootKeyID: "invalid-format", + RootKeyID: "invalid/key/path", + LeafKeyID: "projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1", + Options: map[string]string{"gcp-credentials-file": "/path/to/creds.json"}, }, - wantErr: "gcpkms RootKeyID must start with 'projects/'", + wantError: "gcpkms RootKeyID must start with 'projects/'", }, { - name: "HashiVault KMS missing options", + name: "invalid Azure key ID format", config: KMSConfig{ - Type: "hashivault", - RootKeyID: "transit/keys/test-key", + Type: "azurekms", + RootKeyID: "invalid-key-id", + LeafKeyID: "azurekms:name=key2;vault=vault1", + Options: map[string]string{"azure-tenant-id": "tenant-id"}, }, - wantErr: "options map is required for HashiVault KMS", + wantError: "azurekms RootKeyID must start with 'azurekms:name='", }, { - name: "HashiVault KMS missing token", + name: "invalid HashiVault key ID format", config: KMSConfig{ Type: "hashivault", - RootKeyID: "transit/keys/test-key", - Options: map[string]string{"vault-address": "http://vault:8200"}, + RootKeyID: "invalid/path", + LeafKeyID: "transit/keys/leaf-key", + Options: map[string]string{ + "vault-token": "token", + "vault-address": "http://localhost:8200", + }, }, - wantErr: "token is required for HashiVault KMS", + wantError: "hashivault RootKeyID must be in format: transit/keys/keyname", }, { - name: "HashiVault KMS missing address", + name: "missing root key ID", config: KMSConfig{ - Type: "hashivault", - RootKeyID: "transit/keys/test-key", - Options: map[string]string{"vault-token": "test-token"}, + Type: "awskms", + LeafKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, + }, + wantError: "RootKeyID must be specified", + }, + { + name: "missing leaf key ID", + config: KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012", + Options: map[string]string{"aws-region": "us-west-2"}, }, - wantErr: "address is required for HashiVault KMS", + wantError: "LeafKeyID must be specified", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateKMSConfig(tt.config) - if tt.wantErr == "" { - assert.NoError(t, err) + if tt.wantError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) } else { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr) + require.NoError(t, err) } }) } } -func TestValidateTemplatePath(t *testing.T) { +func TestWriteCertificateToFile(t *testing.T) { tests := []struct { - name string - setup func() string - wantError string + name string + cert *x509.Certificate + path string + wantErr string }{ { - name: "nonexistent_file", - setup: func() string { - return "/nonexistent/template.json" - }, - wantError: "no such file or directory", + name: "valid_certificate", + cert: &x509.Certificate{Raw: []byte("test")}, + path: filepath.Join(t.TempDir(), "valid.pem"), }, { - name: "wrong_extension", - setup: func() string { - tmpFile, err := os.CreateTemp("", "template-*.txt") - require.NoError(t, err) - defer tmpFile.Close() - return tmpFile.Name() - }, - wantError: "template file must have .json extension", + name: "nil_certificate", + cert: nil, + path: filepath.Join(t.TempDir(), "nil.pem"), + wantErr: "certificate is nil", }, { - name: "valid_JSON_template", - setup: func() string { - tmpFile, err := os.CreateTemp("", "template-*.json") - require.NoError(t, err) - defer tmpFile.Close() - - content := []byte(`{ - "subject": { - "commonName": "Test CA" - }, - "issuer": { - "commonName": "Test CA" - }, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": { - "isCA": true, - "maxPathLen": 1 - }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2025-01-01T00:00:00Z" - }`) - _, err = tmpFile.Write(content) - require.NoError(t, err) - - return tmpFile.Name() - }, + name: "certificate_with_no_raw_data", + cert: &x509.Certificate{}, + path: filepath.Join(t.TempDir(), "no-raw.pem"), + wantErr: "certificate has no raw data", + }, + { + name: "invalid_file_path", + cert: &x509.Certificate{Raw: []byte("test")}, + path: "/nonexistent/dir/cert.pem", + wantErr: "no such file or directory", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - path := tt.setup() - defer func() { - if _, err := os.Stat(path); err == nil { - os.Remove(path) - } - }() - - err := ValidateTemplatePath(path) - if tt.wantError != "" { + err := WriteCertificateToFile(tt.cert, tt.path) + if tt.wantErr != "" { require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) + assert.Contains(t, err.Error(), tt.wantErr) } else { require.NoError(t, err) + data, err := os.ReadFile(tt.path) + require.NoError(t, err) + block, _ := pem.Decode(data) + require.NotNil(t, block) + require.Equal(t, "CERTIFICATE", block.Type) } }) } } - -func TestCreateCertificates(t *testing.T) { - defer func() { InitKMS = originalInitKMS }() - InitKMS = func(_ context.Context, _ KMSConfig) (signature.SignerVerifier, error) { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - return &mockSignerVerifier{key: key}, 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 - wantError bool - }{ - { - 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", - }, - }, - 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", - }, - }, - 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", - }, - }, - 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, - }, - { - 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"}, - }, - 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"}, - }, - wantError: true, - }, - { - 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", - }, - 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", - }, - }, - wantError: true, - }, - { - name: "gcp_kms_with_uri", - config: KMSConfig{ - Type: "gcpkms", - RootKeyID: "gcpkms://projects/test-project/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", - }, - 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: true, - }, - } - - 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 { - require.NoError(t, err) - } - }) - } -} - -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 - 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 TestValidateTemplateWithValidFields(t *testing.T) { - 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, - }, - } - - err := ValidateTemplate(template, nil, "root") - require.NoError(t, err) -} - -func TestValidateTemplateWithDurationBasedValidity(t *testing.T) { - 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, - }, - } - - 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, - }, - } - - 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(), "Fulcio leaf certificates must have codeSign extended key usage") -} - -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) { - tests := []struct { - name string - cert *x509.Certificate - path string - wantError string - wantType string - }{ - { - 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", - }, - }, - wantError: "gcpkms RootKeyID must start with 'projects/'", - }, - { - name: "azure_kms_missing_tenant_id", - config: KMSConfig{ - Type: "azurekms", - RootKeyID: "azurekms:name=test-key;vault=test-vault", - LeafKeyID: "azurekms:name=test-leaf-key;vault=test-vault", - Options: map[string]string{}, - }, - wantError: "azure-tenant-id is required for Azure KMS", - }, - { - 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", - }, - }, - wantError: "vault-token is required for HashiVault KMS", - }, - { - 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", - }, - }, - wantError: "azurekms RootKeyID must start with 'azurekms:name='", - }, - { - 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", - }, - }, - wantError: "hashivault RootKeyID must be in format: transit/keys/keyname", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidateKMSConfig(tt.config) - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - }) - } -} - -func TestWriteCertificateToFileAdditionalErrors(t *testing.T) { - tests := []struct { - name string - cert *x509.Certificate - filename string - wantError string - }{ - { - name: "invalid_directory", - cert: &x509.Certificate{ - Raw: []byte("test"), - IsCA: true, - }, - filename: "/nonexistent/directory/cert.pem", - wantError: "failed to create file", - }, - { - name: "nil_certificate", - cert: nil, - filename: "test.pem", - wantError: "certificate cannot be nil", - }, - { - name: "empty_raw_data", - cert: &x509.Certificate{ - IsCA: true, - }, - filename: "test.pem", - wantError: "certificate has no raw data", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := WriteCertificateToFile(tt.cert, tt.filename) - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - }) - } -} - -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 - } - }` - - 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": 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 := `{ - "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(), "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.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 - } - }` - - 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") -} diff --git a/pkg/certmaker/template.go b/pkg/certmaker/template.go index 625e92fb5..15655194a 100644 --- a/pkg/certmaker/template.go +++ b/pkg/certmaker/template.go @@ -18,249 +18,143 @@ package certmaker import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "crypto/x509" - "crypto/x509/pkix" - "encoding/json" + _ "embed" "fmt" - "math/big" "os" "time" + + "go.step.sm/crypto/x509util" ) -// CertificateTemplate defines the structure for the JSON certificate templates -type CertificateTemplate struct { - Subject struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - } `json:"subject"` - Issuer struct { - CommonName string `json:"commonName"` - } `json:"issuer"` - CertLifetime string `json:"certLife"` // e.g. "8760h" for 1 year - KeyUsage []string `json:"keyUsage"` - BasicConstraints struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - } `json:"basicConstraints"` - Extensions []struct { - ID string `json:"id"` - Critical bool `json:"critical"` - Value string `json:"value"` - } `json:"extensions,omitempty"` - ExtKeyUsage []string `json:"extKeyUsage,omitempty"` -} +//go:embed templates/root-template.json +var rootTemplate string -// ParseTemplate creates an x509 certificate from JSON template -func ParseTemplate(filename string, parent *x509.Certificate) (*x509.Certificate, error) { - content, err := os.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("error reading template file: %w", err) +//go:embed templates/intermediate-template.json +var intermediateTemplate string + +//go:embed templates/leaf-template.json +var leafTemplate string + +func ParseTemplate(input interface{}, parent *x509.Certificate, notAfter time.Time, publicKey crypto.PublicKey, commonName string) (*x509.Certificate, error) { + var content string + + switch v := input.(type) { + case string: + content = v + case []byte: + content = string(v) + default: + return nil, fmt.Errorf("input must be either a template string or template content ([]byte)") } - var tmpl CertificateTemplate - if err := json.Unmarshal(content, &tmpl); err != nil { - return nil, fmt.Errorf("error parsing template JSON: %w", err) + // Parse/validate template + if err := x509util.ValidateTemplate([]byte(content)); err != nil { + return nil, fmt.Errorf("error parsing template: %w", err) } - certType := "root" - if parent != nil { - if tmpl.BasicConstraints.IsCA { - certType = "intermediate" + // Get cert life and subject from template + data := x509util.NewTemplateData() + if commonName != "" { + fmt.Printf("Using CN from CLI: %s\n", commonName) + data.SetSubject(x509util.Subject{CommonName: commonName}) + } else { + // Get CN from template + cert, err := x509util.NewCertificateFromX509(&x509.Certificate{}, x509util.WithTemplate(content, data)) + if err == nil && cert != nil { + fmt.Printf("Using CN from template: %s\n", cert.Subject.CommonName) + data.SetSubject(x509util.Subject{CommonName: cert.Subject.CommonName}) } else { - certType = "leaf" + fmt.Printf("Using CN from template: \n") } } - if err := ValidateTemplate(&tmpl, parent, certType); err != nil { - return nil, err + // Create base cert with public key + baseCert := &x509.Certificate{ + PublicKey: publicKey, + PublicKeyAlgorithm: determinePublicKeyAlgorithm(publicKey), + NotBefore: time.Now().UTC(), + NotAfter: notAfter, } - return CreateCertificateFromTemplate(&tmpl, parent) -} + cert, err := x509util.NewCertificateFromX509(baseCert, x509util.WithTemplate(content, data)) + if err != nil { + return nil, fmt.Errorf("error parsing template: %w", err) + } -// ValidateTemplate performs validation checks on the certificate template. -func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate, certType string) error { - var notBefore, notAfter time.Time + x509Cert := cert.GetCertificate() - if tmpl.CertLifetime == "" { - return fmt.Errorf("certLife must be specified") + // Set parent cert info + if parent != nil { + x509Cert.Issuer = parent.Subject + x509Cert.AuthorityKeyId = parent.SubjectKeyId } - duration, err := time.ParseDuration(tmpl.CertLifetime) - if err != nil { - return fmt.Errorf("invalid certLife format: %w", err) + // Ensure cert life is set + x509Cert.NotBefore = baseCert.NotBefore + x509Cert.NotAfter = baseCert.NotAfter + + return x509Cert, nil +} + +func determinePublicKeyAlgorithm(publicKey crypto.PublicKey) x509.PublicKeyAlgorithm { + switch publicKey.(type) { + case *ecdsa.PublicKey: + return x509.ECDSA + case *rsa.PublicKey: + return x509.RSA + case ed25519.PublicKey: + return x509.Ed25519 + default: + return x509.ECDSA // Default to ECDSA if key type is unknown } - if duration <= 0 { - return fmt.Errorf("certLife must be positive") +} + +// Performs validation checks on the cert template +func ValidateTemplate(filename string, parent *x509.Certificate, certType string) error { + content, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("error reading template file: %w", err) } - notBefore = time.Now().UTC() - notAfter = notBefore.Add(duration) - if tmpl.Subject.CommonName == "" { - return fmt.Errorf("template subject.commonName cannot be empty") + if err := x509util.ValidateTemplate(content); err != nil { + return fmt.Errorf("error validating template: %w", err) } switch certType { case "root": - if !tmpl.BasicConstraints.IsCA { - return fmt.Errorf("root certificate must be a CA") - } - 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 - var akidValue, skidValue string - for _, ext := range tmpl.Extensions { - if ext.ID == "2.5.29.35" { // AKID OID - hasAKID = true - akidValue = ext.Value - } else if ext.ID == "2.5.29.14" { // SKID OID - hasSKID = true - skidValue = ext.Value - } - } - if hasAKID && hasSKID && akidValue != skidValue { - return fmt.Errorf("root certificate SKID and AKID must match") - } + if parent != nil { + return fmt.Errorf("root certificate cannot have a parent") } case "intermediate": if parent == nil { - return fmt.Errorf("parent certificate is required for non-root certificates") - } - if !tmpl.BasicConstraints.IsCA { - return fmt.Errorf("intermediate certificate must be a CA") - } - if tmpl.BasicConstraints.MaxPathLen != 0 { - return fmt.Errorf("intermediate CA MaxPathLen must be 0") - } - if !containsKeyUsage(tmpl.KeyUsage, "certSign") { - return fmt.Errorf("intermediate CA certificate must have certSign key usage") + return fmt.Errorf("intermediate certificate must have a parent") } case "leaf": if parent == nil { - return fmt.Errorf("parent certificate is required for non-root certificates") - } - if tmpl.BasicConstraints.IsCA { - return fmt.Errorf("leaf certificate cannot be a CA") - } - if containsKeyUsage(tmpl.KeyUsage, "certSign") { - return fmt.Errorf("leaf certificate cannot have certSign key usage") - } - hasCodeSigning := false - for _, usage := range tmpl.ExtKeyUsage { - if usage == "CodeSigning" { - hasCodeSigning = true - break - } - } - if !hasCodeSigning { - return fmt.Errorf("Fulcio leaf certificates must have codeSign extended key usage") + return fmt.Errorf("leaf certificate must have a parent") } default: return fmt.Errorf("invalid certificate type: %s", certType) } - // Basic CA validation - if tmpl.BasicConstraints.IsCA { - if len(tmpl.KeyUsage) == 0 { - return fmt.Errorf("CA certificate must specify at least one key usage") - } - if !containsKeyUsage(tmpl.KeyUsage, "certSign") { - return fmt.Errorf("CA certificate must have certSign key usage") - } - } - - // Time validation against parent - if parent != nil { - if notBefore.Before(parent.NotBefore) { - return fmt.Errorf("certificate notBefore time cannot be before parent's notBefore time") - } - if notAfter.After(parent.NotAfter) { - return fmt.Errorf("certificate notAfter time cannot be after parent's notAfter time") - } - } - return nil } -// 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 - } - } - - // Set extended key usages - for _, usage := range extKeyUsages { - if usage == "CodeSigning" { - cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageCodeSigning) - } - } -} - -// 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{ - Country: tmpl.Subject.Country, - Organization: tmpl.Subject.Organization, - OrganizationalUnit: tmpl.Subject.OrganizationalUnit, - CommonName: tmpl.Subject.CommonName, - }, - Issuer: func() pkix.Name { - if parent != nil { - return parent.Subject - } - return pkix.Name{CommonName: tmpl.Issuer.CommonName} - }(), - SerialNumber: big.NewInt(time.Now().Unix()), - NotBefore: notBefore, - NotAfter: notAfter, - BasicConstraintsValid: true, - IsCA: tmpl.BasicConstraints.IsCA, - } - - if tmpl.BasicConstraints.IsCA { - cert.MaxPathLen = tmpl.BasicConstraints.MaxPathLen - cert.MaxPathLenZero = tmpl.BasicConstraints.MaxPathLen == 0 - } - - SetCertificateUsages(cert, tmpl.KeyUsage, tmpl.ExtKeyUsage) - - return cert, nil -} - -// Helper function to check if a key usage is present -func containsKeyUsage(usages []string, target string) bool { - for _, usage := range usages { - if usage == target { - return true - } +// Returns a default JSON template string for the specified cert type +func GetDefaultTemplate(certType string) (string, error) { + switch certType { + case "root": + return rootTemplate, nil + case "intermediate": + return intermediateTemplate, nil + case "leaf": + return leafTemplate, nil + default: + return "", fmt.Errorf("invalid certificate type: %s", certType) } - return false } diff --git a/pkg/certmaker/template_test.go b/pkg/certmaker/template_test.go index cb6faaf6f..d8f9b6529 100644 --- a/pkg/certmaker/template_test.go +++ b/pkg/certmaker/template_test.go @@ -13,688 +13,308 @@ // limitations under the License. // +// Package certmaker provides template parsing and certificate generation functionality +// for creating X.509 certificates from JSON templates per RFC3161 standards. package certmaker import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "math/big" "os" - "strings" + "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.step.sm/crypto/x509util" ) -func TestValidateTemplateFields(t *testing.T) { +func TestParseTemplate(t *testing.T) { + tmpDir := t.TempDir() + + validTemplate := `{ + "subject": { + "commonName": "{{ .Subject.CommonName }}" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` + + invalidTemplate := `{ + invalid json + }` + + validPath := filepath.Join(tmpDir, "valid.json") + invalidPath := filepath.Join(tmpDir, "invalid.json") + err := os.WriteFile(validPath, []byte(validTemplate), 0600) + require.NoError(t, err) + err = os.WriteFile(invalidPath, []byte(invalidTemplate), 0600) + require.NoError(t, err) + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tests := []struct { - name string - tmpl *CertificateTemplate - parent *x509.Certificate - certType string - wantError string + name string + filename string + parent *x509.Certificate + notAfter time.Time + publicKey crypto.PublicKey + commonName string + wantError string }{ { - name: "valid_root_CA", - 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"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{IsCA: true}, - }, - certType: "root", - }, - { - name: "missing_subject_common_name", - tmpl: &CertificateTemplate{ - Subject: struct { - Country []string `json:"country,omitempty"` - Organization []string `json:"organization,omitempty"` - OrganizationalUnit []string `json:"organizationalUnit,omitempty"` - CommonName string `json:"commonName"` - }{}, - CertLifetime: "8760h", - }, - certType: "root", - wantError: "subject.commonName cannot be empty", - }, - { - name: "missing_issuer_common_name_for_root", - 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", - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{IsCA: true}, - }, - certType: "root", - wantError: "issuer.commonName cannot be empty for root certificate", + name: "valid template", + filename: validPath, + parent: nil, + notAfter: time.Now().Add(time.Hour * 24), + publicKey: key.Public(), + commonName: "Test CA", }, { - name: "CA_without_key_usage", - 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"}, - KeyUsage: []string{}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{IsCA: true}, - CertLifetime: "8760h", - }, - certType: "root", - wantError: "CA certificate must specify at least one key usage", + name: "invalid template", + filename: invalidPath, + parent: nil, + notAfter: time.Now().Add(time.Hour * 24), + publicKey: key.Public(), + commonName: "Test CA", + wantError: "error parsing template: error unmarshaling certificate", }, { - name: "CA_without_certSign_usage", - 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"}, - KeyUsage: []string{"crlSign"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{IsCA: true}, - CertLifetime: "8760h", - }, - certType: "root", - wantError: "CA certificate must have certSign key usage", - }, - { - name: "leaf_with_certSign_usage", - 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"}, - KeyUsage: []string{"certSign", "digitalSignature"}, - ExtKeyUsage: []string{"CodeSigning"}, - CertLifetime: "8760h", - 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), - }, - certType: "leaf", - wantError: "leaf certificate cannot have certSign key usage", - }, - { - name: "invalid_certLife_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", - }, - certType: "root", - wantError: "invalid certLife format", - }, - { - name: "leaf_without_CodeSigning_usage", - 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"}, - KeyUsage: []string{"digitalSignature"}, - CertLifetime: "8760h", - 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), - }, - certType: "leaf", - wantError: "Fulcio leaf certificates must have codeSign extended key usage", - }, - { - name: "valid_intermediate_CA", - 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 Intermediate CA"}, - Issuer: struct { - CommonName string `json:"commonName"` - }{CommonName: "Test Root CA"}, - CertLifetime: "8760h", - KeyUsage: []string{"certSign", "crlSign"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{ - IsCA: true, - MaxPathLen: 0, - }, - }, - parent: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Test Root CA", - }, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour * 24 * 365 * 2), // 2 years - }, - certType: "intermediate", - }, - { - name: "intermediate_with_wrong_MaxPathLen", - 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 Intermediate CA"}, - Issuer: struct { - CommonName string `json:"commonName"` - }{CommonName: "Test Root CA"}, - CertLifetime: "8760h", - KeyUsage: []string{"certSign", "crlSign"}, - BasicConstraints: struct { - IsCA bool `json:"isCA"` - MaxPathLen int `json:"maxPathLen"` - }{ - IsCA: true, - MaxPathLen: 1, - }, - }, - parent: &x509.Certificate{ - Subject: pkix.Name{ - CommonName: "Test Root CA", - }, - 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", - 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"}, - 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), // 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", + name: "nonexistent file", + filename: "nonexistent.json", + parent: nil, + notAfter: time.Now().Add(time.Hour * 24), + publicKey: key.Public(), + commonName: "Test CA", + wantError: "input must be either a template string or template content", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateTemplate(tt.tmpl, tt.parent, tt.certType) + var content interface{} + if tt.filename == "nonexistent.json" { + content = struct{}{} // Use invalid type to trigger type error + } else { + // Read the file content for valid cases + fileContent, err := os.ReadFile(tt.filename) + if err != nil { + t.Fatalf("failed to read test file: %v", err) + } + content = string(fileContent) + } + + cert, err := ParseTemplate(content, tt.parent, tt.notAfter, tt.publicKey, tt.commonName) if tt.wantError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantError) } else { require.NoError(t, err) + require.NotNil(t, cert) + assert.Equal(t, tt.commonName, cert.Subject.CommonName) + assert.Equal(t, tt.publicKey, cert.PublicKey) + assert.Equal(t, tt.notAfter, cert.NotAfter) } }) } } -func TestParseTemplateErrors(t *testing.T) { - tests := []struct { - name string - content string - wantError string - }{ - { - name: "invalid JSON", - content: `{"invalid": json}`, - wantError: "invalid character", - }, - { - name: "missing_time_fields", - content: `{ - "subject": { - "commonName": "Test CA" - }, - "certLife": "", - "keyUsage": ["certSign"] - }`, - wantError: "certLife must be specified", +func TestValidateTemplate(t *testing.T) { + tmpDir := t.TempDir() + + rootTemplate := `{ + "subject": { + "commonName": "Test Root CA" }, - { - name: "invalid time format", - content: `{ - "subject": { - "commonName": "Test" - }, - "certLife": "invalid" - }`, - wantError: "invalid certLife format", + "issuer": { + "commonName": "Test Root CA" }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpFile, err := os.CreateTemp("", "cert-template-*.json") - require.NoError(t, err) - defer os.Remove(tmpFile.Name()) - - err = os.WriteFile(tmpFile.Name(), []byte(tt.content), 0600) - require.NoError(t, err) - - _, err = ParseTemplate(tmpFile.Name(), nil) - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) - }) - } - - _, err := ParseTemplate("nonexistent.json", nil) - if err == nil { - t.Errorf("expected error, got nil") - } else if !strings.Contains(err.Error(), "error reading template file") { - t.Errorf("error %q should contain %q", err.Error(), "error reading template file") - } -} + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + } + }` -func TestInvalidCertificateType(t *testing.T) { - 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: "8760h", - } + leafTemplate := `{ + "subject": { + "commonName": "Test Leaf" + }, + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning"], + "basicConstraints": { + "isCA": false + } + }` - err := ValidateTemplate(tmpl, nil, "invalid") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid certificate type") -} + rootTmplPath := filepath.Join(tmpDir, "root.json") + leafTmplPath := filepath.Join(tmpDir, "leaf.json") + err := os.WriteFile(rootTmplPath, []byte(rootTemplate), 0600) + require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(leafTemplate), 0600) + require.NoError(t, err) -func TestContainsExtKeyUsage(t *testing.T) { - assert.False(t, containsExtKeyUsage(nil, "CodeSigning"), "empty list (nil) should return false") - assert.False(t, containsExtKeyUsage([]string{}, "CodeSigning"), "empty list should return false") - assert.True(t, containsExtKeyUsage([]string{"CodeSigning"}, "CodeSigning"), "should find matching usage") - assert.False(t, containsExtKeyUsage([]string{"OtherUsage"}, "CodeSigning"), "should not find non-matching usage") -} + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) -func containsExtKeyUsage(usages []string, target string) bool { - for _, usage := range usages { - if usage == target { - return true - } + parent := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test Parent CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + PublicKey: key.Public(), } - return false -} -func TestCreateCertificateFromTemplate(t *testing.T) { tests := []struct { name string - tmpl *CertificateTemplate + filename string parent *x509.Certificate - wantError bool + certType string + wantError string }{ { - name: "valid leaf certificate", - 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"}, - Organization: []string{"Test Org"}, - OrganizationalUnit: []string{"Test Unit"}, - CommonName: "Test Leaf", - }, - 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), - }, - wantError: false, + name: "valid root template", + filename: rootTmplPath, + parent: nil, + certType: "root", }, { - name: "valid root certificate", - 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: "root with parent", + filename: rootTmplPath, + parent: parent, + certType: "root", + wantError: "root certificate cannot have a parent", }, { - name: "invalid time 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, + name: "valid leaf template", + filename: leafTmplPath, + parent: parent, + certType: "leaf", }, { - 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: "leaf without parent", + filename: leafTmplPath, + parent: nil, + certType: "leaf", + wantError: "leaf certificate must have a parent", }, { - 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, + name: "invalid cert type", + filename: leafTmplPath, + parent: parent, + certType: "invalid", + wantError: "invalid certificate type: invalid", + }, + { + name: "nonexistent file", + filename: "nonexistent.json", + parent: parent, + certType: "leaf", + wantError: "error reading template file", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cert, err := CreateCertificateFromTemplate(tt.tmpl, tt.parent) - if tt.wantError { + err := ValidateTemplate(tt.filename, tt.parent, tt.certType) + if tt.wantError != "" { require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) } 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 TestSetCertificateUsages(t *testing.T) { - cert := &x509.Certificate{} +func TestValidateTemplatePath(t *testing.T) { + tmpDir := t.TempDir() - SetCertificateUsages(cert, []string{"certSign", "crlSign", "digitalSignature"}, nil) - if cert.KeyUsage&x509.KeyUsageCertSign == 0 { - t.Error("expected KeyUsageCertSign to be set") - } - if cert.KeyUsage&x509.KeyUsageCRLSign == 0 { - t.Error("expected KeyUsageCRLSign to be set") - } - if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { - t.Error("expected KeyUsageDigitalSignature to be set") - } + validTemplate := `{ + "subject": { + "commonName": "Test CA" + }, + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + }, + "validity": { + "notBefore": "{{ now }}", + "notAfter": "{{ .NotAfter }}" + } + }` - newCert := &x509.Certificate{} - SetCertificateUsages(newCert, nil, nil) - if newCert.KeyUsage != x509.KeyUsage(0) { - t.Error("expected no key usages to be set") - } - if len(newCert.ExtKeyUsage) != 0 { - t.Error("expected no extended key usages to be set") - } + invalidTemplate := `{ + "subject": { + "commonName": "Test CA", + }, + invalid json + }` - // 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") - } -} + validPath := filepath.Join(tmpDir, "valid.json") + invalidPath := filepath.Join(tmpDir, "invalid.json") + wrongExtPath := filepath.Join(tmpDir, "wrong.txt") + + require.NoError(t, os.WriteFile(validPath, []byte(validTemplate), 0600)) + require.NoError(t, os.WriteFile(invalidPath, []byte(invalidTemplate), 0600)) + require.NoError(t, os.WriteFile(wrongExtPath, []byte(validTemplate), 0600)) -func TestValidateTemplateWithDurationAndTimestamps(t *testing.T) { tests := []struct { name string - tmpl *CertificateTemplate - parent *x509.Certificate - certType string - wantError string + path string + wantError bool + errMsg 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: "valid template", + path: validPath, + wantError: false, }, { - 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: "nonexistent file", + path: "nonexistent.json", + wantError: true, + errMsg: "template not found at nonexistent.json", }, { - 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: "wrong file extension", + path: wrongExtPath, + wantError: true, + errMsg: "template file must have .json extension", }, { - 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", + name: "invalid JSON", + path: invalidPath, + wantError: true, + errMsg: "invalid JSON in template file", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateTemplate(tt.tmpl, tt.parent, tt.certType) - if tt.wantError != "" { + err := ValidateTemplatePath(tt.path) + if tt.wantError { require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantError) + assert.Contains(t, err.Error(), tt.errMsg) } else { require.NoError(t, err) } @@ -702,191 +322,98 @@ func TestValidateTemplateWithDurationAndTimestamps(t *testing.T) { } } -func TestValidateTemplateWithExtendedKeyUsage(t *testing.T) { +func TestDeterminePublicKeyAlgorithm(t *testing.T) { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + _, ed25519Key, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + tests := []struct { name string - template *CertificateTemplate - parent *x509.Certificate - certType string - wantError string + publicKey crypto.PublicKey + want x509.PublicKeyAlgorithm }{ { - 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: "ECDSA key", + publicKey: ecKey.Public(), + want: x509.ECDSA, }, { - 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: "RSA key", + publicKey: rsaKey.Public(), + want: x509.RSA, }, { - 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", + name: "Ed25519 key", + publicKey: ed25519Key, + want: 3, // x509.Ed25519 + }, + { + name: "Unknown key type", + publicKey: struct{}{}, + want: x509.ECDSA, }, } 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) - } + got := determinePublicKeyAlgorithm(tt.publicKey) + assert.Equal(t, tt.want, got) }) } } -func TestCreateCertificateFromTemplateWithExtendedFields(t *testing.T) { +func TestGetDefaultTemplate(t *testing.T) { tests := []struct { name string - tmpl *CertificateTemplate - parent *x509.Certificate - wantError bool - checkFunc func(*testing.T, *x509.Certificate) + certType string + wantError string }{ { - 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: "root template", + certType: "root", + }, + { + name: "intermediate template", + certType: "intermediate", + }, + { + name: "leaf template", + certType: "leaf", }, { - 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) - }, + name: "invalid type", + certType: "invalid", + wantError: "invalid certificate type", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cert, err := CreateCertificateFromTemplate(tt.tmpl, tt.parent) - if tt.wantError { + got, err := GetDefaultTemplate(tt.certType) + if tt.wantError != "" { require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantError) } else { require.NoError(t, err) - require.NotNil(t, cert) - if tt.checkFunc != nil { - tt.checkFunc(t, cert) + assert.NotEmpty(t, got) + + err = x509util.ValidateTemplate([]byte(got)) + require.NoError(t, err) + + assert.Contains(t, got, "subject") + assert.Contains(t, got, "keyUsage") + assert.Contains(t, got, "basicConstraints") + + switch tt.certType { + case "root", "intermediate": + assert.Contains(t, got, `"isCA": true`) + case "leaf": + assert.Contains(t, got, `"isCA": false`) } } }) diff --git a/pkg/certmaker/templates/intermediate-template.json b/pkg/certmaker/templates/intermediate-template.json index 4ea1094a9..385fc22bb 100644 --- a/pkg/certmaker/templates/intermediate-template.json +++ b/pkg/certmaker/templates/intermediate-template.json @@ -1,23 +1,9 @@ { "subject": { - "country": [ - "" - ], - "organization": [ - "" - ], - "organizationalUnit": [ - "" - ], - "commonName": "" - }, - "issuer": { - "commonName": "" - }, - "certLife": "43800h", - "basicConstraints": { - "isCA": true, - "maxPathLen": 0 + "country": [], + "organization": [], + "organizationalUnit": [], + "commonName": "{{ .Subject.CommonName }}" }, "keyUsage": [ "certSign", @@ -25,5 +11,9 @@ ], "extKeyUsage": [ "CodeSigning" - ] + ], + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + } } \ No newline at end of file diff --git a/pkg/certmaker/templates/leaf-template.json b/pkg/certmaker/templates/leaf-template.json index f93a0a03e..396950a80 100644 --- a/pkg/certmaker/templates/leaf-template.json +++ b/pkg/certmaker/templates/leaf-template.json @@ -1,27 +1,18 @@ { "subject": { - "country": [ - "" - ], - "organization": [ - "" - ], - "organizationalUnit": [ - "" - ], - "commonName": "" - }, - "issuer": { - "commonName": "" - }, - "certLife": "8760h", - "basicConstraints": { - "isCA": false + "country": [], + "organization": [], + "organizationalUnit": [], + "commonName": "{{ .Subject.CommonName }}" }, "keyUsage": [ "digitalSignature" ], "extKeyUsage": [ "CodeSigning" - ] + ], + "basicConstraints": { + "isCA": false, + "maxPathLen": 0 + } } \ No newline at end of file diff --git a/pkg/certmaker/templates/root-template.json b/pkg/certmaker/templates/root-template.json index fdfb070c4..92fdefb2c 100644 --- a/pkg/certmaker/templates/root-template.json +++ b/pkg/certmaker/templates/root-template.json @@ -1,20 +1,10 @@ { "subject": { - "country": [ - "" - ], - "organization": [ - "" - ], - "organizationalUnit": [ - "" - ], - "commonName": "" + "country": [], + "organization": [], + "organizationalUnit": [], + "commonName": "{{ .Subject.CommonName }}" }, - "issuer": { - "commonName": "" - }, - "certLife": "87600h", "keyUsage": [ "certSign", "crlSign"