Skip to content

Commit

Permalink
feat: adds templating, positional arg for common name and other impro…
Browse files Browse the repository at this point in the history
…vements.

Signed-off-by: ianhundere <[email protected]>
  • Loading branch information
ianhundere committed Jan 24, 2025
1 parent 3d56169 commit 10601d4
Show file tree
Hide file tree
Showing 10 changed files with 980 additions and 3,228 deletions.
105 changes: 65 additions & 40 deletions cmd/certificate_maker/certificate_maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/sigstore/timestamp-authority/pkg/certmaker"
Expand All @@ -44,9 +45,14 @@ var (
}

createCmd = &cobra.Command{
Use: "create",
Use: "create [common-name]",
Short: "Create certificate chain",
RunE: runCreate,
Long: `Create a certificate chain with the specified common name.
The common name will be used as the Subject Common Name for the certificates.
If no common name is provided, the values from the templates will be used.
Example: tsa-certificate-maker create "https://timestamp.example.com"`,
Args: cobra.RangeArgs(0, 1),
RunE: runCreate,
}
)

Expand All @@ -65,72 +71,83 @@ func mustBindEnv(key, envVar string) {
func init() {
log.ConfigureLogger("prod")

viper.AutomaticEnv()
viper.SetEnvPrefix("")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))

mustBindEnv("kms-type", "KMS_TYPE")
mustBindEnv("aws-region", "AWS_REGION")
mustBindEnv("azure-tenant-id", "AZURE_TENANT_ID")
mustBindEnv("gcp-credentials-file", "GCP_CREDENTIALS_FILE")
mustBindEnv("vault-token", "VAULT_TOKEN")
mustBindEnv("vault-address", "VAULT_ADDR")
mustBindEnv("root-key-id", "KMS_ROOT_KEY_ID")
mustBindEnv("intermediate-key-id", "KMS_INTERMEDIATE_KEY_ID")
mustBindEnv("leaf-key-id", "KMS_LEAF_KEY_ID")

rootCmd.AddCommand(createCmd)
rootCmd.SetVersionTemplate("{{.Version}}\n")

// KMS provider flags
createCmd.Flags().String("kms-type", "", "KMS provider type (awskms, gcpkms, azurekms, hashivault)")
createCmd.Flags().String("kms-type", "", "KMS provider type")
createCmd.Flags().String("aws-region", "", "AWS KMS region")
createCmd.Flags().String("azure-tenant-id", "", "Azure KMS tenant ID")
createCmd.Flags().String("gcp-credentials-file", "", "Path to credentials file for GCP KMS")
createCmd.Flags().String("vault-token", "", "HashiVault token")
createCmd.Flags().String("vault-address", "", "HashiVault server address")

// Root certificate flags
createCmd.Flags().String("root-template", "pkg/certmaker/templates/root-template.json", "Path to root certificate template")
createCmd.Flags().String("root-key-id", "", "KMS key identifier for root certificate")
createCmd.Flags().String("root-template", "", "Path to root certificate template (optional)")
createCmd.Flags().String("root-cert", "root.pem", "Output path for root certificate")

// Intermediate certificate flags
createCmd.Flags().String("intermediate-template", "pkg/certmaker/templates/intermediate-template.json", "Path to intermediate certificate template")
createCmd.Flags().String("intermediate-key-id", "", "KMS key identifier for intermediate certificate")
createCmd.Flags().String("intermediate-template", "", "Path to intermediate certificate template (optional)")
createCmd.Flags().String("intermediate-cert", "intermediate.pem", "Output path for intermediate certificate")

// Leaf certificate flags
createCmd.Flags().String("leaf-template", "pkg/certmaker/templates/leaf-template.json", "Path to leaf certificate template")
createCmd.Flags().String("leaf-key-id", "", "KMS key identifier for leaf certificate")
createCmd.Flags().String("leaf-template", "", "Path to leaf certificate template (optional)")
createCmd.Flags().String("leaf-cert", "leaf.pem", "Output path for leaf certificate")

