From 2a2709da0e29646f3b4249d5cf19ba058ab8da32 Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Sat, 30 Nov 2024 12:31:56 -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 | 36 +++-- pkg/certmaker/certmaker.go | 95 +++++++---- pkg/certmaker/certmaker_test.go | 149 +++++++++++++----- pkg/certmaker/template.go | 4 +- .../templates/intermediate-template.json | 30 ++++ pkg/certmaker/templates/leaf-template.json | 5 +- 6 files changed, 229 insertions(+), 90 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 726bf4858..d49d8beec 100644 --- a/cmd/certificate_maker/certificate_maker.go +++ b/cmd/certificate_maker/certificate_maker.go @@ -47,25 +47,29 @@ 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 + kmsVaultName string + kmsTenantID string + kmsCredsFile string + rootTemplatePath string + leafTemplatePath string + rootKeyID string + leafKeyID string + rootCertPath string + leafCertPath string + withIntermediate bool + intermediateKeyID string + intermediateTemplate string + intermediateCert string rawJSON = []byte(`{ "level": "debug", "encoding": "json", "outputPaths": ["stdout"], "errorOutputPaths": ["stderr"], - "initialFields": {"service": "sigstore-certificate-maker"}, + "initialFields": {"service": "fulcio-certificate-maker"}, "encoderConfig": { "messageKey": "message", "levelKey": "level", @@ -93,6 +97,10 @@ 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().BoolVar(&withIntermediate, "with-intermediate", false, "Create certificate chain with intermediate CA") + 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 { @@ -134,7 +142,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, withIntermediate, intermediateKeyID, intermediateTemplate, intermediateCert) } func main() { diff --git a/pkg/certmaker/certmaker.go b/pkg/certmaker/certmaker.go index a066e9ed5..40ffb26af 100644 --- a/pkg/certmaker/certmaker.go +++ b/pkg/certmaker/certmaker.go @@ -19,6 +19,7 @@ package certmaker import ( "context" + "crypto" "crypto/x509" "encoding/json" "encoding/pem" @@ -35,11 +36,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. @@ -84,51 +86,81 @@ 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 leaf certificates using the provided templates // and KMS signing keys. -func CreateCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, leafTemplatePath, rootCertPath, leafCertPath string) error { - // Parse root template +func CreateCertificates(km apiv1.KeyManager, config KMSConfig, + rootTemplatePath, leafTemplatePath string, + rootCertPath, leafCertPath string, + withIntermediate bool, + intermediateKeyID, intermediateTemplate, 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) } + var signingCert *x509.Certificate + var signingKey crypto.Signer + + if withIntermediate { + // Create intermediate cert + intermediateTmpl, err := ParseTemplate(intermediateTemplate, 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 + } + // Create leaf cert - leafTmpl, err := ParseTemplate(leafTemplatePath, rootCert) + leafTmpl, err := ParseTemplate(leafTemplatePath, signingCert) if err != nil { return fmt.Errorf("error parsing leaf template: %w", err) } - leafKeyName := config.LeafKeyID - if config.Type == "azurekms" { - leafKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s", - config.Options["vault-name"], config.LeafKeyID) - } + leafSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ - SigningKey: leafKeyName, + SigningKey: config.LeafKeyID, }) if err != nil { return fmt.Errorf("error creating leaf signer: %w", err) } - leafCert, err := x509util.CreateCertificate(leafTmpl, rootCert, leafSigner.Public(), rootSigner) + leafCert, err := x509util.CreateCertificate(leafTmpl, signingCert, leafSigner.Public(), signingKey) if err != nil { return fmt.Errorf("error creating leaf certificate: %w", err) } @@ -137,17 +169,6 @@ func CreateCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, return fmt.Errorf("error writing leaf certificate: %w", err) } - // Verify cert chain - pool := x509.NewCertPool() - pool.AddCert(rootCert) - opts := x509.VerifyOptions{ - Roots: pool, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - } - if _, err := leafCert.Verify(opts); err != nil { - return fmt.Errorf("certificate chain verification failed: %w", err) - } - return nil } @@ -163,11 +184,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.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 } diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index 770855cab..1c79bf041 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 @@ -131,44 +127,31 @@ func TestCreateCertificates(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(tmpDir) }) - // root template (same for both) + // Create test templates rootContent := `{ "subject": { - "commonName": "https://blah.com" + "commonName": "Test Root CA" }, "issuer": { - "commonName": "https://blah.com" + "commonName": "Test Root CA" }, - "keyUsage": [ - "certSign", - "crlSign" - ], - "extKeyUsage": [ - "CodeSigning" - ], + "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, - "maxPathLen": 0 + "maxPathLen": 1 }, "notBefore": "2024-01-01T00:00:00Z", "notAfter": "2025-01-01T00:00:00Z" }` - // leaf template - leafContent := `{ + intermediateContent := `{ "subject": { - "commonName": "https://blah.com" + "commonName": "Test Intermediate CA" }, "issuer": { - "commonName": "https://blah.com" + "commonName": "Test Root CA" }, - "keyUsage": [ - "certSign", - "crlSign" - ], - "extKeyUsage": [ - "CodeSigning" - ], + "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, "maxPathLen": 0 @@ -177,7 +160,29 @@ func TestCreateCertificates(t *testing.T) { "notAfter": "2025-01-01T00:00:00Z" }` - testCertificateCreation(t, tmpDir, rootContent, leafContent) + leafContent := `{ + "subject": { + "commonName": "Test Leaf" + }, + "issuer": { + "commonName": "Test Root CA" + }, + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["CodeSigning"], + "basicConstraints": { + "isCA": false + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2025-01-01T00:00:00Z" + }` + + t.Run("without intermediate", func(t *testing.T) { + testCertificateCreation(t, tmpDir, rootContent, leafContent, false) + }) + + t.Run("with intermediate", func(t *testing.T) { + testCertificateCreation(t, tmpDir, rootContent, leafContent, true, intermediateContent) + }) } // TestWriteCertificateToFile tests PEM file writing @@ -218,7 +223,7 @@ func TestWriteCertificateToFile(t *testing.T) { } // testCertificateCreation creates and verifies certificate chains -func testCertificateCreation(t *testing.T, tmpDir, rootContent, leafContent string) { +func testCertificateCreation(t *testing.T, tmpDir, rootContent, leafContent string, withIntermediate bool, intermediateContent ...string) { rootTmplPath := filepath.Join(tmpDir, "root-template.json") leafTmplPath := filepath.Join(tmpDir, "leaf-template.json") rootCertPath := filepath.Join(tmpDir, "root.pem") @@ -230,16 +235,86 @@ func testCertificateCreation(t *testing.T, tmpDir, rootContent, leafContent stri err = os.WriteFile(leafTmplPath, []byte(leafContent), 0600) require.NoError(t, err) + intermediateTemplatePath := "" + intermediateKeyID := "" + intermediateCertPath := "" + + if withIntermediate { + require.NotEmpty(t, intermediateContent, "intermediate content required when withIntermediate is true") + intermediateTemplatePath = filepath.Join(tmpDir, "intermediate-template.json") + err = os.WriteFile(intermediateTemplatePath, []byte(intermediateContent[0]), 0600) + require.NoError(t, err) + intermediateKeyID = "intermediate-key" + intermediateCertPath = filepath.Join(tmpDir, "intermediate.pem") + } + km := newMockKMS() config := KMSConfig{ - Type: "mockkms", - RootKeyID: "root-key", - LeafKeyID: "leaf-key", - Options: make(map[string]string), + Type: "mockkms", + RootKeyID: "root-key", + IntermediateKeyID: intermediateKeyID, + LeafKeyID: "leaf-key", + Options: make(map[string]string), + } + + err = CreateCertificates(km, config, + rootTmplPath, leafTmplPath, + rootCertPath, leafCertPath, + withIntermediate, + intermediateKeyID, intermediateTemplatePath, intermediateCertPath) + require.NoError(t, err) + + // Verify certificate chain + if withIntermediate { + verifyIntermediateChain(t, rootCertPath, intermediateCertPath, leafCertPath) + } else { + verifyDirectChain(t, rootCertPath, leafCertPath) } +} + +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, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }) + require.NoError(t, err) +} + +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, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }) + 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) - err = CreateCertificates(km, config, rootTmplPath, leafTmplPath, rootCertPath, leafCertPath) + cert, err := x509.ParseCertificate(block.Bytes) require.NoError(t, err) + return cert } func TestValidateKMSConfig(t *testing.T) { diff --git a/pkg/certmaker/template.go b/pkg/certmaker/template.go index 2e7c4d900..537a47e26 100644 --- a/pkg/certmaker/template.go +++ b/pkg/certmaker/template.go @@ -53,14 +53,14 @@ func ParseTemplate(filename string, parent *x509.Certificate) (*x509.Certificate return nil, fmt.Errorf("error parsing template JSON: %w", err) } - if err := ValidateTemplate(&tmpl, parent); err != nil { + if err := ValidateTemplate(&tmpl, parent, "root"); err != nil { return nil, err } return CreateCertificateFromTemplate(&tmpl, parent) } -func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate) error { +func ValidateTemplate(tmpl *CertificateTemplate, parent *x509.Certificate, certType string) error { if tmpl.Subject.CommonName == "" { return fmt.Errorf("template subject.commonName cannot be empty") } diff --git a/pkg/certmaker/templates/intermediate-template.json b/pkg/certmaker/templates/intermediate-template.json new file mode 100644 index 000000000..4dfd95ffb --- /dev/null +++ b/pkg/certmaker/templates/intermediate-template.json @@ -0,0 +1,30 @@ +{ + "subject": { + "country": [ + "US" + ], + "organization": [ + "Sigstore" + ], + "organizationalUnit": [ + "Fulcio Intermediate CA" + ], + "commonName": "https://fulcio.com" + }, + "issuer": { + "commonName": "https://fulcio.com" + }, + "notBefore": "2024-01-01T00:00:00Z", + "notAfter": "2034-01-01T00:00:00Z", + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + }, + "keyUsage": [ + "certSign", + "crlSign" + ], + "extKeyUsage": [ + "CodeSigning" + ] +} \ No newline at end of file diff --git a/pkg/certmaker/templates/leaf-template.json b/pkg/certmaker/templates/leaf-template.json index 005276551..91fe972c5 100644 --- a/pkg/certmaker/templates/leaf-template.json +++ b/pkg/certmaker/templates/leaf-template.json @@ -17,12 +17,9 @@ "notBefore": "2024-01-01T00:00:00Z", "notAfter": "2034-01-01T00:00:00Z", "basicConstraints": { - "isCA": true, - "maxPathLen": 0 + "isCA": false }, "keyUsage": [ - "certSign", - "crlSign", "digitalSignature" ], "extKeyUsage": [