From ca1ae31e8b543d7de86d88c19ab1355d291e1e55 Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Sat, 30 Nov 2024 12:31:49 -0500 Subject: [PATCH] feat: adds optional intermediate flag(s). Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> --- cmd/certificate_maker/certificate_maker.go | 48 +-- pkg/certmaker/certmaker.go | 205 +++++++---- pkg/certmaker/certmaker_test.go | 317 ++++++++++++++---- pkg/certmaker/template.go | 11 +- .../templates/intermediate-template.json | 27 ++ pkg/certmaker/templates/leaf-template.json | 4 +- 6 files changed, 449 insertions(+), 163 deletions(-) create mode 100644 pkg/certmaker/templates/intermediate-template.json diff --git a/cmd/certificate_maker/certificate_maker.go b/cmd/certificate_maker/certificate_maker.go index 85ee413ce..a27495259 100644 --- a/cmd/certificate_maker/certificate_maker.go +++ b/cmd/certificate_maker/certificate_maker.go @@ -38,7 +38,7 @@ var ( rootCmd = &cobra.Command{ Use: "tsa-certificate-maker", Short: "Create certificate chains for Timestamp Authority", - Long: `A tool for creating root and leaf certificates for Timestamp Authority with timestamping capabilities`, + Long: `A tool for creating root, intermediate, and leaf certificates for Timestamp Authority with timestamping capabilities`, Version: version, } @@ -48,18 +48,20 @@ var ( RunE: runCreate, } - kmsType string - kmsRegion string - kmsKeyID string - kmsVaultName string - kmsTenantID string - kmsCredsFile string - rootTemplatePath string - leafTemplatePath string - rootKeyID string - leafKeyID string - rootCertPath string - leafCertPath string + kmsType string + kmsRegion string + kmsKeyID string + kmsTenantID string + kmsCredsFile string + rootTemplatePath string + leafTemplatePath string + rootKeyID string + leafKeyID string + rootCertPath string + leafCertPath string + intermediateKeyID string + intermediateTemplate string + intermediateCert string rawJSON = []byte(`{ "level": "debug", @@ -85,7 +87,6 @@ func init() { createCmd.Flags().StringVar(&kmsType, "kms-type", "", "KMS provider type (awskms, cloudkms, azurekms)") createCmd.Flags().StringVar(&kmsRegion, "kms-region", "", "KMS region") createCmd.Flags().StringVar(&kmsKeyID, "kms-key-id", "", "KMS key identifier") - createCmd.Flags().StringVar(&kmsVaultName, "kms-vault-name", "", "Azure KMS vault name") createCmd.Flags().StringVar(&kmsTenantID, "kms-tenant-id", "", "Azure KMS tenant ID") createCmd.Flags().StringVar(&kmsCredsFile, "kms-credentials-file", "", "Path to credentials file (for Google Cloud KMS)") createCmd.Flags().StringVar(&rootTemplatePath, "root-template", @@ -96,6 +97,9 @@ func init() { createCmd.Flags().StringVar(&leafKeyID, "leaf-key-id", "", "KMS key identifier for leaf certificate") createCmd.Flags().StringVar(&rootCertPath, "root-cert", "root.pem", "Output path for root certificate") createCmd.Flags().StringVar(&leafCertPath, "leaf-cert", "leaf.pem", "Output path for leaf certificate") + createCmd.Flags().StringVar(&intermediateKeyID, "intermediate-key-id", "", "KMS key identifier for intermediate certificate") + createCmd.Flags().StringVar(&intermediateTemplate, "intermediate-template", "pkg/certmaker/templates/intermediate-template.json", "Path to intermediate certificate template") + createCmd.Flags().StringVar(&intermediateCert, "intermediate-cert", "intermediate.pem", "Output path for intermediate certificate") } func runCreate(cmd *cobra.Command, args []string) error { @@ -104,11 +108,12 @@ func runCreate(cmd *cobra.Command, args []string) error { // Build KMS config from flags and environment config := certmaker.KMSConfig{ - Type: getConfigValue(kmsType, "KMS_TYPE"), - Region: getConfigValue(kmsRegion, "KMS_REGION"), - RootKeyID: getConfigValue(rootKeyID, "KMS_ROOT_KEY_ID"), - LeafKeyID: getConfigValue(leafKeyID, "KMS_LEAF_KEY_ID"), - Options: make(map[string]string), + Type: getConfigValue(kmsType, "KMS_TYPE"), + Region: getConfigValue(kmsRegion, "KMS_REGION"), + RootKeyID: getConfigValue(rootKeyID, "KMS_ROOT_KEY_ID"), + IntermediateKeyID: getConfigValue(intermediateKeyID, "KMS_INTERMEDIATE_KEY_ID"), + LeafKeyID: getConfigValue(leafKeyID, "KMS_LEAF_KEY_ID"), + Options: make(map[string]string), } // Handle KMS provider options @@ -118,9 +123,6 @@ func runCreate(cmd *cobra.Command, args []string) error { config.Options["credentials-file"] = credsFile } case "azurekms": - if vaultName := getConfigValue(kmsVaultName, "KMS_VAULT_NAME"); vaultName != "" { - config.Options["vault-name"] = vaultName - } if tenantID := getConfigValue(kmsTenantID, "KMS_TENANT_ID"); tenantID != "" { config.Options["tenant-id"] = tenantID } @@ -139,7 +141,7 @@ func runCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("leaf template error: %w", err) } - return certmaker.CreateCertificates(km, config, rootTemplatePath, leafTemplatePath, rootCertPath, leafCertPath) + return certmaker.CreateCertificates(km, config, rootTemplatePath, leafTemplatePath, rootCertPath, leafCertPath, intermediateKeyID, intermediateTemplate, intermediateCert) } func main() { diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go index a2c5c5de9..0f0c811cd 100644 --- a/pkg/certmaker/certmaker.go +++ b/pkg/certmaker/certmaker.go @@ -19,6 +19,7 @@ package certmaker import ( "context" + "crypto" "crypto/x509" "encoding/pem" "fmt" @@ -34,11 +35,12 @@ import ( // KMSConfig holds config for KMS providers. type KMSConfig struct { - Type string // KMS provider type: "awskms", "cloudkms", "azurekms" - Region string // AWS region or Cloud location - RootKeyID string // Root CA key identifier - LeafKeyID string // Leaf CA key identifier - Options map[string]string // Provider-specific options + Type string + Region string + RootKeyID string + IntermediateKeyID string + LeafKeyID string + Options map[string]string } // InitKMS initializes KMS provider based on the given config, KMSConfig. @@ -52,7 +54,7 @@ func InitKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { URI: "", } - // Falls back to IntermediateKeyID if root is not set + // Falls back to LeafKeyID if root is not set keyID := config.RootKeyID if keyID == "" { keyID = config.LeafKeyID @@ -65,12 +67,21 @@ func InitKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { case "cloudkms": opts.URI = fmt.Sprintf("cloudkms:%s", keyID) if credFile, ok := config.Options["credentials-file"]; ok { + if _, err := os.Stat(credFile); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("credentials file not found: %s", credFile) + } + return nil, fmt.Errorf("error accessing credentials file: %w", err) + } opts.URI += fmt.Sprintf("?credentials-file=%s", credFile) } - return cloudkms.New(ctx, opts) + km, err := cloudkms.New(ctx, opts) + if err != nil { + return nil, fmt.Errorf("failed to initialize Cloud KMS: %w", err) + } + return km, nil case "azurekms": - opts.URI = fmt.Sprintf("azurekms://%s.vault.azure.net/keys/%s", - config.Options["vault-name"], keyID) + opts.URI = keyID if config.Options["tenant-id"] != "" { opts.URI += fmt.Sprintf("?tenant-id=%s", config.Options["tenant-id"]) } @@ -80,72 +91,89 @@ func InitKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { } } -// CreateCertificates generates a certificate chain using the configured KMS provider. -// It creates both root and intermediate certificates using the provided templates -// and KMS signing keys. -func CreateCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, intermediateTemplatePath, rootCertPath, intermCertPath string) error { - // Parse root template +// 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(km apiv1.KeyManager, config KMSConfig, + rootTemplatePath, leafTemplatePath string, + rootCertPath, leafCertPath string, + intermediateKeyID, intermediateTemplatePath, intermediateCertPath string) error { + + // Create root cert rootTmpl, err := ParseTemplate(rootTemplatePath, nil) if err != nil { return fmt.Errorf("error parsing root template: %w", err) } - rootKeyName := config.RootKeyID - if config.Type == "azurekms" { - rootKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s", - config.Options["vault-name"], config.RootKeyID) - } rootSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ - SigningKey: rootKeyName, + SigningKey: config.RootKeyID, }) if err != nil { return fmt.Errorf("error creating root signer: %w", err) } - // Create root cert rootCert, err := x509util.CreateCertificate(rootTmpl, rootTmpl, rootSigner.Public(), rootSigner) if err != nil { return fmt.Errorf("error creating root certificate: %w", err) } + if err := WriteCertificateToFile(rootCert, rootCertPath); err != nil { return fmt.Errorf("error writing root certificate: %w", err) } - // Parse / sign intermediate template - intermediateTmpl, err := ParseTemplate(intermediateTemplatePath, rootCert) - if err != nil { - return fmt.Errorf("error parsing intermediate template: %w", err) + var signingCert *x509.Certificate + var signingKey crypto.Signer + + 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) + } + + intermediateSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ + SigningKey: intermediateKeyID, + }) + if err != nil { + return fmt.Errorf("error creating intermediate signer: %w", err) + } + + intermediateCert, err := x509util.CreateCertificate(intermediateTmpl, rootCert, intermediateSigner.Public(), rootSigner) + if err != nil { + return fmt.Errorf("error creating intermediate certificate: %w", err) + } + + if err := WriteCertificateToFile(intermediateCert, intermediateCertPath); err != nil { + return fmt.Errorf("error writing intermediate certificate: %w", err) + } + + signingCert = intermediateCert + signingKey = intermediateSigner + } else { + signingCert = rootCert + signingKey = rootSigner } - intermediateKeyName := config.LeafKeyID - if config.Type == "azurekms" { - intermediateKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s", - config.Options["vault-name"], config.LeafKeyID) + + // Create leaf cert + leafTmpl, err := ParseTemplate(leafTemplatePath, signingCert) + if err != nil { + return fmt.Errorf("error parsing leaf template: %w", err) } - intermediateSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ - SigningKey: intermediateKeyName, + + leafSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ + SigningKey: config.LeafKeyID, }) if err != nil { - return fmt.Errorf("error creating intermediate signer: %w", err) + return fmt.Errorf("error creating leaf signer: %w", err) } - // Create intermediate/leaf cert - intermediateCert, err := x509util.CreateCertificate(intermediateTmpl, rootCert, intermediateSigner.Public(), rootSigner) + leafCert, err := x509util.CreateCertificate(leafTmpl, signingCert, leafSigner.Public(), signingKey) if err != nil { - return fmt.Errorf("error creating intermediate certificate: %w", err) - } - if err := WriteCertificateToFile(intermediateCert, intermCertPath); err != nil { - return fmt.Errorf("error writing intermediate certificate: %w", err) + return fmt.Errorf("error creating leaf certificate: %w", err) } - // Verify certificate chain - pool := x509.NewCertPool() - pool.AddCert(rootCert) - opts := x509.VerifyOptions{ - Roots: pool, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, - } - if _, err := intermediateCert.Verify(opts); err != nil { - return fmt.Errorf("certificate chain verification failed: %w", err) + if err := WriteCertificateToFile(leafCert, leafCertPath); err != nil { + return fmt.Errorf("error writing leaf certificate: %w", err) } return nil @@ -163,14 +191,19 @@ func WriteCertificateToFile(cert *x509.Certificate, filename string) error { 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) } + // Determine cert type certType := "root" - if cert.Subject.OrganizationalUnit != nil && cert.Subject.OrganizationalUnit[0] == "TSA Intermediate CA" { + if !cert.IsCA { + certType = "leaf" + } else if cert.MaxPathLen == 0 { certType = "intermediate" } + fmt.Printf("Your %s certificate has been saved in %s.\n", certType, filename) return nil } @@ -186,29 +219,89 @@ func ValidateKMSConfig(config KMSConfig) error { switch config.Type { case "awskms": + // AWS KMS validation if config.Region == "" { return fmt.Errorf("region is required for AWS KMS") } + validateAWSKeyID := func(keyID, keyType string) error { + if keyID == "" { + return nil + } + if !strings.HasPrefix(keyID, "arn:aws:kms:") && !strings.HasPrefix(keyID, "alias/") { + return fmt.Errorf("awskms %s must start with 'arn:aws:kms:' or 'alias/'", keyType) + } + 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 { + return err + } + case "cloudkms": - if config.RootKeyID != "" && !strings.HasPrefix(config.RootKeyID, "projects/") { - return fmt.Errorf("cloudkms RootKeyID must start with 'projects/'") + // GCP KMS validation + validateGCPKeyID := func(keyID, keyType string) error { + if keyID == "" { + return nil + } + if !strings.HasPrefix(keyID, "projects/") { + return fmt.Errorf("cloudkms %s must start with 'projects/'", keyType) + } + if !strings.Contains(keyID, "/locations/") || !strings.Contains(keyID, "/keyRings/") { + return fmt.Errorf("invalid cloudkms key format for %s: %s", keyType, keyID) + } + return nil } - if config.LeafKeyID != "" && !strings.HasPrefix(config.LeafKeyID, "projects/") { - return fmt.Errorf("cloudkms LeafKeyID must start with 'projects/'") + if err := validateGCPKeyID(config.RootKeyID, "RootKeyID"); err != nil { + return err } - case "azurekms": - if config.Options["vault-name"] == "" { - return fmt.Errorf("vault-name is required for Azure KMS") + if err := validateGCPKeyID(config.IntermediateKeyID, "IntermediateKeyID"); err != nil { + return err } + if err := validateGCPKeyID(config.LeafKeyID, "LeafKeyID"); err != nil { + return err + } + + case "azurekms": + // Azure KMS validation if config.Options["tenant-id"] == "" { return fmt.Errorf("tenant-id is required for Azure KMS") } + validateAzureKeyID := func(keyID, keyType string) error { + if keyID == "" { + return nil + } + // Validate format: azurekms:name=;vault= + if !strings.HasPrefix(keyID, "azurekms:name=") { + return fmt.Errorf("azurekms %s must start with 'azurekms:name='", keyType) + } + if !strings.Contains(keyID, ";vault=") { + return fmt.Errorf("azurekms %s must contain ';vault=' parameter", keyType) + } + 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 { + return err + } + + default: + return fmt.Errorf("unsupported KMS type: %s", config.Type) } return nil } -// ValidateTemplatePath validates that a template file exists and contains valid JSON +// ValidateTemplatePath checks if the template file exists and has a .json extension func ValidateTemplatePath(path string) error { if _, err := os.Stat(path); err != nil { return fmt.Errorf("template not found at %s: %w", path, err) diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index 9facd47ed..6dc1f9576 100644 --- a/pkg/certmaker/certmaker_test.go +++ b/pkg/certmaker/certmaker_test.go @@ -47,16 +47,12 @@ func newMockKMS() *mockKMS { } // Pre-create test keys - rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(fmt.Errorf("failed to generate root key: %v", err)) - } - leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(fmt.Errorf("failed to generate leaf key: %v", err)) - } + rootKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + intermediateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + leafKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) m.keys["root-key"] = rootKey + m.keys["intermediate-key"] = intermediateKey m.keys["leaf-key"] = leafKey return m @@ -124,17 +120,94 @@ func TestParseTemplate(t *testing.T) { // TestCreateCertificates tests certificate chain creation func TestCreateCertificates(t *testing.T) { - t.Run("TSA", func(t *testing.T) { + rootContent := `{ + "subject": { + "country": ["US"], + "organization": ["Sigstore"], + "organizationalUnit": ["Timestamp Authority Root CA"], + "commonName": "https://tsa.com" + }, + "issuer": { + "commonName": "https://tsa.com" + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2034-01-01T00:00:00Z", + "basicConstraints": { + "isCA": true, + "maxPathLen": 1 + }, + "keyUsage": [ + "certSign", + "crlSign" + ] + }` + + leafContent := `{ + "subject": { + "country": ["US"], + "organization": ["Sigstore"], + "organizationalUnit": ["Timestamp Authority"], + "commonName": "https://tsa.com" + }, + "issuer": { + "commonName": "https://tsa.com" + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2034-01-01T00:00:00Z", + "basicConstraints": { + "isCA": false + }, + "keyUsage": [ + "digitalSignature" + ], + "extKeyUsage": [ + "timeStamping" + ] + }` + + t.Run("TSA without intermediate", func(t *testing.T) { tmpDir, err := os.MkdirTemp("", "cert-test-tsa-*") require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(tmpDir) }) - // root template (same for both) - rootContent := `{ + km := newMockKMS() + config := KMSConfig{ + Type: "mockkms", + RootKeyID: "root-key", + LeafKeyID: "leaf-key", + Options: make(map[string]string), + } + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + rootCertPath := filepath.Join(tmpDir, "root.pem") + leafCertPath := filepath.Join(tmpDir, "leaf.pem") + + err = os.WriteFile(rootTmplPath, []byte(rootContent), 0600) + require.NoError(t, err) + + err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) + require.NoError(t, err) + + err = CreateCertificates(km, config, + rootTmplPath, leafTmplPath, + rootCertPath, leafCertPath, + "", "", "") + require.NoError(t, err) + + verifyDirectChain(t, rootCertPath, leafCertPath) + }) + + t.Run("TSA with intermediate", func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cert-test-tsa-*") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + + intermediateContent := `{ "subject": { "country": ["US"], "organization": ["Sigstore"], - "organizationalUnit": ["Timestamp Authority Root CA"], + "organizationalUnit": ["TSA Intermediate CA"], "commonName": "https://tsa.com" }, "issuer": { @@ -144,7 +217,7 @@ func TestCreateCertificates(t *testing.T) { "notAfter": "2034-01-01T00:00:00Z", "basicConstraints": { "isCA": true, - "maxPathLen": 1 + "maxPathLen": 0 }, "keyUsage": [ "certSign", @@ -152,36 +225,36 @@ func TestCreateCertificates(t *testing.T) { ] }` - // leaf template - leafContent := `{ - "subject": { - "country": ["US"], - "organization": ["Sigstore"], - "organizationalUnit": ["Timestamp Authority"], - "commonName": "https://tsa.com" - }, - "issuer": { - "commonName": "https://tsa.com" - }, - "notBefore": "2024-01-01T00:00:00Z", - "notAfter": "2034-01-01T00:00:00Z", - "basicConstraints": { - "isCA": false, - "maxPathLen": 0 - }, - "keyUsage": [ - "digitalSignature" - ], - "extensions": [ - { - "id": "2.5.29.37", - "critical": true, - "value": {{ asn1Seq (asn1Enc "oid:1.3.6.1.5.5.7.3.8") | toJson }} - } - ] - }` + km := newMockKMS() + config := KMSConfig{ + Type: "mockkms", + RootKeyID: "root-key", + IntermediateKeyID: "intermediate-key", + LeafKeyID: "leaf-key", + Options: make(map[string]string), + } + + rootTmplPath := filepath.Join(tmpDir, "root-template.json") + leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") + intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + rootCertPath := filepath.Join(tmpDir, "root.pem") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.pem") + leafCertPath := filepath.Join(tmpDir, "leaf.pem") + + err = os.WriteFile(rootTmplPath, []byte(rootContent), 0600) + require.NoError(t, err) + err = os.WriteFile(intermediateTmplPath, []byte(intermediateContent), 0600) + require.NoError(t, err) + err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) + require.NoError(t, err) - testCertificateCreation(t, tmpDir, rootContent, leafContent) + err = CreateCertificates(km, config, + rootTmplPath, leafTmplPath, + rootCertPath, leafCertPath, + "intermediate-key", intermediateTmplPath, intermediateCertPath) + require.NoError(t, err) + + verifyIntermediateChain(t, rootCertPath, intermediateCertPath, leafCertPath) }) } @@ -222,59 +295,114 @@ func TestWriteCertificateToFile(t *testing.T) { assert.Equal(t, "Test Cert", parsedCert.Subject.CommonName) } -// testCertificateCreation creates and verifies certificate chains -func testCertificateCreation(t *testing.T, tmpDir, rootContent, leafContent string) { - rootTmplPath := filepath.Join(tmpDir, "root-template.json") - leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") - rootCertPath := filepath.Join(tmpDir, "root.pem") - leafCertPath := filepath.Join(tmpDir, "leaf.pem") - - err := os.WriteFile(rootTmplPath, []byte(rootContent), 0600) - require.NoError(t, err) - - err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) - require.NoError(t, err) - - km := newMockKMS() - config := KMSConfig{ - Type: "mockkms", - RootKeyID: "root-key", - LeafKeyID: "leaf-key", - Options: make(map[string]string), - } - - err = CreateCertificates(km, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath) - require.NoError(t, err) -} - func TestValidateKMSConfig(t *testing.T) { tests := []struct { name string config KMSConfig wantErr bool }{ + { + name: "valid aws config", + config: KMSConfig{ + Type: "awskms", + Region: "us-west-2", + RootKeyID: "arn:aws:kms:us-west-2:account-id:key/key-id", + LeafKeyID: "arn:aws:kms:us-west-2:account-id:key/leaf-key-id", + }, + wantErr: false, + }, + { + name: "valid gcp config", + config: KMSConfig{ + Type: "cloudkms", + RootKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/key-id", + LeafKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/leaf-key-id", + Options: map[string]string{ + "credentials-file": "/path/to/credentials.json", + }, + }, + wantErr: false, + }, { name: "valid azure config", config: KMSConfig{ Type: "azurekms", - RootKeyID: "root-key", - LeafKeyID: "leaf-key", + RootKeyID: "azurekms:name=root-key;vault=test-vault", + LeafKeyID: "azurekms:name=leaf-key;vault=test-vault", Options: map[string]string{ - "vault-name": "test-vault", - "tenant-id": "test-tenant", + "tenant-id": "test-tenant", }, }, wantErr: false, }, + { + name: "missing aws region", + config: KMSConfig{ + Type: "awskms", + RootKeyID: "arn:aws:kms:us-west-2:account-id:key/key-id", + }, + wantErr: true, + }, + { + name: "invalid gcp key format", + config: KMSConfig{ + Type: "cloudkms", + RootKeyID: "invalid-format", + }, + wantErr: true, + }, { name: "missing key IDs", config: KMSConfig{ Type: "azurekms", Options: map[string]string{ - "vault-name": "test-vault", - "tenant-id": "test-tenant", + "tenant-id": "test-tenant", + }, + }, + wantErr: true, + }, + { + name: "valid aws config with intermediate", + config: KMSConfig{ + Type: "awskms", + Region: "us-west-2", + RootKeyID: "arn:aws:kms:us-west-2:account-id:key/key-id", + IntermediateKeyID: "arn:aws:kms:us-west-2:account-id:key/intermediate-key-id", + LeafKeyID: "arn:aws:kms:us-west-2:account-id:key/leaf-key-id", + }, + wantErr: false, + }, + { + name: "valid gcp config with intermediate", + config: KMSConfig{ + Type: "cloudkms", + RootKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/key-id", + IntermediateKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/intermediate-key-id", + LeafKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/leaf-key-id", + }, + wantErr: false, + }, + { + name: "valid azure config with intermediate", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "azurekms:name=root-key;vault=test-vault", + IntermediateKeyID: "azurekms:name=intermediate-key;vault=test-vault", + LeafKeyID: "azurekms:name=leaf-key;vault=test-vault", + Options: map[string]string{ + "tenant-id": "test-tenant", }, }, + wantErr: false, + }, + { + name: "invalid intermediate key format", + config: KMSConfig{ + Type: "cloudkms", + RootKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/key-id", + IntermediateKeyID: "invalid-format", + LeafKeyID: "projects/project-id/locations/global/keyRings/ring-id/cryptoKeys/leaf-key-id", + }, wantErr: true, }, } @@ -290,3 +418,46 @@ func TestValidateKMSConfig(t *testing.T) { }) } } + +func verifyDirectChain(t *testing.T, rootPath, leafPath string) { + root := loadCertificate(t, rootPath) + leaf := loadCertificate(t, leafPath) + + rootPool := x509.NewCertPool() + rootPool.AddCert(root) + + _, err := leaf.Verify(x509.VerifyOptions{ + Roots: rootPool, + }) + require.NoError(t, err) +} + +func verifyIntermediateChain(t *testing.T, rootPath, intermediatePath, leafPath string) { + root := loadCertificate(t, rootPath) + intermediate := loadCertificate(t, intermediatePath) + leaf := loadCertificate(t, leafPath) + + intermediatePool := x509.NewCertPool() + intermediatePool.AddCert(intermediate) + + rootPool := x509.NewCertPool() + rootPool.AddCert(root) + + _, err := leaf.Verify(x509.VerifyOptions{ + Roots: rootPool, + Intermediates: intermediatePool, + }) + require.NoError(t, err) +} + +func loadCertificate(t *testing.T, path string) *x509.Certificate { + data, err := os.ReadFile(path) + require.NoError(t, err) + + block, _ := pem.Decode(data) + require.NotNil(t, block) + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + return cert +} diff --git a/pkg/certmaker/template.go b/pkg/certmaker/template.go index 4fb58bfa1..0ff7d4884 100644 --- a/pkg/certmaker/template.go +++ b/pkg/certmaker/template.go @@ -21,8 +21,7 @@ import ( "go.step.sm/crypto/x509util" ) -// CertificateTemplate defines the JSON structure for X.509 certificate templates -// including subject, issuer, validity period, and certificate constraints. +// CertificateTemplate defines the structure for the JSON certificate templates type CertificateTemplate struct { Subject struct { Country []string `json:"country,omitempty"` @@ -88,8 +87,6 @@ func ParseTemplate(filename string, parent *x509.Certificate) (*x509.Certificate } // ValidateTemplate performs validation checks on the certificate template. -// CA certs: verifies proper key usage is set. -// non-CA certs: verifies digitalSignature usage is set. func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error { if tmpl.Subject.CommonName == "" { return fmt.Errorf("template subject.commonName cannot be empty") @@ -147,9 +144,7 @@ func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error return nil } -// CreateCertificateFromTemplate generates an x509.Certificate from the provided template -// applying all specified attributes including subject, issuer, validity period, -// constraints and extensions. +// CreateCertificateFromTemplate creates an x509.Certificate from the provided template func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) (*x509.Certificate, error) { notBefore, err := time.Parse(time.RFC3339, tmpl.NotBefore) if err != nil { @@ -189,7 +184,7 @@ func CreateCertificateFromTemplate(tmpl *CertificateTemplate, parent *x509.Certi SetKeyUsages(cert, tmpl.KeyUsage) - // Sets extensions + // Sets extensions (e.g. Timestamping) for _, ext := range tmpl.Extensions { var oid []int for _, n := range strings.Split(ext.ID, ".") { diff --git a/pkg/certmaker/templates/intermediate-template.json b/pkg/certmaker/templates/intermediate-template.json new file mode 100644 index 000000000..e9d9650dc --- /dev/null +++ b/pkg/certmaker/templates/intermediate-template.json @@ -0,0 +1,27 @@ +{ + "subject": { + "country": [ + "US" + ], + "organization": [ + "Sigstore" + ], + "organizationalUnit": [ + "Timestamp Authority Intermediate CA" + ], + "commonName": "https://tsa.com" + }, + "issuer": { + "commonName": "https://tsa.com" + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2034-01-01T00:00:00Z", + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + }, + "keyUsage": [ + "certSign", + "crlSign" + ] +} \ No newline at end of file diff --git a/pkg/certmaker/templates/leaf-template.json b/pkg/certmaker/templates/leaf-template.json index 79f8c3e9d..a5ab9c730 100644 --- a/pkg/certmaker/templates/leaf-template.json +++ b/pkg/certmaker/templates/leaf-template.json @@ -16,10 +16,8 @@ }, "notBefore": "2024-01-01T00:00:00Z", "notAfter": "2034-01-01T00:00:00Z", - "serialNumber": 2, "basicConstraints": { - "isCA": false, - "maxPathLen": 0 + "isCA": false }, "keyUsage": [ "digitalSignature"