// Bind flags to viper
// Lifetime flags
createCmd.Flags().Duration("root-lifetime", 87600*time.Hour, "Root certificate lifetime")
createCmd.Flags().Duration("intermediate-lifetime", 43800*time.Hour, "Intermediate certificate lifetime")
createCmd.Flags().Duration("leaf-lifetime", 8760*time.Hour, "Leaf certificate lifetime")

mustBindPFlag("kms-type", createCmd.Flags().Lookup("kms-type"))
mustBindPFlag("aws-region", createCmd.Flags().Lookup("aws-region"))
mustBindPFlag("azure-tenant-id", createCmd.Flags().Lookup("azure-tenant-id"))
mustBindPFlag("gcp-credentials-file", createCmd.Flags().Lookup("gcp-credentials-file"))
mustBindPFlag("vault-token", createCmd.Flags().Lookup("vault-token"))
mustBindPFlag("vault-address", createCmd.Flags().Lookup("vault-address"))

mustBindPFlag("root-template", createCmd.Flags().Lookup("root-template"))
mustBindPFlag("root-key-id", createCmd.Flags().Lookup("root-key-id"))
mustBindPFlag("root-template", createCmd.Flags().Lookup("root-template"))
mustBindPFlag("root-cert", createCmd.Flags().Lookup("root-cert"))

mustBindPFlag("intermediate-template", createCmd.Flags().Lookup("intermediate-template"))
mustBindPFlag("intermediate-key-id", createCmd.Flags().Lookup("intermediate-key-id"))
mustBindPFlag("intermediate-template", createCmd.Flags().Lookup("intermediate-template"))
mustBindPFlag("intermediate-cert", createCmd.Flags().Lookup("intermediate-cert"))

mustBindPFlag("leaf-template", createCmd.Flags().Lookup("leaf-template"))
mustBindPFlag("leaf-key-id", createCmd.Flags().Lookup("leaf-key-id"))
mustBindPFlag("leaf-template", createCmd.Flags().Lookup("leaf-template"))
mustBindPFlag("leaf-cert", createCmd.Flags().Lookup("leaf-cert"))

// Bind environment variables
mustBindEnv("kms-type", "KMS_TYPE")
mustBindEnv("aws-region", "AWS_REGION")
mustBindEnv("azure-tenant-id", "AZURE_TENANT_ID")
mustBindEnv("gcp-credentials-file", "GCP_CREDENTIALS_FILE")
mustBindEnv("vault-token", "VAULT_TOKEN")
mustBindEnv("vault-address", "VAULT_ADDR")

mustBindEnv("root-key-id", "KMS_ROOT_KEY_ID")
mustBindEnv("intermediate-key-id", "KMS_INTERMEDIATE_KEY_ID")
mustBindEnv("leaf-key-id", "KMS_LEAF_KEY_ID")
mustBindPFlag("root-lifetime", createCmd.Flags().Lookup("root-lifetime"))
mustBindPFlag("intermediate-lifetime", createCmd.Flags().Lookup("intermediate-lifetime"))
mustBindPFlag("leaf-lifetime", createCmd.Flags().Lookup("leaf-lifetime"))
}

