|
| 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