Repository: sigstore/fulcio (go)

Issue:
make leaf optional in certificate-maker
relates to https://github.com/sigstore/fulcio/pull/1870 / https://github.com/sigstore/timestamp-authority/pull/889

**Description**

<!--What needs to be improved? What should be done? Has there been any related work
before? Any relevant links or material?
-->

it's been recommended to make the certificate-maker more agnostic so that it may live in one place rather two with the  respective templates living w/ the service (e.g. fulcio and tsa).

the certificate-maker would allow both leaf and intermediate certs be options rather than the current behavior of leafs being mandatory.

- [Roots are always required, intermediates are optional, and leafs are optional (for a CA vs TSA).
](https://github.com/sigstore/timestamp-authority/pull/889#issuecomment-2613482618)

cc @haydentherapper 

Pull request:
feat: makes leaf optional and other changes. 
<!--
Thanks for opening a pull request! Please do not just delete this text.  The three fields below are mandatory.

Please remember to:
- This PR requires an issue. If it is a new feature, the issue should proceed the PR and will have allowed sufficient time for discussions to take place. Please use
issue tags such as "Closes #XYZ" or "Resolves sigstore/repo-name#XYZ".
  [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
- ensure your commits are signed-off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits
- lastly, ensure there are no merge commits!

Thank you :)
-->
closes #1930 
#### Summary
<!--
 Explain the **motivation** for making this change. What existing problem does the pull request solve? How can reviewers test this PR?
-->

makes leaf optional as per @haydentherapper's request as well as updates docs etc. 

#### Release Note
<!--
Add a release note for each of the following conditions:

* Config changes (additions, deletions, updates)
* API additions—new endpoint, new response fields, or newly accepted request parameters
* Database changes (any)
* Websocket additions or changes
* Anything noteworthy to an administrator running private sigstore instances (err on the side of over-communicating)
* New features and improvements, including behavioural changes, UI changes and CLI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes

If no release notes are required write NONE. Use past-tense.

-->

#### Documentation
<!--

Does this change require an update to documentation? How will users implement your new feature?

Please reference a PR within https://docs.sigstore.dev

-->


Patch:
diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml
index 3f87eed..0c700cd 100644
--- a/.github/workflows/container-build.yml
+++ b/.github/workflows/container-build.yml
@@ -53,7 +53,7 @@ jobs:
       - uses: ko-build/setup-ko@d982fec422852203cfb2053a8ec6ad302280d04d # v0.8
 
       - name: Set up Cloud SDK
-        uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7
+        uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
         with:
           workload_identity_provider: 'projects/498091336538/locations/global/workloadIdentityPools/githubactions/providers/sigstore-fulcio'
           service_account: 'github-actions-fulcio@projectsigstore.iam.gserviceaccount.com'
diff --git a/.gitignore b/.gitignore
index ba4c8f0..f392acd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,4 @@ hack/tools/bin
 
 # macOS
 .DS_Store
-fulcio-certificate-maker
+certificate-maker
diff --git a/Makefile b/Makefile
index bb08d96..987de29 100644
--- a/Makefile
+++ b/Makefile
@@ -78,7 +78,7 @@ fulcio: $(SRCS) ## Build Fulcio for local tests
 	go build -trimpath -ldflags "$(LDFLAGS)"
 
 cert-maker: ## Build the Fulcio Certificate Maker tool
-	go build -trimpath -ldflags "$(LDFLAGS)" -o fulcio-certificate-maker ./cmd/certificate_maker
+	go build -trimpath -ldflags "$(LDFLAGS)" -o certificate-maker ./cmd/certificate_maker
 
 test: ## Runs go test
 	go test ./...
@@ -86,7 +86,10 @@ test: ## Runs go test
 clean: ## Clean the workspace
 	rm -rf dist
 	rm -rf fulcio
-	rm -rf fulcio-certificate-maker
+	rm -rf certificate-maker
+	rm -rf root.pem
+	rm -rf intermediate.pem
+	rm -rf leaf.pem
 
 clean-gen: clean
 	rm -rf $(shell find pkg/generated -iname "*.go") *.swagger.json
diff --git a/README.md b/README.md
index 68c4754..dc62182 100644
--- a/README.md
+++ b/README.md
@@ -77,10 +77,13 @@ mygUY7Ii2zbdCdliiow=
 
 ### Certificate Maker
 
-The Fulcio's Certificate Maker is a tool for creating Fulcio compliant certificate chains. It supports:
+Certificate Maker is a tool for creating [Fulcio compliant certificate chains](docs/certificate-specification.md). It supports:
 
-* Two-level chains (root -> leaf)
-* Three-level chains (root -> intermediate -> leaf)
+* Two-level chains:
+  * root → leaf
+  * root → intermediate
+* Three-level chains:
+  * root → intermediate → leaf
 * Multiple KMS providers (AWS, Google Cloud, Azure, HashiCorp Vault)
 
 For detailed usage instructions and examples, see the [Certificate Maker documentation](docs/certificate-maker.md).
diff --git a/cmd/certificate_maker/certificate_maker.go b/cmd/certificate_maker/certificate_maker.go
index d30e0f6..7aa5f55 100644
--- a/cmd/certificate_maker/certificate_maker.go
+++ b/cmd/certificate_maker/certificate_maker.go
@@ -18,7 +18,6 @@
 package main
 
 import (
-	"context"
 	"fmt"
 	"os"
 	"strings"
@@ -38,7 +37,7 @@ var (
 	version string
 
 	rootCmd = &cobra.Command{
-		Use:     "fulcio-certificate-maker",
+		Use:     "certificate-maker",
 		Short:   "Create certificate chains for Fulcio",
 		Long:    `A tool for creating root, intermediate, and leaf certificates for Fulcio with code signing capabilities`,
 		Version: version,
@@ -50,7 +49,7 @@ var (
 		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"`,
+Example: certificate-maker create "https://fulcio.example.com"`,
 		Args: cobra.RangeArgs(0, 1),
 		RunE: runCreate,
 	}
@@ -137,8 +136,6 @@ func init() {
 
 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
@@ -148,12 +145,10 @@ func runCreate(_ *cobra.Command, args []string) error {
 
 	// 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"),
-		LeafKeyID:         viper.GetString("leaf-key-id"),
-		Options:           make(map[string]string),
+		CommonName: commonName,
+		Type:       viper.GetString("kms-type"),
+		KeyID:      viper.GetString("root-key-id"),
+		Options:    make(map[string]string),
 	}
 
 	// Handle KMS provider options
@@ -186,11 +181,6 @@ func runCreate(_ *cobra.Command, args []string) error {
 		}
 	}
 
-	km, err := certmaker.InitKMS(ctx, config)
-	if err != nil {
-		return fmt.Errorf("failed to initialize KMS: %w", err)
-	}
-
 	// Get template paths
 	rootTemplate := viper.GetString("root-template")
 	intermediateTemplate := viper.GetString("intermediate-template")
@@ -213,7 +203,7 @@ func runCreate(_ *cobra.Command, args []string) error {
 		}
 	}
 
-	return certmaker.CreateCertificates(km, config,
+	return certmaker.CreateCertificates(config,
 		rootTemplate,
 		leafTemplate,
 		viper.GetString("root-cert"),
@@ -221,6 +211,7 @@ func runCreate(_ *cobra.Command, args []string) error {
 		viper.GetString("intermediate-key-id"),
 		viper.GetString("intermediate-template"),
 		viper.GetString("intermediate-cert"),
+		viper.GetString("leaf-key-id"),
 		viper.GetDuration("root-lifetime"),
 		viper.GetDuration("intermediate-lifetime"),
 		viper.GetDuration("leaf-lifetime"))
diff --git a/docs/certificate-maker.md b/docs/certificate-maker.md
index 96f65a1..26812be 100644
--- a/docs/certificate-maker.md
+++ b/docs/certificate-maker.md
@@ -1,11 +1,8 @@
-# Fulcio Certificate Maker
+# Certificate Maker
 
-This tool creates root, intermediate (optional), and leaf certificates for Fulcio ([certificate requirements](certificate-specification.md)):
+This tool creates certificates (root, intermediate, and leaf) that meet Fulcio's ([certificate requirements](certificate-specification.md)).
 
-- 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. The tool includes embedded default templates that are compiled into the binary, making it ready to use without external template files.
+It 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
 
@@ -18,7 +15,7 @@ Build the binary:
 
 ```bash
 make cert-maker
-./fulcio-certificate-maker --help
+./certificate-maker --help
 ```
 
 ## Usage
@@ -30,7 +27,7 @@ The tool can be configured using either command-line flags or environment variab
 The `create` command accepts an optional positional argument for the common name:
 
 ```bash
-fulcio-certificate-maker create [common-name]
+./certificate-maker create [common-name]
 ```
 
 If no common name is provided, the values from the templates will be used.
@@ -38,22 +35,27 @@ 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)
+
 - `--root-key-id`: KMS key identifier for root certificate
+- `--intermediate-key-id`: KMS key identifier for intermediate certificate
 - `--leaf-key-id`: KMS key identifier for leaf certificate
+
 - `--aws-region`: AWS region (required for AWS KMS)
 - `--azure-tenant-id`: Azure KMS tenant ID
 - `--gcp-credentials-file`: Path to credentials file (for Google Cloud KMS)
 - `--vault-address`: HashiCorp Vault address
 - `--vault-token`: HashiCorp Vault token
+
 - `--root-template`: Path to root certificate template
-- `--leaf-template`: Path to leaf certificate template
+- `--root-lifetime`: Root certificate lifetime (default: 87600h, 10 years)
 - `--root-cert`: Output path for root certificate (default: root.pem)
-- `--leaf-cert`: Output path for leaf certificate (default: leaf.pem)
-- `--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-template`: Path to leaf certificate template
+- `--leaf-cert`: Output path for leaf certificate (default: leaf.pem)
 - `--leaf-lifetime`: Leaf certificate lifetime (default: 8760h, 1 year)
 
 ### Environment Variables
@@ -261,7 +263,7 @@ Certificate:
 Example with AWS KMS:
 
 ```bash
-fulcio-certificate-maker create "https://fulcio.example.com" \
+./certificate-maker create "https://fulcio.example.com" \
   --kms-type awskms \
   --aws-region us-east-1 \
   --root-key-id alias/fulcio-root \
@@ -275,7 +277,7 @@ fulcio-certificate-maker create "https://fulcio.example.com" \
 Example with Azure KMS:
 
 ```bash
-fulcio-certificate-maker create "https://fulcio.example.com" \
+./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" \
@@ -292,7 +294,7 @@ fulcio-certificate-maker create "https://fulcio.example.com" \
 Example with GCP KMS:
 
 ```bash
-fulcio-certificate-maker create "https://fulcio.example.com" \
+./certificate-maker create "https://fulcio.example.com" \
   --kms-type gcpkms \
   --gcp-credentials-file ~/.config/gcloud/application_default_credentials.json \
   --root-key-id  projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/fulcio-key1/cryptoKeyVersions/<version> \
@@ -309,7 +311,7 @@ fulcio-certificate-maker create "https://fulcio.example.com" \
 Example with HashiCorp Vault KMS:
 
 ```bash
-fulcio-certificate-maker create "https://fulcio.example.com" \
+./certificate-maker create "https://fulcio.example.com" \
   --kms-type hashivault \
   --vault-address http://vault:8200 \
   --vault-token token \
diff --git a/pkg/ca/csc.go b/pkg/ca/csc.go
index da9598b..4c63af8 100644
--- a/pkg/ca/csc.go
+++ b/pkg/ca/csc.go
@@ -68,9 +68,6 @@ func CreateCSCFromDER(cert []byte, chain []*x509.Certificate) (*CodeSigningCerti
 
 	// convert to X509 and store both formats
 	c.FinalChain = chain
-	if err != nil {
-		return nil, err
-	}
 	for _, chainCert := range c.FinalChain {
 		c.finalChainPEM = append(c.finalChainPEM, strings.TrimSpace(string(cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, chainCert.Raw))))
 	}
diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go
index c0f3407..8af160b 100644
--- a/pkg/certmaker/certmaker.go
+++ b/pkg/certmaker/certmaker.go
@@ -49,12 +49,10 @@ type CryptoSignerVerifier interface {
 
 // KMSConfig holds config for KMS providers.
 type KMSConfig struct {
-	CommonName        string
-	Type              string
-	RootKeyID         string
-	IntermediateKeyID string
-	LeafKeyID         string
-	Options           map[string]string
+	CommonName string
+	Type       string
+	KeyID      string
+	Options    map[string]string
 }
 
 // InitKMS initializes KMS provider based on the given config, KMSConfig.
@@ -68,7 +66,7 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif
 
 	switch config.Type {
 	case "awskms":
-		ref := fmt.Sprintf("awskms:///%s", config.RootKeyID)
+		ref := fmt.Sprintf("awskms:///%s", config.KeyID)
 		if awsRegion := config.Options["aws-region"]; awsRegion != "" {
 			os.Setenv("AWS_REGION", awsRegion)
 		}
@@ -78,7 +76,7 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif
 		}
 
 	case "gcpkms":
-		ref := fmt.Sprintf("gcpkms://%s", config.RootKeyID)
+		ref := fmt.Sprintf("gcpkms://%s", config.KeyID)
 		if gcpCredsFile := config.Options["gcp-credentials-file"]; gcpCredsFile != "" {
 			os.Setenv("GCP_CREDENTIALS_FILE", gcpCredsFile)
 		}
@@ -88,13 +86,13 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif
 		}
 
 	case "azurekms":
-		keyURI := config.RootKeyID
-		if strings.HasPrefix(config.RootKeyID, "azurekms:name=") {
-			nameStart := strings.Index(config.RootKeyID, "name=") + 5
-			vaultIndex := strings.Index(config.RootKeyID, ";vault=")
+		keyURI := config.KeyID
+		if strings.HasPrefix(config.KeyID, "azurekms:name=") {
+			nameStart := strings.Index(config.KeyID, "name=") + 5
+			vaultIndex := strings.Index(config.KeyID, ";vault=")
 			if vaultIndex != -1 {
-				keyName := strings.TrimSpace(config.RootKeyID[nameStart:vaultIndex])
-				vaultName := strings.TrimSpace(config.RootKeyID[vaultIndex+7:])
+				keyName := strings.TrimSpace(config.KeyID[nameStart:vaultIndex])
+				vaultName := strings.TrimSpace(config.KeyID[vaultIndex+7:])
 				keyURI = fmt.Sprintf("azurekms://%s.vault.azure.net/%s", vaultName, keyName)
 			}
 		}
@@ -111,7 +109,7 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif
 		}
 
 	case "hashivault":
-		keyURI := fmt.Sprintf("hashivault://%s", config.RootKeyID)
+		keyURI := fmt.Sprintf("hashivault://%s", config.KeyID)
 		if config.Options != nil {
 			if vaultToken := config.Options["vault-token"]; vaultToken != "" {
 				os.Setenv("VAULT_TOKEN", vaultToken)
@@ -141,15 +139,23 @@ var InitKMS = func(ctx context.Context, config KMSConfig) (signature.SignerVerif
 }
 
 // CreateCertificates creates certificates using the provided KMS and templates.
-// It creates 3 certificates (root -> intermediate -> leaf) if intermediateKeyID is provided,
-// otherwise creates just 2 certs (root -> leaf).
-func CreateCertificates(sv signature.SignerVerifier, config KMSConfig,
+// Root certificate is always required.
+// Intermediate and leaf certificates are optional based on provided key IDs and templates.
+func CreateCertificates(config KMSConfig,
 	rootTemplatePath, leafTemplatePath string,
 	rootCertPath, leafCertPath string,
 	intermediateKeyID, intermediateTemplatePath, intermediateCertPath string,
+	leafKeyID string,
 	rootLifetime, intermediateLifetime, leafLifetime time.Duration) error {
 
-	// Create root cert
+	// Initialize root KMS signer
+	rootConfig := config
+	sv, err := InitKMS(context.Background(), rootConfig)
+	if err != nil {
+		return fmt.Errorf("error initializing root KMS: %w", err)
+	}
+
+	// Create root cert (required)
 	rootPubKey, err := sv.PublicKey()
 	if err != nil {
 		return fmt.Errorf("error getting root public key: %w", err)
@@ -172,6 +178,9 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig,
 		if err != nil {
 			return fmt.Errorf("error getting default root template: %w", err)
 		}
+		if defaultTemplate == "" {
+			return fmt.Errorf("root template is required but no template was provided")
+		}
 		rootTemplate = defaultTemplate
 	} else {
 		// Read from FS if path is provided
@@ -200,10 +209,10 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig,
 	var signingCert *x509.Certificate
 	var signingKey crypto.Signer
 
+	// Create intermediate cert (optional)
 	if intermediateKeyID != "" {
-		// Create intermediate cert if key ID is provided
 		intermediateConfig := config
-		intermediateConfig.RootKeyID = intermediateKeyID
+		intermediateConfig.KeyID = intermediateKeyID
 		intermediateSV, err := InitKMS(context.Background(), intermediateConfig)
 		if err != nil {
 			return fmt.Errorf("error initializing intermediate KMS: %w", err)
@@ -262,58 +271,50 @@ func CreateCertificates(sv signature.SignerVerifier, config KMSConfig,
 		signingKey = rootSigner
 	}
 
-	// Create leaf cert
-	leafConfig := config
-	leafConfig.RootKeyID = config.LeafKeyID
-	leafSV, err := InitKMS(context.Background(), leafConfig)
-	if err != nil {
-		return fmt.Errorf("error initializing leaf KMS: %w", err)
-	}
-
-	leafPubKey, err := leafSV.PublicKey()
-	if err != nil {
-		return fmt.Errorf("error getting leaf public key: %w", err)
-	}
+	// Create leaf cert (optional)
+	if leafKeyID != "" {
+		leafConfig := config
+		leafConfig.KeyID = leafKeyID
+		leafSV, err := InitKMS(context.Background(), leafConfig)
+		if err != nil {
+			return fmt.Errorf("error initializing leaf KMS: %w", err)
+		}
 
-	leafCryptoSV, ok := leafSV.(CryptoSignerVerifier)
-	if !ok {
-		return fmt.Errorf("leaf signer does not implement CryptoSigner")
-	}
+		leafPubKey, err := leafSV.PublicKey()
+		if err != nil {
+			return fmt.Errorf("error getting leaf public key: %w", err)
+		}
 
-	_, _, 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)
+		}
 
-	var leafTemplate interface{}
-	if leafTemplatePath == "" {
-		defaultTemplate, err := GetDefaultTemplate("leaf")
+		leafNotAfter := time.Now().UTC().Add(leafLifetime)
+		leafTmpl, err := ParseTemplate(leafTemplate, signingCert, leafNotAfter, leafPubKey, config.CommonName)
 		if err != nil {
-			return fmt.Errorf("error getting default leaf template: %w", err)
+			return fmt.Errorf("error parsing leaf template: %w", err)
 		}
-		leafTemplate = defaultTemplate
-	} else {
-		// Read from FS if path is provided
-		content, err := os.ReadFile(leafTemplatePath)
+
+		leafCert, err := x509util.CreateCertificate(leafTmpl, signingCert, leafPubKey, signingKey)
 		if err != nil {
-			return fmt.Errorf("leaf template error: template not found at %s: %w", leafTemplatePath, err)
+			return fmt.Errorf("error creating leaf certificate: %w", 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)
-	}
-
-	if err := WriteCertificateToFile(leafCert, leafCertPath); err != nil {
-		return fmt.Errorf("error writing leaf certificate: %w", err)
+		if err := WriteCertificateToFile(leafCert, leafCertPath); err != nil {
+			return fmt.Errorf("error writing leaf certificate: %w", err)
+		}
 	}
 
 	return nil
@@ -339,7 +340,7 @@ func WriteCertificateToFile(cert *x509.Certificate, filename string) error {
 	}
 	defer f.Close()
 
-	// get cert type
+	// Get certificate type
 	certType := "leaf"
 	if cert.IsCA {
 		if cert.CheckSignatureFrom(cert) == nil {
@@ -359,6 +360,11 @@ func ValidateKMSConfig(config KMSConfig) error {
 		return fmt.Errorf("KMS type cannot be empty")
 	}
 
+	// Root key is always required
+	if config.KeyID == "" {
+		return fmt.Errorf("KeyID must be specified")
+	}
+
 	switch config.Type {
 	case "awskms":
 		// AWS KMS validation
@@ -387,21 +393,12 @@ func ValidateKMSConfig(config KMSConfig) error {
 			}
 			return nil
 		}
-		if err := validateAWSKeyID(config.RootKeyID, "RootKeyID"); err != nil {
-			return err
-		}
-		if err := validateAWSKeyID(config.IntermediateKeyID, "IntermediateKeyID"); err != nil {
-			return err
-		}
-		if err := validateAWSKeyID(config.LeafKeyID, "LeafKeyID"); err != nil {
+		if err := validateAWSKeyID(config.KeyID, "KeyID"); err != nil {
 			return err
 		}
 
 	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
@@ -423,13 +420,7 @@ func ValidateKMSConfig(config KMSConfig) error {
 			}
 			return nil
 		}
-		if err := validateGCPKeyID(config.RootKeyID, "RootKeyID"); err != nil {
-			return err
-		}
-		if err := validateGCPKeyID(config.IntermediateKeyID, "IntermediateKeyID"); err != nil {
-			return err
-		}
-		if err := validateGCPKeyID(config.LeafKeyID, "LeafKeyID"); err != nil {
+		if err := validateGCPKeyID(config.KeyID, "KeyID"); err != nil {
 			return err
 		}
 
@@ -461,13 +452,7 @@ func ValidateKMSConfig(config KMSConfig) error {
 			}
 			return nil
 		}
-		if err := validateAzureKeyID(config.RootKeyID, "RootKeyID"); err != nil {
-			return err
-		}
-		if err := validateAzureKeyID(config.IntermediateKeyID, "IntermediateKeyID"); err != nil {
-			return err
-		}
-		if err := validateAzureKeyID(config.LeafKeyID, "LeafKeyID"); err != nil {
+		if err := validateAzureKeyID(config.KeyID, "KeyID"); err != nil {
 			return err
 		}
 
@@ -498,13 +483,7 @@ func ValidateKMSConfig(config KMSConfig) error {
 			}
 			return nil
 		}
-		if err := validateHashiVaultKeyID(config.RootKeyID, "RootKeyID"); err != nil {
-			return err
-		}
-		if err := validateHashiVaultKeyID(config.IntermediateKeyID, "IntermediateKeyID"); err != nil {
-			return err
-		}
-		if err := validateHashiVaultKeyID(config.LeafKeyID, "LeafKeyID"); err != nil {
+		if err := validateHashiVaultKeyID(config.KeyID, "KeyID"); err != nil {
 			return err
 		}
 
@@ -512,13 +491,5 @@ func ValidateKMSConfig(config KMSConfig) error {
 		return fmt.Errorf("unsupported KMS type: %s", config.Type)
 	}
 
-	// Check that both key IDs are specified
-	if config.RootKeyID == "" {
-		return fmt.Errorf("RootKeyID must be specified")
-	}
-	if config.LeafKeyID == "" {
-		return fmt.Errorf("LeafKeyID must be specified")
-	}
-
 	return nil
 }
diff --git a/pkg/certmaker/template.go b/pkg/certmaker/template.go
index c1ecedc..a8322ce 100644
--- a/pkg/certmaker/template.go
+++ b/pkg/certmaker/template.go
@@ -134,12 +134,30 @@ func ValidateTemplate(filename string, _ *x509.Certificate, _ string) error {
 func GetDefaultTemplate(certType string) (string, error) {
 	switch certType {
 	case "root":
+		if rootTemplate == "" {
+			return "", fmt.Errorf("root template is required but not found")
+		}
 		return rootTemplate, nil
+	// Both intermediate and leaf are optional - return empty if not found
 	case "intermediate":
+		if intermediateTemplate == "" {
+			return "", nil
+		}
 		return intermediateTemplate, nil
 	case "leaf":
+		if leafTemplate == "" {
+			return "", nil
+		}
 		return leafTemplate, nil
 	default:
 		return "", fmt.Errorf("invalid certificate type: %s", certType)
 	}
 }
+
+// Ensures that required templates are present
+func ValidateTemplateRequirements() error {
+	if rootTemplate == "" {
+		return fmt.Errorf("root template is required but not found")
+	}
+	return nil
+}


Test patch:
diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go
index a0f587c..738dbf4 100644
--- a/pkg/certmaker/certmaker_test.go
+++ b/pkg/certmaker/certmaker_test.go
@@ -152,12 +152,11 @@ func TestCreateCertificates(t *testing.T) {
 		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, "", "", "",
+	err := CreateCertificates(KMSConfig{
+		Type:    "awskms",
+		KeyID:   "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)
@@ -177,12 +176,11 @@ func TestCreateCertificates(t *testing.T) {
 		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, "", "", "",
+	err = CreateCertificates(KMSConfig{
+		Type:    "awskms",
+		KeyID:   "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)
@@ -199,79 +197,45 @@ func TestCreateCertificates(t *testing.T) {
 		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, "", "", "",
+	err = CreateCertificates(KMSConfig{
+		Type:    "awskms",
+		KeyID:   "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)
 
+	leafKey, 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,
+	mockLeafSigner := &mockSignerVerifier{
+		key: leafKey,
 	}
 
 	InitKMS = func(_ context.Context, config KMSConfig) (signature.SignerVerifier, error) {
-		if config.RootKeyID == "intermediate-key" {
+		switch config.KeyID {
+		case "intermediate-key":
 			return mockIntermediateSigner, nil
+		case "leaf-key":
+			return mockLeafSigner, nil
+		default:
+			return mockSigner, 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,
+	err = CreateCertificates(KMSConfig{
+		Type:    "awskms",
+		KeyID:   "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, "leaf-key",
 		87600*time.Hour, 43800*time.Hour, 8760*time.Hour)
 
 	require.NoError(t, err)
@@ -293,36 +257,32 @@ func TestValidateKMSConfig(t *testing.T) {
 		{
 			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"},
+				Type:    "awskms",
+				KeyID:   "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"},
+				Type:    "gcpkms",
+				KeyID:   "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: "valid Azure config",
 			config: KMSConfig{
-				Type:      "azurekms",
-				RootKeyID: "azurekms:name=key1;vault=vault1",
-				LeafKeyID: "azurekms:name=key2;vault=vault1",
-				Options:   map[string]string{"azure-tenant-id": "tenant-id"},
+				Type:    "azurekms",
+				KeyID:   "azurekms:name=key1;vault=vault1",
+				Options: map[string]string{"azure-tenant-id": "tenant-id"},
 			},
 		},
 		{
 			name: "valid HashiVault config",
 			config: KMSConfig{
-				Type:      "hashivault",
-				RootKeyID: "transit/keys/root-key",
-				LeafKeyID: "transit/keys/leaf-key",
+				Type:  "hashivault",
+				KeyID: "transit/keys/root-key",
 				Options: map[string]string{
 					"vault-token":   "token",
 					"vault-address": "http://localhost:8200",
@@ -332,27 +292,24 @@ func TestValidateKMSConfig(t *testing.T) {
 		{
 			name: "missing AWS region",
 			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",
+				Type:  "awskms",
+				KeyID: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012",
 			},
 			wantError: "aws-region is required for AWS KMS",
 		},
 		{
 			name: "missing Azure tenant ID",
 			config: KMSConfig{
-				Type:      "azurekms",
-				RootKeyID: "azurekms:name=key1;vault=vault1",
-				LeafKeyID: "azurekms:name=key2;vault=vault1",
+				Type:  "azurekms",
+				KeyID: "azurekms:name=key1;vault=vault1",
 			},
 			wantError: "options map is required for Azure KMS",
 		},
 		{
 			name: "missing HashiVault token",
 			config: KMSConfig{
-				Type:      "hashivault",
-				RootKeyID: "transit/keys/root-key",
-				LeafKeyID: "transit/keys/leaf-key",
+				Type:  "hashivault",
+				KeyID: "transit/keys/root-key",
 				Options: map[string]string{
 					"vault-address": "http://localhost:8200",
 				},
@@ -362,9 +319,8 @@ func TestValidateKMSConfig(t *testing.T) {
 		{
 			name: "missing HashiVault address",
 			config: KMSConfig{
-				Type:      "hashivault",
-				RootKeyID: "transit/keys/root-key",
-				LeafKeyID: "transit/keys/leaf-key",
+				Type:  "hashivault",
+				KeyID: "transit/keys/root-key",
 				Options: map[string]string{
 					"vault-token": "token",
 				},
@@ -374,82 +330,66 @@ func TestValidateKMSConfig(t *testing.T) {
 		{
 			name: "unsupported KMS type",
 			config: KMSConfig{
-				Type:      "unsupported",
-				RootKeyID: "key1",
-				LeafKeyID: "key2",
+				Type:  "unsupported",
+				KeyID: "key1",
 			},
 			wantError: "unsupported KMS type: unsupported",
 		},
 		{
 			name: "invalid AWS key ID format",
 			config: KMSConfig{
-				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"},
+				Type:    "awskms",
+				KeyID:   "invalid-key-id",
+				Options: map[string]string{"aws-region": "us-west-2"},
 			},
-			wantError: "awskms RootKeyID must start with 'arn:aws:kms:' or 'alias/'",
+			wantError: "awskms KeyID 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"},
+				Type:    "awskms",
+				KeyID:   "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
+				Options: map[string]string{"aws-region": "us-west-2"},
 			},
 			wantError: "region in ARN (us-east-1) does not match configured region (us-west-2)",
 		},
 		{
 			name: "invalid GCP key ID format",
 			config: KMSConfig{
-				Type:      "gcpkms",
-				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"},
+				Type:    "gcpkms",
+				KeyID:   "invalid/key/path",
+				Options: map[string]string{"gcp-credentials-file": "/path/to/creds.json"},
 			},
-			wantError: "gcpkms RootKeyID must start with 'projects/'",
+			wantError: "gcpkms KeyID must start with 'projects/'",
 		},
 		{
 			name: "invalid Azure key ID format",
 			config: KMSConfig{
-				Type:      "azurekms",
-				RootKeyID: "invalid-key-id",
-				LeafKeyID: "azurekms:name=key2;vault=vault1",
-				Options:   map[string]string{"azure-tenant-id": "tenant-id"},
+				Type:    "azurekms",
+				KeyID:   "invalid-key-id",
+				Options: map[string]string{"azure-tenant-id": "tenant-id"},
 			},
-			wantError: "azurekms RootKeyID must start with 'azurekms:name='",
+			wantError: "azurekms KeyID must start with 'azurekms:name='",
 		},
 		{
 			name: "invalid HashiVault key ID format",
 			config: KMSConfig{
-				Type:      "hashivault",
-				RootKeyID: "invalid/path",
-				LeafKeyID: "transit/keys/leaf-key",
+				Type:  "hashivault",
+				KeyID: "invalid/path",
 				Options: map[string]string{
 					"vault-token":   "token",
 					"vault-address": "http://localhost:8200",
 				},
 			},
-			wantError: "hashivault RootKeyID must be in format: transit/keys/keyname",
-		},
-		{
-			name: "missing root key ID",
-			config: KMSConfig{
-				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",
+			wantError: "hashivault KeyID must be in format: transit/keys/keyname",
 		},
 		{
-			name: "missing leaf key ID",
+			name: "missing 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"},
+				Type:    "awskms",
+				Options: map[string]string{"aws-region": "us-west-2"},
 			},
-			wantError: "LeafKeyID must be specified",
+			wantError: "KeyID must be specified",
 		},
 	}