func runCreate(_ *cobra.Command, _ []string) error {
defer func() { rootCmd.SilenceUsage = true }()
func runCreate(_ *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Get common name from args if provided, otherwise templates used
commonName := viper.GetString("common-name")
if len(args) > 0 {
commonName = args[0]
}

// Build KMS config from flags and environment
config := certmaker.KMSConfig{
CommonName: commonName,
Type: viper.GetString("kms-type"),
RootKeyID: viper.GetString("root-key-id"),
IntermediateKeyID: viper.GetString("intermediate-key-id"),
Expand All @@ -146,7 +163,7 @@ func runCreate(_ *cobra.Command, _ []string) error {
}
case "gcpkms":
if gcpCredsFile := viper.GetString("gcp-credentials-file"); gcpCredsFile != "" {
// Check if credentials file exists before trying to use it
// Check if gcp creds exists
if _, err := os.Stat(gcpCredsFile); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("failed to initialize KMS: credentials file not found: %s", gcpCredsFile)
Expand All @@ -173,24 +190,32 @@ func runCreate(_ *cobra.Command, _ []string) error {
return fmt.Errorf("failed to initialize KMS: %w", err)
}

// Validate template paths
// Validate template paths if provided
rootTemplate := viper.GetString("root-template")
leafTemplate := viper.GetString("leaf-template")
intermediateTemplate := viper.GetString("intermediate-template")

if err := certmaker.ValidateTemplatePath(rootTemplate); err != nil {
return fmt.Errorf("root template error: %w", err)
}
if err := certmaker.ValidateTemplatePath(leafTemplate); err != nil {
return fmt.Errorf("leaf template error: %w", err)
if rootTemplate != "" {
if err := certmaker.ValidateTemplate(rootTemplate, nil, "root"); err != nil {
return fmt.Errorf("root template error: %w", err)
}
}
if viper.GetString("intermediate-key-id") != "" {
if err := certmaker.ValidateTemplatePath(intermediateTemplate); err != nil {
return fmt.Errorf("intermediate template error: %w", err)
if leafTemplate != "" {
if err := certmaker.ValidateTemplate(leafTemplate, nil, "leaf"); err != nil {
return fmt.Errorf("leaf template error: %w", err)
}
}

return certmaker.CreateCertificates(km, config, viper.GetString("root-template"), viper.GetString("leaf-template"), viper.GetString("root-cert"), viper.GetString("leaf-cert"), viper.GetString("intermediate-key-id"), viper.GetString("intermediate-template"), viper.GetString("intermediate-cert"))
return certmaker.CreateCertificates(km, config,
rootTemplate,
leafTemplate,
viper.GetString("root-cert"),
viper.GetString("leaf-cert"),
viper.GetString("intermediate-key-id"),
viper.GetString("intermediate-template"),
viper.GetString("intermediate-cert"),
viper.GetDuration("root-lifetime"),
viper.GetDuration("intermediate-lifetime"),
viper.GetDuration("leaf-lifetime"))
}

func main() {
Expand Down
8 changes: 2 additions & 6 deletions cmd/certificate_maker/certificate_maker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ func TestGetConfigValue(t *testing.T) {
envValue string
want string
}{
// KMS provider flags
{
name: "get KMS type from flag",
flag: "kms-type",
Expand Down Expand Up @@ -85,7 +84,6 @@ func TestGetConfigValue(t *testing.T) {
envValue: "http://vault:8200",
want: "http://vault:8200",
},
// Root certificate flags
{
name: "get root key ID from env",
flag: "root-key-id",
Expand All @@ -94,7 +92,6 @@ func TestGetConfigValue(t *testing.T) {
envValue: "root-key-123",
want: "root-key-123",
},
// Intermediate certificate flags
{
name: "get intermediate key ID from env",
flag: "intermediate-key-id",
Expand All @@ -103,7 +100,6 @@ func TestGetConfigValue(t *testing.T) {
envValue: "intermediate-key-123",
want: "intermediate-key-123",
},
// Leaf certificate flags
{
name: "get leaf key ID from env",
flag: "leaf-key-id",
Expand Down Expand Up @@ -290,7 +286,7 @@ func TestRunCreate(t *testing.T) {
"--leaf-template", leafTmplPath,
},
wantError: true,
errMsg: "error getting root public key: getting public key: operation error KMS: GetPublicKey",
errMsg: "leaf template error: leaf certificate must have a parent",
},
{
name: "HashiVault KMS without token",
Expand Down Expand Up @@ -425,7 +421,7 @@ func TestCreateCommand(t *testing.T) {
"--leaf-template", leafTmplPath,
},
wantError: true,
errMsg: "error getting root public key: getting public key: operation error KMS: GetPublicKey",
errMsg: "leaf template error: leaf certificate must have a parent",
},
}

Expand Down
67 changes: 44 additions & 23 deletions docs/certificate-maker.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ This tool creates root, intermediate (optional), and leaf certificates for Times
- Two-level chain (root -> leaf)
- Three-level chain (root -> intermediate -> leaf)

Relies on [x509util](https://pkg.go.dev/go.step.sm/crypto/x509util) which builds X.509 certificates from JSON templates.
Relies on [x509util](https://pkg.go.dev/go.step.sm/crypto/x509util) which builds X.509 certificates from JSON templates. The tool includes embedded default templates that are compiled into the binary, making it ready to use without external template files.

## Requirements

- Access to one of the supported KMS providers (AWS, Google Cloud, Azure)
- Access to one of the supported KMS providers (AWS, Google Cloud, Azure, HashiCorp Vault)
- Pre-existing KMS keys (the tool uses existing keys and does not create new ones)

## Local Development
Expand All @@ -27,6 +27,14 @@ The tool can be configured using either command-line flags or environment variab

### Command-Line Interface

The `create` command accepts an optional positional argument for the common name:

```bash
./bin/tsa-certificate-maker create [common-name]
```

If no common name is provided, the values from the templates will be used.

Available flags:

- `--kms-type`: KMS provider type (awskms, gcpkms, azurekms, hashivault)
Expand All @@ -44,6 +52,9 @@ Available flags:
- `--intermediate-key-id`: KMS key identifier for intermediate certificate
- `--intermediate-template`: Path to intermediate certificate template
- `--intermediate-cert`: Output path for intermediate certificate
- `--root-lifetime`: Root certificate lifetime (default: 87600h, 10 years)
- `--intermediate-lifetime`: Intermediate certificate lifetime (default: 43800h, 5 years)
- `--leaf-lifetime`: Leaf certificate lifetime (default: 8760h, 1 year)

### Environment Variables

Expand All @@ -52,21 +63,20 @@ Available flags:
- `KMS_INTERMEDIATE_KEY_ID`: Key identifier for intermediate certificate
- `LEAF_KEY_ID`: Key identifier for leaf certificate
- `AWS_REGION`: AWS Region (required for AWS KMS)
- `KMS_VAULT_NAME`: Azure Key Vault name
- `AZURE_TENANT_ID`: Azure tenant ID
- `GCP_CREDENTIALS_FILE`: Path to credentials file (for Google Cloud KMS)
- `VAULT_ADDR`: HashiCorp Vault address
- `VAULT_TOKEN`: HashiCorp Vault token

### Certificate Templates

The tool uses JSON templates to define certificate properties:
The embedded templates are located in `pkg/certmaker/templates/` in the source code and are compiled into the binary. You can override these defaults by providing your own template files using:

- `root-template.json`: Defines root CA certificate properties
- `intermediate-template.json`: Defines intermediate CA certificate properties (when using --intermediate-key-id)
- `leaf-template.json`: Defines leaf certificate properties
- `--root-template`: Custom root CA template
- `--intermediate-template`: Custom intermediate CA template
- `--leaf-template`: Custom leaf template

Templates are located in `pkg/certmaker/templates/`.
If no custom templates are provided via flags, the tool will automatically use the embedded defaults which are designed to work with TSA's certificate requirements as long as the intended common name is used as a positional argument.

Note: Templates use ASN.1/OID format for timestamping-specific extensions.

Expand Down Expand Up @@ -96,7 +106,7 @@ export KMS_INTERMEDIATE_KEY_ID=projects/PROJECT_ID/locations/LOCATION/keyRings/K
```shell
export KMS_TYPE=azurekms
export ROOT_KEY_ID=azurekms:name=root-key;vault=tsa-keys
export KMS_INTERMEDIATE_KEY_ID=azurekms:name=leaf-key;vault=fulcio-keys
export KMS_INTERMEDIATE_KEY_ID=azurekms:name=leaf-key;vault=tsa-keys
export LEAF_KEY_ID=azurekms:name=leaf-key;vault=tsa-keys
export AZURE_TENANT_ID=83j229-83j229-83j229-83j229-83j229
```
Expand Down Expand Up @@ -242,54 +252,65 @@ Certificate:
Example with AWS KMS:

```bash
tsa-certificate-maker create \
tsa-certificate-maker create "https://tsa.example.com" \
--kms-type awskms \
--aws-region us-east-1 \
--root-key-id alias/tsa-root \
--leaf-key-id alias/tsa-leaf \
--root-template pkg/certmaker/templates/root-template.json \
--leaf-template pkg/certmaker/templates/leaf-template.json
--leaf-template pkg/certmaker/templates/leaf-template.json \
--root-lifetime 87600h \
--leaf-lifetime 8760h
```

Example with Azure KMS:

```bash
tsa-certificate-maker create \
tsa-certificate-maker create "https://tsa.example.com" \
--kms-type azurekms \
--azure-tenant-id 1b4a4fed-fed8-4823-a8a0-3d5cea83d122 \
--root-key-id "azurekms:name=sigstore-key;vault=sigstore-key" \
--leaf-key-id "azurekms:name=sigstore-key-intermediate;vault=sigstore-key" \
--intermediate-key-id "azurekms:name=sigstore-key-intermediate;vault=sigstore-key \
--intermediate-key-id "azurekms:name=sigstore-key-intermediate;vault=sigstore-key" \
--root-cert root.pem \
--leaf-cert leaf.pem \
--intermediate-cert intermediate.pem
--intermediate-cert intermediate.pem \
--root-lifetime 87600h \
--intermediate-lifetime 43800h \
--leaf-lifetime 8760h
```

Example with GCP KMS:

```bash
tsa-certificate-maker create \
tsa-certificate-maker create "https://tsa.example.com" \
--kms-type gcpkms \
---gcp-credentials-file ~/.config/gcloud/application_default_credentials.json \
--root-key-id projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/fulcio-key1/cryptoKeyVersions/<version> \
--intermediate-key-id projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/fulcio-key1/cryptoKeyVersions/<version> \
--leaf-key-id projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/fulcio-key1/cryptoKeyVersions/<version> \
--gcp-credentials-file ~/.config/gcloud/application_default_credentials.json \
--root-key-id projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/tsa-key1/cryptoKeyVersions/<version> \
--intermediate-key-id projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/tsa-key1/cryptoKeyVersions/<version> \
--leaf-key-id projects/<project_id>/locations/<location>/keyRings/<keyring>/cryptoKeys/tsa-key1/cryptoKeyVersions/<version> \
--root-cert root.pem \
--leaf-cert leaf.pem \
--intermediate-cert intermediate.pem
--intermediate-cert intermediate.pem \
--root-lifetime 87600h \
--intermediate-lifetime 43800h \
--leaf-lifetime 8760h
```

Example with HashiCorp Vault KMS:

```bash
tsa-certificate-maker create \
tsa-certificate-maker create "https://tsa.example.com" \
--kms-type hashivault \
--vault-address http://vault:8200 \
--vault-token token \
--root-key-id "transit/keys/root-key" \
--leaf-key-id "transit/keys/leaf-key" \
--intermediate-key-id "transit/keys/intermediate-key \
--intermediate-key-id "transit/keys/intermediate-key" \
--root-cert root.pem \
--leaf-cert leaf.pem \
--intermediate-cert intermediate.pem
--intermediate-cert intermediate.pem \
--root-lifetime 87600h \
--intermediate-lifetime 43800h \
--leaf-lifetime 8760h
```
Loading

0 comments on commit 10601d4

Please sign in to comment.