This repository has been archived by the owner on Jan 18, 2025. It is now read-only.
generated from liatrio/go-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
376 lines (327 loc) · 12.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
// 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 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 (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/awskms"
"go.step.sm/crypto/kms/azurekms"
"go.step.sm/crypto/kms/cloudkms"
"go.step.sm/crypto/x509util"
"go.uber.org/zap"
)
var (
logger *zap.Logger
version string
rootCmd = &cobra.Command{
Use: "sigstore-certificate-maker",
Short: "Create certificate chains for Sigstore services",
Long: `A tool for creating root and intermediate certificates for Fulcio and Timestamp Authority`,
Version: version,
}
createCmd = &cobra.Command{
Use: "create",
Short: "Create certificate chain",
RunE: runCreate,
}
// Flag variables
kmsType string
kmsRegion string
kmsKeyID string
kmsVaultName string
kmsTenantID string
kmsCredsFile string
rootTemplatePath string
intermTemplatePath string
rootKeyID string
intermediateKeyID string
rootCertPath string
intermCertPath string
rawJSON = []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
"initialFields": {"service": "sigstore-certificate-maker"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase",
"timeKey": "timestamp",
"timeEncoder": "iso8601"
}
}`)
)
func init() {
logger = initLogger()
// Add create command
rootCmd.AddCommand(createCmd)
// Add flags to create command
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", "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"),
RootKeyID: getConfigValue(rootKeyID, "KMS_ROOT_KEY_ID"),
IntermediateKeyID: getConfigValue(intermediateKeyID, "KMS_INTERMEDIATE_KEY_ID"),
Options: make(map[string]string),
}
// Handle provider-specific options
switch kmsConfig.Type {
case "cloudkms":
if credsFile := getConfigValue(kmsCredsFile, "KMS_CREDENTIALS_FILE"); credsFile != "" {
kmsConfig.Options["credentials-file"] = credsFile
}
case "azurekms":
if vaultName := getConfigValue(kmsVaultName, "KMS_VAULT_NAME"); vaultName != "" {
kmsConfig.Options["vault-name"] = vaultName
}
if tenantID := getConfigValue(kmsTenantID, "KMS_TENANT_ID"); tenantID != "" {
kmsConfig.Options["tenant-id"] = tenantID
}
}
ctx := context.Background()
km, err := initKMS(ctx, kmsConfig)
if err != nil {
return fmt.Errorf("failed to initialize KMS: %w", err)
}
// Validate template paths
if err := validateTemplatePath(rootTemplatePath); err != nil {
return fmt.Errorf("root template error: %w", err)
}
if err := validateTemplatePath(intermTemplatePath); err != nil {
return fmt.Errorf("intermediate template error: %w", err)
}
return createCertificates(km, kmsConfig, rootTemplatePath, intermTemplatePath, rootCertPath, intermCertPath)
}
func main() {
if err := rootCmd.Execute(); err != nil {
logger.Fatal("Command failed", zap.Error(err))
}
}
// 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)
}
opts := apiv1.Options{
Type: apiv1.Type(config.Type),
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", keyID, config.Region)
return awskms.New(ctx, opts)
case "cloudkms":
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"], keyID)
if config.Options["tenant-id"] != "" {
opts.URI += fmt.Sprintf("?tenant-id=%s", config.Options["tenant-id"])
}
return azurekms.New(ctx, opts)
default:
return nil, fmt.Errorf("unsupported KMS type: %s", config.Type)
}
}
// 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 {
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,
})
if err != nil {
return fmt.Errorf("error creating root signer: %w", err)
}
// Create root certificate
rootCert, err := x509util.CreateCertificate(rootTmpl, rootTmpl, rootSigner.Public(), rootSigner)
if err != nil {
return fmt.Errorf("error creating root certificate: %w", err)
}
// Parse intermediate template
intermediateTmpl, err := ParseTemplate(intermediateTemplatePath, rootCert)
if err != nil {
return fmt.Errorf("error parsing intermediate template: %w", err)
}
intermediateKeyName := config.IntermediateKeyID
if config.Type == "azurekms" {
intermediateKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s",
config.Options["vault-name"], config.IntermediateKeyID)
}
intermediateSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: intermediateKeyName,
})
if err != nil {
return fmt.Errorf("error creating intermediate signer: %w", err)
}
// Create intermediate certificate
intermediateCert, err := x509util.CreateCertificate(intermediateTmpl, rootCert, intermediateSigner.Public(), rootSigner)
if err != nil {
return fmt.Errorf("error creating intermediate certificate: %w", err)
}
if err := writeCertificateToFile(rootCert, rootCertPath); err != nil {
return fmt.Errorf("error writing root certificate: %w", err)
}
if err := writeCertificateToFile(intermediateCert, intermCertPath); err != nil {
return fmt.Errorf("error writing intermediate certificate: %w", err)
}
// Verify certificate chain
pool := x509.NewCertPool()
pool.AddCert(rootCert)
if _, err := intermediateCert.Verify(x509.VerifyOptions{
Roots: pool,
}); err != nil {
return fmt.Errorf("CA.Intermediate.Verify() error = %v", err)
}
logger.Info("Certificates created successfully",
zap.String("root_cert", rootCert.Subject.CommonName),
zap.String("intermediate_cert", intermediateCert.Subject.CommonName),
zap.Bool("root_is_ca", rootCert.IsCA),
zap.Bool("intermediate_is_ca", intermediateCert.IsCA),
zap.Int("root_path_len", rootCert.MaxPathLen),
zap.String("key_usage", fmt.Sprintf("%v", rootCert.KeyUsage)),
zap.String("ext_key_usage", fmt.Sprintf("%v", rootCert.ExtKeyUsage)))
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",
Bytes: cert.Raw,
}
file, err := os.Create(filename)
if err != nil {
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)
}
return nil
}
// 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")
}
if config.RootKeyID == "" && config.IntermediateKeyID == "" {
return fmt.Errorf("at least one of RootKeyID or IntermediateKeyID must be specified")
}
switch config.Type {
case "awskms":
if config.Region == "" {
return fmt.Errorf("region is required for AWS KMS")
}
case "cloudkms":
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"] == "" {
return fmt.Errorf("vault-name is required for Azure KMS")
}
if config.Options["tenant-id"] == "" {
return fmt.Errorf("tenant-id is required for Azure KMS")
}
}
return nil
}
func getConfigValue(flagValue, envVar string) string {
if flagValue != "" {
return flagValue
}
return os.Getenv(envVar)
}
func initLogger() *zap.Logger {
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
return zap.Must(cfg.Build())
}
func validateTemplatePath(path string) error {
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("template not found at %s: %w", path, err)
}
if !strings.HasSuffix(path, ".json") {
return fmt.Errorf("template file must have .json extension: %s", path)
}
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading template file: %w", err)
}
var js json.RawMessage
if err := json.Unmarshal(content, &js); err != nil {
return fmt.Errorf("invalid JSON in template file: %w", err)
}
return nil
}