diff --git a/cmd/certificate_maker/certificate_maker.go b/cmd/certificate_maker/certificate_maker.go index 85ee413c..d645f124 100644 --- a/cmd/certificate_maker/certificate_maker.go +++ b/cmd/certificate_maker/certificate_maker.go @@ -48,18 +48,22 @@ 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", @@ -96,6 +100,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 { @@ -139,7 +147,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 a2c5c5de..bdcc2e73 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. @@ -83,69 +85,87 @@ 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 +func CreateCertificates(km apiv1.KeyManager, config KMSConfig, + rootTemplatePath, leafTemplatePath string, + rootCertPath, leafCertPath string, + withIntermediate bool, + 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 withIntermediate { + // Only create intermediate if flag is set + 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 +183,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 } diff --git a/pkg/certmaker/certmaker_test.go b/pkg/certmaker/certmaker_test.go index 9facd47e..90d97ec5 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, + false, "", "", "") + 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,37 @@ 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) + + err = CreateCertificates(km, config, + rootTmplPath, leafTmplPath, + rootCertPath, leafCertPath, + true, + "intermediate-key", intermediateTmplPath, intermediateCertPath) + require.NoError(t, err) - testCertificateCreation(t, tmpDir, rootContent, leafContent) + verifyIntermediateChain(t, rootCertPath, intermediateCertPath, leafCertPath) }) } @@ -222,31 +296,6 @@ 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 @@ -290,3 +339,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/templates/intermediate-template.json b/pkg/certmaker/templates/intermediate-template.json new file mode 100644 index 00000000..37378d91 --- /dev/null +++ b/pkg/certmaker/templates/intermediate-template.json @@ -0,0 +1,27 @@ +{ + "subject": { + "country": [ + "US" + ], + "organization": [ + "Sigstore" + ], + "organizationalUnit": [ + "TSA 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 79f8c3e9..a5ab9c73 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"