From e34b55d7e3754b390f654ed62b8caeaa8cc974e7 Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:25:44 -0500 Subject: [PATCH 1/8] fix: fixes build.yml. --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51929b5..c0044a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,7 @@ jobs: with: version: v1.62.0 args: --timeout=5m - skip-pkg-cache: true - skip-build-cache: true + only-new-issues: true - name: Run linter run: golangci-lint run test: From 7df8a1bb32825dc9fd364b5a20c2d0565d0119e7 Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:34:12 -0500 Subject: [PATCH 2/8] feat: splits root/intermediate kms keys from one another, adds output option for certs, updates docs/tests, and other improvements. --- README.md | 264 +++++++++++++++++++++++++++++---------------------- main.go | 89 ++++++++--------- main_test.go | 121 ++++++++++++----------- 3 files changed, 260 insertions(+), 214 deletions(-) diff --git a/README.md b/README.md index 12d5409..717ba81 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,9 @@ This tool creates root and intermediate certificates for: ## Requirements - Access to one of the supported KMS providers (AWS, Google Cloud, Azure) +- Pre-existing KMS keys (the tool uses existing keys and does not create new ones) - Go 1.21 or higher -## Installation - -```bash -go install github.com/liatrio/sigstore-certificate-maker@latest -``` - ## Local Development Clone and build the project locally: @@ -33,19 +28,6 @@ cd sigstore-certificate-maker # Build the binary go build -o sigstore-certificate-maker - -# Run locally -./sigstore-certificate-maker create -``` - -For development, you can also use: - -```bash -# Run directly with Go -go run main.go create - -# Run tests -go test ./... ``` ## Usage @@ -54,44 +36,29 @@ The tool can be configured using either command-line flags or environment variab ### Command-Line Interface -```shell -# Create certificates using default settings -sigstore-certificate-maker create - -# Specify KMS provider and settings -sigstore-certificate-maker create \ - --kms-type cloudkms \ - --kms-key-id projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key \ - --kms-credentials-file /path/to/credentials.json - -# Specify custom template paths -sigstore-certificate-maker create \ - --root-template path/to/root.json \ - --intermediate-template path/to/intermediate.json -``` - Available flags: - `--kms-type`: KMS provider type (awskms, cloudkms, azurekms) - `--kms-region`: KMS region (required for AWS KMS) -- `--kms-key-id`: Key identifier +- `--root-key-id`: KMS key identifier for root certificate +- `--intermediate-key-id`: KMS key identifier for intermediate certificate - `--kms-vault-name`: Azure KMS vault name - `--kms-tenant-id`: Azure KMS tenant ID - `--kms-credentials-file`: Path to credentials file (for Google Cloud KMS) - `--root-template`: Path to root certificate template - `--intermediate-template`: Path to intermediate certificate template +- `--root-cert`: Output path for root certificate (default: root.pem) +- `--intermediate-cert`: Output path for intermediate certificate (default: intermediate.pem) ### Environment Variables - `KMS_TYPE`: KMS provider type ("awskms", "cloudkms", "azurekms") - `KMS_REGION`: Region (required for AWS KMS, defaults to us-east-1) -- `KMS_KEY_ID`: Key identifier - - AWS: Key alias (default: alias/fulcio-key) - - Google Cloud: Full resource name (projects/_/locations/_/keyRings/_/cryptoKeys/_) - - Azure: Key name -- `KMS_OPTIONS`: Provider-specific options - - Google Cloud: credentials-file - - Azure: vault-name, tenant-id +- `ROOT_KEY_ID`: Key identifier for root certificate +- `INTERMEDIATE_KEY_ID`: Key identifier for intermediate certificate +- `KMS_VAULT_NAME`: Azure Key Vault name +- `KMS_TENANT_ID`: Azure tenant ID +- `KMS_CREDENTIALS_FILE`: Path to credentials file (for Google Cloud KMS) ### Provider-Specific Configuration Examples @@ -100,64 +67,114 @@ Available flags: ```shell export KMS_TYPE=awskms export KMS_REGION=us-east-1 -export KMS_KEY_ID=alias/fulcio-key +export ROOT_KEY_ID=alias/fulcio-root +export INTERMEDIATE_KEY_ID=alias/fulcio-intermediate ``` #### Google Cloud KMS ```shell export KMS_TYPE=cloudkms -export KMS_KEY_ID=projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key -export KMS_OPTIONS_CREDENTIALS_FILE=/path/to/credentials.json +export ROOT_KEY_ID=projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/root-key +export INTERMEDIATE_KEY_ID=projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/intermediate-key +export KMS_CREDENTIALS_FILE=/path/to/credentials.json ``` #### Azure KMS ```shell export KMS_TYPE=azurekms -export KMS_KEY_ID=my-key -export KMS_OPTIONS_VAULT_NAME=my-vault -export KMS_OPTIONS_TENANT_ID=tenant-id +export ROOT_KEY_ID=root-key +export INTERMEDIATE_KEY_ID=intermediate-key +export KMS_VAULT_NAME=my-vault +export KMS_TENANT_ID=tenant-id ``` -### Templates +### Example Templates -The tool uses JSON templates to define certificate properties: +#### Fulcio Root Template + +```json +{ + "subject": { + "country": ["US"], + "organization": ["Sigstore"], + "organizationalUnit": ["Fulcio Root 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": 1 + }, + "keyUsage": [ + "certSign", + "crlSign" + ], + "extKeyUsage": [ + "CodeSigning" + ] +} +``` #### Fulcio Intermediate Template ```json { - "subject": { - "commonName": "fulcio.example.com" - }, - "issuer": { - "commonName": "fulcio.example.com" - }, - "keyUsage": ["certSign", "crlSign"], - "extKeyUsage": ["codeSign"], - "basicConstraints": { - "isCA": true, - "maxPathLen": 0 - } + "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", + "serialNumber": 2, + "basicConstraints": { + "isCA": true, + "maxPathLen": 0 + }, + "keyUsage": [ + "certSign", + "crlSign", + "digitalSignature" + ], + "extKeyUsage": [ + "CodeSigning" + ] } ``` -#### Root CA Template +#### TSA Root Template ```json { - "subject": { - "commonName": "https://blah.com" - }, - "issuer": { - "commonName": "https://blah.com" - }, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": { - "isCA": true, - "maxPathLen": 0 - } + "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" + ] } ``` @@ -165,23 +182,32 @@ The tool uses JSON templates to define certificate properties: ```json { - "subject": { - "commonName": "tsa.example.com" - }, - "issuer": { - "commonName": "tsa.example.com" - }, - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": { - "isCA": false - }, - "extensions": [ - { - "id": "2.5.29.37", - "critical": true, - "value": "asn1Seq (asn1Enc oid:1.3.6.1.5.5.7.3.8) | toJson" - } - ] + "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", + "serialNumber": 2, + "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" + } + ] } ``` @@ -195,13 +221,15 @@ Certificate: Version: 3 (0x2) Serial Number: 1 (0x1) Signature Algorithm: ecdsa-with-SHA256 - Issuer: CN=https://blah.com - Subject: CN=https://blah.com + Issuer: C=US, O=Sigstore, OU=Fulcio Root CA, CN=https://fulcio.com + Subject: C=US, O=Sigstore, OU=Fulcio Root CA, CN=https://fulcio.com X509v3 extensions: X509v3 Key Usage: critical Certificate Sign, CRL Sign X509v3 Basic Constraints: critical - CA:TRUE + CA:TRUE, pathlen:1 + X509v3 Extended Key Usage: + Code Signing ``` #### TSA Intermediate Certificate @@ -210,14 +238,17 @@ Certificate: Certificate: Data: Version: 3 (0x2) + Serial Number: 2 (0x2) Signature Algorithm: ecdsa-with-SHA256 - Issuer: CN=https://blah.com - Subject: O=Liatrio, CN=Intermediate CA + Issuer: C=US, O=Sigstore, OU=Timestamp Authority Root CA, CN=https://tsa.com + Subject: C=US, O=Sigstore, OU=Timestamp Authority Intermediate CA, CN=https://tsa.com X509v3 extensions: X509v3 Key Usage: critical - Certificate Sign, CRL Sign + Digital Signature X509v3 Basic Constraints: critical CA:FALSE + X509v3 Extended Key Usage: critical + Time Stamping ``` #### Fulcio Intermediate Certificate @@ -226,12 +257,13 @@ Certificate: Certificate: Data: Version: 3 (0x2) + Serial Number: 2 (0x2) Signature Algorithm: ecdsa-with-SHA256 - Issuer: CN=https://blah.com - Subject: CN=fulcio.example.com + Issuer: C=US, O=Sigstore, OU=Fulcio Root CA, CN=https://fulcio.com + Subject: C=US, O=Sigstore, OU=Fulcio Intermediate CA, CN=https://fulcio.com X509v3 extensions: X509v3 Key Usage: critical - Certificate Sign, CRL Sign + Certificate Sign, CRL Sign, Digital Signature X509v3 Basic Constraints: critical CA:TRUE, pathlen:0 X509v3 Extended Key Usage: @@ -240,23 +272,27 @@ Certificate: ## Running the Tool -```bash -# Basic usage with default settings -sigstore-certificate-maker create +Example for Fulcio with AWS KMS: -# Using AWS KMS with custom templates +```bash sigstore-certificate-maker create \ --kms-type awskms \ --kms-region us-east-1 \ - --kms-key-id alias/fulcio-key \ - --root-template path/to/root.json \ - --intermediate-template path/to/intermediate.json + --root-key-id alias/fulcio-root \ + --intermediate-key-id alias/fulcio-intermediate \ + --root-template fulcio-root-template.json \ + --intermediate-template fulcio-intermediate-template.json ``` -### Configuration Precedence +Example for TSA with Azure KMS: -The tool uses the following precedence order for configuration: - -1. Command-line flags (highest priority) -2. Environment variables -3. Default values (lowest priority) +```bash +sigstore-certificate-maker create \ + --kms-type azurekms \ + --kms-vault-name my-vault \ + --kms-tenant-id tenant-id \ + --root-key-id tsa-root \ + --intermediate-key-id tsa-intermediate \ + --root-template tsa-root-template.json \ + --intermediate-template tsa-intermediate-template.json +``` diff --git a/main.go b/main.go index f824fbd..0cb6f81 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,10 @@ var ( kmsCredsFile string rootTemplatePath string intermTemplatePath string + rootKeyID string + intermediateKeyID string + rootCertPath string + intermCertPath string rawJSON = []byte(`{ "level": "debug", @@ -78,15 +82,20 @@ func init() { createCmd.Flags().StringVar(&kmsCredsFile, "kms-credentials-file", "", "Path to credentials file (for Google Cloud KMS)") createCmd.Flags().StringVar(&rootTemplatePath, "root-template", "root-template.json", "Path to root certificate template") createCmd.Flags().StringVar(&intermTemplatePath, "intermediate-template", "intermediate-template.json", "Path to intermediate certificate template") + createCmd.Flags().StringVar(&rootKeyID, "root-key-id", "", "KMS key identifier for root certificate") + createCmd.Flags().StringVar(&intermediateKeyID, "intermediate-key-id", "", "KMS key identifier for intermediate certificate") + createCmd.Flags().StringVar(&rootCertPath, "root-cert", "root.pem", "Output path for root certificate") + createCmd.Flags().StringVar(&intermCertPath, "intermediate-cert", "intermediate.pem", "Output path for intermediate certificate") } func runCreate(cmd *cobra.Command, args []string) error { // Build KMS config from flags and environment kmsConfig := KMSConfig{ - Type: getConfigValue(kmsType, "KMS_TYPE"), - Region: getConfigValue(kmsRegion, "KMS_REGION"), - KeyID: getConfigValue(kmsKeyID, "KMS_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"), + Options: make(map[string]string), } // Handle provider-specific options @@ -118,7 +127,7 @@ func runCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("intermediate template error: %w", err) } - return createCertificates(km, rootTemplatePath, intermTemplatePath) + return createCertificates(km, kmsConfig, rootTemplatePath, intermTemplatePath, rootCertPath, intermCertPath) } func main() { @@ -137,19 +146,25 @@ func initKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { URI: "", } + // Use RootKeyID as the primary key ID, fall back to IntermediateKeyID if root is not set + keyID := config.RootKeyID + if keyID == "" { + keyID = config.IntermediateKeyID + } + switch config.Type { case "awskms": - opts.URI = fmt.Sprintf("awskms:///%s?region=%s", config.KeyID, config.Region) + opts.URI = fmt.Sprintf("awskms:///%s?region=%s", keyID, config.Region) return awskms.New(ctx, opts) case "cloudkms": - opts.URI = fmt.Sprintf("cloudkms:%s", config.KeyID) + opts.URI = fmt.Sprintf("cloudkms:%s", keyID) if credFile, ok := config.Options["credentials-file"]; ok { opts.URI += fmt.Sprintf("?credentials-file=%s", credFile) } return cloudkms.New(ctx, opts) case "azurekms": opts.URI = fmt.Sprintf("azurekms://%s.vault.azure.net/keys/%s", - config.Options["vault-name"], config.KeyID) + config.Options["vault-name"], keyID) if config.Options["tenant-id"] != "" { opts.URI += fmt.Sprintf("?tenant-id=%s", config.Options["tenant-id"]) } @@ -160,29 +175,21 @@ func initKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { } // createCertificates generates a certificate chain using the configured KMS provider -func createCertificates(km apiv1.KeyManager, rootTemplatePath, intermediateTemplatePath string) error { +func createCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, intermediateTemplatePath, rootCertPath, intermediateCertPath string) error { // Parse templates rootTmpl, err := ParseTemplate(rootTemplatePath, nil) if err != nil { return fmt.Errorf("error parsing root template: %w", err) } - rootKeyName := "sigstore-key" - if kmsType == "azurekms" { + rootKeyName := config.RootKeyID + if config.Type == "azurekms" { rootKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s", - kmsVaultName, rootKeyName) - } - - rootKey, err := km.CreateKey(&apiv1.CreateKeyRequest{ - Name: rootKeyName, - SignatureAlgorithm: apiv1.ECDSAWithSHA256, - }) - if err != nil { - return fmt.Errorf("error creating root key: %w", err) + config.Options["vault-name"], config.RootKeyID) } rootSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ - SigningKey: rootKey.Name, + SigningKey: rootKeyName, }) if err != nil { return fmt.Errorf("error creating root signer: %w", err) @@ -200,22 +207,14 @@ func createCertificates(km apiv1.KeyManager, rootTemplatePath, intermediateTempl return fmt.Errorf("error parsing intermediate template: %w", err) } - intermediateKeyName := "sigstore-key-intermediate" - if kmsType == "azurekms" { + intermediateKeyName := config.IntermediateKeyID + if config.Type == "azurekms" { intermediateKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s", - kmsVaultName, intermediateKeyName) - } - - intermediateKey, err := km.CreateKey(&apiv1.CreateKeyRequest{ - Name: intermediateKeyName, - SignatureAlgorithm: apiv1.ECDSAWithSHA256, - }) - if err != nil { - return fmt.Errorf("error creating intermediate key: %w", err) + config.Options["vault-name"], config.IntermediateKeyID) } intermediateSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{ - SigningKey: intermediateKey.Name, + SigningKey: intermediateKeyName, }) if err != nil { return fmt.Errorf("error creating intermediate signer: %w", err) @@ -227,11 +226,11 @@ func createCertificates(km apiv1.KeyManager, rootTemplatePath, intermediateTempl return fmt.Errorf("error creating intermediate certificate: %w", err) } - if err := writeCertificateToFile(rootCert, "root.pem"); err != nil { + if err := writeCertificateToFile(rootCert, rootCertPath); err != nil { return fmt.Errorf("error writing root certificate: %w", err) } - if err := writeCertificateToFile(intermediateCert, "intermediate.pem"); err != nil { + if err := writeCertificateToFile(intermediateCert, intermCertPath); err != nil { return fmt.Errorf("error writing intermediate certificate: %w", err) } @@ -276,18 +275,19 @@ func writeCertificateToFile(cert *x509.Certificate, filename string) error { } type KMSConfig struct { - Type string // "awskms", "cloudkms", "azurekms" - Region string // AWS region or Cloud location - KeyID string // Key identifier - Options map[string]string // Provider-specific options + Type string // "awskms", "cloudkms", "azurekms" + Region string // AWS region or Cloud location + RootKeyID string // Root CA key identifier + IntermediateKeyID string // Intermediate CA key identifier + Options map[string]string // Provider-specific options } func validateKMSConfig(config KMSConfig) error { if config.Type == "" { return fmt.Errorf("KMS type cannot be empty") } - if config.KeyID == "" { - return fmt.Errorf("KeyID cannot be empty") + if config.RootKeyID == "" && config.IntermediateKeyID == "" { + return fmt.Errorf("at least one of RootKeyID or IntermediateKeyID must be specified") } switch config.Type { @@ -296,8 +296,11 @@ func validateKMSConfig(config KMSConfig) error { return fmt.Errorf("region is required for AWS KMS") } case "cloudkms": - if !strings.HasPrefix(config.KeyID, "projects/") { - return fmt.Errorf("cloudkms KeyID must start with 'projects/'") + if config.RootKeyID != "" && !strings.HasPrefix(config.RootKeyID, "projects/") { + return fmt.Errorf("cloudkms RootKeyID must start with 'projects/'") + } + if config.IntermediateKeyID != "" && !strings.HasPrefix(config.IntermediateKeyID, "projects/") { + return fmt.Errorf("cloudkms IntermediateKeyID must start with 'projects/'") } case "azurekms": if config.Options["vault-name"] == "" { diff --git a/main_test.go b/main_test.go index e17add7..00c4926 100644 --- a/main_test.go +++ b/main_test.go @@ -26,22 +26,19 @@ type mockKMS struct { } func newMockKMS() *mockKMS { - return &mockKMS{ + m := &mockKMS{ keys: make(map[string]*ecdsa.PrivateKey), signers: make(map[string]crypto.Signer), } -} -func (m *mockKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - m.keys[req.Name] = key - return &apiv1.CreateKeyResponse{ - Name: req.Name, - PublicKey: key.Public(), - }, nil + // Pre-create test keys + rootKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + intermediateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + m.keys["root-key"] = rootKey + m.keys["intermediate-key"] = intermediateKey + + return m } func (m *mockKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { @@ -65,6 +62,10 @@ func (m *mockKMS) Close() error { return nil } +func (m *mockKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + return nil, fmt.Errorf("CreateKey is not supported in mockKMS") +} + // TestParseTemplate tests JSON template parsing func TestParseTemplate(t *testing.T) { tmpFile, err := os.CreateTemp("", "cert-template-*.json") @@ -221,14 +222,8 @@ func TestWriteCertificateToFile(t *testing.T) { t.Cleanup(func() { os.RemoveAll(tmpDir) }) km := newMockKMS() - key, err := km.CreateKey(&apiv1.CreateKeyRequest{ - Name: "test-key", - SignatureAlgorithm: apiv1.ECDSAWithSHA256, - }) - require.NoError(t, err) - signer, err := km.CreateSigner(&apiv1.CreateSignerRequest{ - SigningKey: key.Name, + SigningKey: "root-key", }) require.NoError(t, err) @@ -261,6 +256,8 @@ func TestWriteCertificateToFile(t *testing.T) { func testCertificateCreation(t *testing.T, tmpDir, rootContent, intermediateContent string) { rootTmplPath := filepath.Join(tmpDir, "root-template.json") intermediateTmplPath := filepath.Join(tmpDir, "intermediate-template.json") + rootCertPath := filepath.Join(tmpDir, "root.pem") + intermediateCertPath := filepath.Join(tmpDir, "intermediate.pem") err := os.WriteFile(rootTmplPath, []byte(rootContent), 0600) require.NoError(t, err) @@ -269,47 +266,57 @@ func testCertificateCreation(t *testing.T, tmpDir, rootContent, intermediateCont require.NoError(t, err) km := newMockKMS() - err = createCertificates(km, rootTmplPath, intermediateTmplPath) - require.NoError(t, err) - - // Verify root certificate - rootPEM, err := os.ReadFile("root.pem") - require.NoError(t, err) - rootBlock, _ := pem.Decode(rootPEM) - require.NotNil(t, rootBlock) - rootCert, err := x509.ParseCertificate(rootBlock.Bytes) - require.NoError(t, err) - - // Root CA checks - assert.Equal(t, "https://blah.com", rootCert.Subject.CommonName) - assert.True(t, rootCert.IsCA) - assert.Equal(t, 0, rootCert.MaxPathLen) - assert.True(t, rootCert.KeyUsage&x509.KeyUsageCertSign != 0) - assert.True(t, rootCert.KeyUsage&x509.KeyUsageCRLSign != 0) + config := KMSConfig{ + Type: "mockkms", + RootKeyID: "root-key", + IntermediateKeyID: "intermediate-key", + Options: make(map[string]string), + } - // Verify intermediate certificate - intermediatePEM, err := os.ReadFile("intermediate.pem") - require.NoError(t, err) - intermediateBlock, _ := pem.Decode(intermediatePEM) - require.NotNil(t, intermediateBlock) - intermediateCert, err := x509.ParseCertificate(intermediateBlock.Bytes) + err = createCertificates(km, config, rootTmplPath, intermediateTmplPath, rootCertPath, intermediateCertPath) require.NoError(t, err) +} - // Intermediate checks (different for Fulcio and TSA) - assert.Equal(t, "https://blah.com", intermediateCert.Subject.CommonName) - if intermediateCert.IsCA { - // Fulcio case - assert.Equal(t, 0, intermediateCert.MaxPathLen) - } else { - // TSA case - assert.Equal(t, -1, intermediateCert.MaxPathLen) +func TestValidateKMSConfig(t *testing.T) { + tests := []struct { + name string + config KMSConfig + wantErr bool + }{ + { + name: "valid azure config", + config: KMSConfig{ + Type: "azurekms", + RootKeyID: "root-key", + IntermediateKeyID: "intermediate-key", + Options: map[string]string{ + "vault-name": "test-vault", + "tenant-id": "test-tenant", + }, + }, + wantErr: false, + }, + { + name: "missing key IDs", + config: KMSConfig{ + Type: "azurekms", + Options: map[string]string{ + "vault-name": "test-vault", + "tenant-id": "test-tenant", + }, + }, + wantErr: true, + }, } - // Verify chain - pool := x509.NewCertPool() - pool.AddCert(rootCert) - _, err = intermediateCert.Verify(x509.VerifyOptions{ - Roots: pool, - }) - assert.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateKMSConfig(tt.config) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } } From c20408729d4269deda4781c44d9f93f601ec422d Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:47:22 -0500 Subject: [PATCH 3/8] fix: adds error checking for both ecdsa.GenerateKey calls. --- main_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/main_test.go b/main_test.go index 00c4926..a218f47 100644 --- a/main_test.go +++ b/main_test.go @@ -32,8 +32,14 @@ func newMockKMS() *mockKMS { } // Pre-create test keys - rootKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - intermediateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(fmt.Errorf("failed to generate root key: %v", err)) + } + intermediateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(fmt.Errorf("failed to generate intermediate key: %v", err)) + } m.keys["root-key"] = rootKey m.keys["intermediate-key"] = intermediateKey From a6a8ba67882df9279e21862f2e3d8aa85ba7fbef Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:51:25 -0500 Subject: [PATCH 4/8] chore: adds copyright. --- .github/workflows/release.yml | 2 +- .golangci.yaml | 2 +- main.go | 15 +++++++++++++++ main_test.go | 15 +++++++++++++++ renovate.json | 2 +- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0115217..7cf28d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5 with: - go-version: '1.22' + go-version: '1.23' - name: Build and release run: | go build -o sigstore-certificate-maker diff --git a/.golangci.yaml b/.golangci.yaml index 473c846..1d43637 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,7 +1,7 @@ --- run: timeout: 5m - go: "1.22" + go: "1.23" linters-settings: errcheck: diff --git a/main.go b/main.go index 0cb6f81..32534d2 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,18 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + // Package main provides certificate creation utilities for Fulcio and Timestamp Authority package main diff --git a/main_test.go b/main_test.go index a218f47..86eca61 100644 --- a/main_test.go +++ b/main_test.go @@ -1,3 +1,18 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + package main import ( diff --git a/renovate.json b/renovate.json index a131b4a..7f06752 100644 --- a/renovate.json +++ b/renovate.json @@ -5,7 +5,7 @@ "config:recommended" ], "constraints": { - "go": "1.22" + "go": "1.23" }, "schedule": [ "before 5am every weekday" From 1885c6e5c8f7ed25b680c035a7b9633e1ea75eea Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:24:44 -0500 Subject: [PATCH 5/8] chore: improves release process. --- .github/workflows/build.yml | 31 ++++++++++++++++++++++++++++--- .github/workflows/release.yml | 24 ++++++++++++++++++++++-- README.md | 2 ++ main.go | 5 ++--- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0044a1..20342d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,10 +27,35 @@ jobs: test: name: test runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.21', '1.22', '1.23'] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: - go-version: '1.23' + go-version: ${{ matrix.go-version }} - name: Run tests run: make test + build: + name: build + runs-on: ubuntu-latest + needs: [test] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: '1.23' + cache: true + - name: Build + run: | + VERSION=${{ github.ref_name }} + if [[ "${{ github.ref_type }}" == "branch" ]]; then + VERSION="${VERSION}-${GITHUB_SHA::8}" + fi + go build -ldflags "-X main.version=${VERSION}" -o bin/sigstore-certificate-maker ./... + - name: Upload binary + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: sigstore-certificate-maker + path: bin/sigstore-certificate-maker diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cf28d2..f49fa81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,7 @@ on: push: tags: - v* + branches: main jobs: release: @@ -14,14 +15,33 @@ jobs: packages: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 0 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5 with: go-version: '1.23' - - name: Build and release + cache: true + + - name: Run go-semantic-release + uses: go-semantic-release/action@48d83acd958dae62e73701aad20a5b5844a3bf45 # v1.23.0 + id: semrel + with: + github-token: ${{ github.token }} + changelog-generator-opt: emojis=true + allow-initial-development-versions: true + + - name: Build and package + if: steps.semrel.outputs.version != '' run: | - go build -o sigstore-certificate-maker + echo "Creating release version v${steps.semrel.outputs.version}" + go test -v . + go build -ldflags "-X main.version=v${steps.semrel.outputs.version}" -o sigstore-certificate-maker tar czf sigstore-certificate-maker.tar.gz sigstore-certificate-maker + - name: Create Release + if: steps.semrel.outputs.version != '' uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2 with: files: sigstore-certificate-maker.tar.gz + tag_name: v${{ steps.semrel.outputs.version }} + generate_release_notes: true diff --git a/README.md b/README.md index 717ba81..1d16578 100644 --- a/README.md +++ b/README.md @@ -296,3 +296,5 @@ sigstore-certificate-maker create \ --root-template tsa-root-template.json \ --intermediate-template tsa-intermediate-template.json ``` + +[![Build and Test](https://github.com/{owner}/{repo}/actions/workflows/build.yml/badge.svg)](https://github.com/{owner}/{repo}/actions/workflows/build.yml) diff --git a/main.go b/main.go index 32534d2..566b8b8 100644 --- a/main.go +++ b/main.go @@ -35,9 +35,8 @@ import ( ) var ( - logger *zap.Logger - - version = "dev" + logger *zap.Logger + version string rootCmd = &cobra.Command{ Use: "sigstore-certificate-maker", From eee312e48c19d14b880a3e33512135388d243f6b Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:50:41 -0500 Subject: [PATCH 6/8] docs: adds go doc comments. --- main.go | 32 +++++++++++++++++++++----------- template.go | 4 +++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 566b8b8..c57a250 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,9 @@ // limitations under the License. // -// Package main provides certificate creation utilities for Fulcio and Timestamp Authority +// Package main implements a certificate creation utility for Sigstore services. +// It supports creating root and intermediate certificates for both Fulcio and +// Timestamp Authority using various KMS providers (AWS, GCP, Azure). package main import ( @@ -150,6 +152,7 @@ func main() { } } +// initKMS creates and configures a KeyManager based on the provided KMS configuration func initKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { if err := validateKMSConfig(config); err != nil { return nil, fmt.Errorf("invalid KMS configuration: %w", err) @@ -188,8 +191,21 @@ func initKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) { } } -// createCertificates generates a certificate chain using the configured KMS provider -func createCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, intermediateTemplatePath, rootCertPath, intermediateCertPath string) error { +// KMSConfig holds the configuration for a Key Management Service provider. +// It supports AWS KMS, Google Cloud KMS, and Azure Key Vault. +type KMSConfig struct { + Type string // KMS provider type: "awskms", "cloudkms", "azurekms" + Region string // AWS region or Cloud location + RootKeyID string // Root CA key identifier + IntermediateKeyID string // Intermediate CA key identifier + Options map[string]string // Provider-specific options +} + +// 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. The certificates are written to the specified output paths +// and the chain is verified before returning. +func createCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, intermediateTemplatePath, rootCertPath, intermCertPath string) error { // Parse templates rootTmpl, err := ParseTemplate(rootTemplatePath, nil) if err != nil { @@ -269,6 +285,7 @@ func createCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, return nil } +// writeCertificateToFile writes an X.509 certificate to a PEM-encoded file func writeCertificateToFile(cert *x509.Certificate, filename string) error { certPEM := &pem.Block{ Type: "CERTIFICATE", @@ -288,14 +305,7 @@ func writeCertificateToFile(cert *x509.Certificate, filename string) error { return nil } -type KMSConfig struct { - Type string // "awskms", "cloudkms", "azurekms" - Region string // AWS region or Cloud location - RootKeyID string // Root CA key identifier - IntermediateKeyID string // Intermediate CA key identifier - Options map[string]string // Provider-specific options -} - +// validateKMSConfig ensures all required KMS configuration parameters are present func validateKMSConfig(config KMSConfig) error { if config.Type == "" { return fmt.Errorf("KMS type cannot be empty") diff --git a/template.go b/template.go index b34eecc..443c0ff 100644 --- a/template.go +++ b/template.go @@ -1,5 +1,7 @@ // Package main provides template parsing and certificate generation functionality -// for creating X.509 certificates from JSON templates +// for creating X.509 certificates from JSON templates. It supports both root and +// intermediate certificate creation with configurable properties including key usage, +// extended key usage, and basic constraints. package main import ( From 3e7f27bb32c778bbece953253675605a6dde4534 Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:52:21 -0500 Subject: [PATCH 7/8] chore: updates wf to ignore files. --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20342d3..c9dc84f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,10 @@ on: branches: [main] push: branches: [main] + paths-ignore: + - README.md + - catalog-info.yaml + - renovate.json jobs: lint: From 1676c8cc66c6e4f9ade8f75772f79812c92c1c43 Mon Sep 17 00:00:00 2001 From: ianhundere <138915+ianhundere@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:53:28 -0500 Subject: [PATCH 8/8] chore: adds .DS_Store to .git_ignore file. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 04cc5ed..069a49e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ build/ # Brew Brewfile.lock.json + +# macOS +.DS_Store