Skip to content

Commit 065f2c2

Browse files
authored
feat: adds cert-utility. (#1870)
* feat: adds cert templates. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * feat: splits/adds cert-utility to pgk/cmd and adds .DS_Store to .git_ignore. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * fix: validates code signing and includes leaf wording. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * feat: adds optional intermediate flag(s) and makes error/validation more consistent w/ tsa cert-utility. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * fix: changes cloudkms flag to gcpkms and makes azure/gcp flags more descriptive. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * fix: makes env vars for azure tenant-id and gcp credentials file more consistent w/ flags. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * fix: changes kms-region flag to aws-region and gcpkms-credentials-file flag to gcp-credentials-file. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * fix: improves kms key validation across providers. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * feat: adds sigstore/sigstore for kms and adds hashivault support. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * docs: adds readme for fulcio-certificate-maker. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * chore: adds fulcio-cert-maker to make file. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * refactor: adds bobcallaway's fb. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * refactor: for usage errors, show help / for operational errors show json error. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * chore: groups flags, adds validation for root-id, removes signer wrapper, and other PR fb. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * refactor: adds certLife to replace before/after timestamps. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * feat: adds templating, positional arg for common name and other improvements. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> * refactor: removes encoding/json and relies on x509util to validate templates. Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com> --------- Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com>
1 parent 3dd60f3 commit 065f2c2

16 files changed

+2934
-3
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ hack/tools/bin
3838

3939
# vscode
4040
.vscode/
41+
42+
# macOS
43+
.DS_Store
44+
fulcio-certificate-maker

.golangci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ linters-settings:
2828
excludes:
2929
- G115 # integer overflow conversion int -> uint32
3030
- G602 # slice index out of range
31-
output:
32-
uniq-by-line: false
3331
issues:
3432
exclude-rules:
3533
- path: _test\.go
@@ -47,6 +45,7 @@ issues:
4745
text: SA1019
4846
max-issues-per-linter: 0
4947
max-same-issues: 0
48+
uniq-by-line: false
5049
run:
5150
issues-exit-code: 1
5251
timeout: 10m

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
.PHONY: all test clean lint gosec
16+
.PHONY: all test clean lint gosec cert-maker
1717

1818
all: fulcio
1919
# Ensure Make is run with bash shell as some syntax below is bash-specific
@@ -77,12 +77,16 @@ gen: $(GENSRC)
7777
fulcio: $(SRCS) ## Build Fulcio for local tests
7878
go build -trimpath -ldflags "$(LDFLAGS)"
7979

80+
cert-maker: ## Build the Fulcio Certificate Maker tool
81+
go build -trimpath -ldflags "$(LDFLAGS)" -o fulcio-certificate-maker ./cmd/certificate_maker
82+
8083
test: ## Runs go test
8184
go test ./...
8285

8386
clean: ## Clean the workspace
8487
rm -rf dist
8588
rm -rf fulcio
89+
rm -rf fulcio-certificate-maker
8690

8791
clean-gen: clean
8892
rm -rf $(shell find pkg/generated -iname "*.go") *.swagger.json

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ mygUY7Ii2zbdCdliiow=
7575
-----END CERTIFICATE-----
7676
```
7777

78+
### Certificate Maker
79+
80+
The Fulcio's Certificate Maker is a tool for creating Fulcio compliant certificate chains. It supports:
81+
82+
* Two-level chains (root -> leaf)
83+
* Three-level chains (root -> intermediate -> leaf)
84+
* Multiple KMS providers (AWS, Google Cloud, Azure, HashiCorp Vault)
85+
86+
For detailed usage instructions and examples, see the [Certificate Maker documentation](docs/certificate-maker.md).
87+
7888
### Verifying releases
7989

8090
You can also verify signed releases (`fulcio-<os>.sig`) using the artifact signing key:
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Copyright 2024 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
// Package main implements a certificate creation utility for Fulcio.
17+
// It supports creating root and leaf certificates using (AWS, GCP, Azure).
18+
package main
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"os"
24+
"strings"
25+
"time"
26+
27+
"github.com/sigstore/fulcio/pkg/certmaker"
28+
"github.com/sigstore/fulcio/pkg/log"
29+
"github.com/spf13/cobra"
30+
"github.com/spf13/pflag"
31+
"github.com/spf13/viper"
32+
"go.uber.org/zap"
33+
)
34+
35+
// CLI flags and env vars for config.
36+
// Supports AWS KMS, Google Cloud KMS, and Azure Key Vault configurations.
37+
var (
38+
version string
39+
40+
rootCmd = &cobra.Command{
41+
Use: "fulcio-certificate-maker",
42+
Short: "Create certificate chains for Fulcio",
43+
Long: `A tool for creating root, intermediate, and leaf certificates for Fulcio with code signing capabilities`,
44+
Version: version,
45+
}
46+
47+
createCmd = &cobra.Command{
48+
Use: "create [common-name]",
49+
Short: "Create certificate chain",
50+
Long: `Create a certificate chain with the specified common name.
51+
The common name will be used as the Subject Common Name for the certificates.
52+
If no common name is provided, the values from the templates will be used.
53+
Example: fulcio-certificate-maker create "https://fulcio.example.com"`,
54+
Args: cobra.RangeArgs(0, 1),
55+
RunE: runCreate,
56+
}
57+
)
58+
59+
func mustBindPFlag(key string, flag *pflag.Flag) {
60+
if err := viper.BindPFlag(key, flag); err != nil {
61+
log.Logger.Fatal("failed to bind flag", zap.String("flag", key), zap.Error(err))
62+
}
63+
}
64+
65+
func mustBindEnv(key, envVar string) {
66+
if err := viper.BindEnv(key, envVar); err != nil {
67+
log.Logger.Fatal("failed to bind env var", zap.String("var", envVar), zap.Error(err))
68+
}
69+
}
70+
71+
func init() {
72+
log.ConfigureLogger("prod")
73+
74+
viper.AutomaticEnv()
75+
viper.SetEnvPrefix("")
76+
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
77+
78+
mustBindEnv("kms-type", "KMS_TYPE")
79+
mustBindEnv("aws-region", "AWS_REGION")
80+
mustBindEnv("azure-tenant-id", "AZURE_TENANT_ID")
81+
mustBindEnv("gcp-credentials-file", "GCP_CREDENTIALS_FILE")
82+
mustBindEnv("vault-token", "VAULT_TOKEN")
83+
mustBindEnv("vault-address", "VAULT_ADDR")
84+
mustBindEnv("root-key-id", "KMS_ROOT_KEY_ID")
85+
mustBindEnv("intermediate-key-id", "KMS_INTERMEDIATE_KEY_ID")
86+
mustBindEnv("leaf-key-id", "KMS_LEAF_KEY_ID")
87+
88+
rootCmd.AddCommand(createCmd)
89+
90+
// KMS provider flags
91+
createCmd.Flags().String("kms-type", "", "KMS provider type")
92+
createCmd.Flags().String("aws-region", "", "AWS KMS region")
93+
createCmd.Flags().String("azure-tenant-id", "", "Azure KMS tenant ID")
94+
createCmd.Flags().String("gcp-credentials-file", "", "Path to credentials file for GCP KMS")
95+
createCmd.Flags().String("vault-token", "", "HashiVault token")
96+
createCmd.Flags().String("vault-address", "", "HashiVault server address")
97+
98+
// Root certificate flags
99+
createCmd.Flags().String("root-key-id", "", "KMS key identifier for root certificate")
100+
createCmd.Flags().String("root-template", "", "Path to root certificate template (optional)")
101+
createCmd.Flags().String("root-cert", "root.pem", "Output path for root certificate")
102+
103+
// Intermediate certificate flags
104+
createCmd.Flags().String("intermediate-key-id", "", "KMS key identifier for intermediate certificate")
105+
createCmd.Flags().String("intermediate-template", "", "Path to intermediate certificate template (optional)")
106+
createCmd.Flags().String("intermediate-cert", "intermediate.pem", "Output path for intermediate certificate")
107+
108+
// Leaf certificate flags
109+
createCmd.Flags().String("leaf-key-id", "", "KMS key identifier for leaf certificate")
110+
createCmd.Flags().String("leaf-template", "", "Path to leaf certificate template (optional)")
111+
createCmd.Flags().String("leaf-cert", "leaf.pem", "Output path for leaf certificate")
112+
113+
// Lifetime flags
114+
createCmd.Flags().Duration("root-lifetime", 87600*time.Hour, "Root certificate lifetime")
115+
createCmd.Flags().Duration("intermediate-lifetime", 43800*time.Hour, "Intermediate certificate lifetime")
116+
createCmd.Flags().Duration("leaf-lifetime", 8760*time.Hour, "Leaf certificate lifetime")
117+
118+
mustBindPFlag("kms-type", createCmd.Flags().Lookup("kms-type"))
119+
mustBindPFlag("aws-region", createCmd.Flags().Lookup("aws-region"))
120+
mustBindPFlag("azure-tenant-id", createCmd.Flags().Lookup("azure-tenant-id"))
121+
mustBindPFlag("gcp-credentials-file", createCmd.Flags().Lookup("gcp-credentials-file"))
122+
mustBindPFlag("vault-token", createCmd.Flags().Lookup("vault-token"))
123+
mustBindPFlag("vault-address", createCmd.Flags().Lookup("vault-address"))
124+
mustBindPFlag("root-key-id", createCmd.Flags().Lookup("root-key-id"))
125+
mustBindPFlag("root-template", createCmd.Flags().Lookup("root-template"))
126+
mustBindPFlag("root-cert", createCmd.Flags().Lookup("root-cert"))
127+
mustBindPFlag("intermediate-key-id", createCmd.Flags().Lookup("intermediate-key-id"))
128+
mustBindPFlag("intermediate-template", createCmd.Flags().Lookup("intermediate-template"))
129+
mustBindPFlag("intermediate-cert", createCmd.Flags().Lookup("intermediate-cert"))
130+
mustBindPFlag("leaf-key-id", createCmd.Flags().Lookup("leaf-key-id"))
131+
mustBindPFlag("leaf-template", createCmd.Flags().Lookup("leaf-template"))
132+
mustBindPFlag("leaf-cert", createCmd.Flags().Lookup("leaf-cert"))
133+
mustBindPFlag("root-lifetime", createCmd.Flags().Lookup("root-lifetime"))
134+
mustBindPFlag("intermediate-lifetime", createCmd.Flags().Lookup("intermediate-lifetime"))
135+
mustBindPFlag("leaf-lifetime", createCmd.Flags().Lookup("leaf-lifetime"))
136+
}
137+
138+
func runCreate(_ *cobra.Command, args []string) error {
139+
defer func() { rootCmd.SilenceUsage = true }()
140+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
141+
defer cancel()
142+
143+
// Get common name from args if provided, otherwise templates used
144+
var commonName string
145+
if len(args) > 0 {
146+
commonName = args[0]
147+
}
148+
149+
// Build KMS config from flags and environment
150+
config := certmaker.KMSConfig{
151+
CommonName: commonName,
152+
Type: viper.GetString("kms-type"),
153+
RootKeyID: viper.GetString("root-key-id"),
154+
IntermediateKeyID: viper.GetString("intermediate-key-id"),
155+
LeafKeyID: viper.GetString("leaf-key-id"),
156+
Options: make(map[string]string),
157+
}
158+
159+
// Handle KMS provider options
160+
switch config.Type {
161+
case "gcpkms":
162+
if gcpCredsFile := viper.GetString("gcp-credentials-file"); gcpCredsFile != "" {
163+
// Check if gcp creds exists
164+
if _, err := os.Stat(gcpCredsFile); err != nil {
165+
if os.IsNotExist(err) {
166+
return fmt.Errorf("failed to initialize KMS: credentials file not found: %s", gcpCredsFile)
167+
}
168+
return fmt.Errorf("failed to initialize KMS: error accessing credentials file: %w", err)
169+
}
170+
config.Options["gcp-credentials-file"] = gcpCredsFile
171+
}
172+
case "azurekms":
173+
if azureTenantID := viper.GetString("azure-tenant-id"); azureTenantID != "" {
174+
config.Options["azure-tenant-id"] = azureTenantID
175+
}
176+
case "awskms":
177+
if awsRegion := viper.GetString("aws-region"); awsRegion != "" {
178+
config.Options["aws-region"] = awsRegion
179+
}
180+
case "hashivault":
181+
if vaultToken := viper.GetString("vault-token"); vaultToken != "" {
182+
config.Options["vault-token"] = vaultToken
183+
}
184+
if vaultAddr := viper.GetString("vault-address"); vaultAddr != "" {
185+
config.Options["vault-address"] = vaultAddr
186+
}
187+
}
188+
189+
km, err := certmaker.InitKMS(ctx, config)
190+
if err != nil {
191+
return fmt.Errorf("failed to initialize KMS: %w", err)
192+
}
193+
194+
// Get template paths
195+
rootTemplate := viper.GetString("root-template")
196+
intermediateTemplate := viper.GetString("intermediate-template")
197+
leafTemplate := viper.GetString("leaf-template")
198+
199+
// Validate template paths if provided
200+
if rootTemplate != "" {
201+
if err := certmaker.ValidateTemplate(rootTemplate, nil, "root"); err != nil {
202+
return fmt.Errorf("root template error: %w", err)
203+
}
204+
}
205+
if intermediateTemplate != "" {
206+
if err := certmaker.ValidateTemplate(intermediateTemplate, nil, "intermediate"); err != nil {
207+
return fmt.Errorf("intermediate template error: %w", err)
208+
}
209+
}
210+
if leafTemplate != "" {
211+
if err := certmaker.ValidateTemplate(leafTemplate, nil, "leaf"); err != nil {
212+
return fmt.Errorf("leaf template error: %w", err)
213+
}
214+
}
215+
216+
return certmaker.CreateCertificates(km, config,
217+
rootTemplate,
218+
leafTemplate,
219+
viper.GetString("root-cert"),
220+
viper.GetString("leaf-cert"),
221+
viper.GetString("intermediate-key-id"),
222+
viper.GetString("intermediate-template"),
223+
viper.GetString("intermediate-cert"),
224+
viper.GetDuration("root-lifetime"),
225+
viper.GetDuration("intermediate-lifetime"),
226+
viper.GetDuration("leaf-lifetime"))
227+
}
228+
229+
func main() {
230+
rootCmd.SilenceErrors = true
231+
if err := rootCmd.Execute(); err != nil {
232+
if rootCmd.SilenceUsage {
233+
log.Logger.Fatal("Command failed", zap.Error(err))
234+
} else {
235+
os.Exit(1)
236+
}
237+
}
238+
}

0 commit comments

Comments
 (0)