Skip to content

Commit

Permalink
fix: enables timestamping / improves validation / includes leaf wording.
Browse files Browse the repository at this point in the history
Signed-off-by: ianhundere <[email protected]>
  • Loading branch information
ianhundere committed Nov 30, 2024
1 parent d846fe9 commit 82768a7
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 185 deletions.
59 changes: 32 additions & 27 deletions cmd/certificate_maker/certificate_maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,31 @@
//

// Package main implements a certificate creation utility for Timestamp Authority.
// It supports creating root and intermediate certificates using(AWS, GCP, Azure).
// It supports creating root and leaf certificates using (AWS, GCP, Azure).
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/sigstore/timestamp-authority/pkg/certmaker"
"github.com/spf13/cobra"
"go.uber.org/zap"
)

// CLI flags and env vars for config.
// Supports AWS KMS, Google Cloud KMS, and Azure Key Vault configurations.
var (
logger *zap.Logger
version string

rootCmd = &cobra.Command{
Use: "tsa-certificate-maker",
Short: "Create certificate chains for Timestamp Authority",
Long: `A tool for creating root and intermediate certificates for Timestamp Authority with timestamping capabilities`,
Long: `A tool for creating root and leaf certificates for Timestamp Authority with timestamping capabilities`,
Version: version,
}

Expand All @@ -45,18 +48,18 @@ var (
RunE: runCreate,
}

kmsType string
kmsRegion string
kmsKeyID string
kmsVaultName string
kmsTenantID string
kmsCredsFile string
rootTemplatePath string
intermTemplatePath string
rootKeyID string
intermediateKeyID string
rootCertPath string
intermCertPath string
kmsType string
kmsRegion string
kmsKeyID string
kmsVaultName string
kmsTenantID string
kmsCredsFile string
rootTemplatePath string
leafTemplatePath string
rootKeyID string
leafKeyID string
rootCertPath string
leafCertPath string

rawJSON = []byte(`{
"level": "debug",
Expand Down Expand Up @@ -87,22 +90,25 @@ func init() {
createCmd.Flags().StringVar(&kmsCredsFile, "kms-credentials-file", "", "Path to credentials file (for Google Cloud KMS)")
createCmd.Flags().StringVar(&rootTemplatePath, "root-template",
"pkg/certmaker/templates/root-template.json", "Path to root certificate template")
createCmd.Flags().StringVar(&intermTemplatePath, "intermediate-template",
"pkg/certmaker/templates/intermediate-template.json", "Path to intermediate certificate template")
createCmd.Flags().StringVar(&leafTemplatePath, "leaf-template",
"pkg/certmaker/templates/leaf-template.json", "Path to leaf 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(&leafKeyID, "leaf-key-id", "", "KMS key identifier for leaf 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")
createCmd.Flags().StringVar(&leafCertPath, "leaf-cert", "leaf.pem", "Output path for leaf certificate")
}

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

// Build KMS config from flags and environment
config := certmaker.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),
Type: getConfigValue(kmsType, "KMS_TYPE"),
Region: getConfigValue(kmsRegion, "KMS_REGION"),
RootKeyID: getConfigValue(rootKeyID, "KMS_ROOT_KEY_ID"),
LeafKeyID: getConfigValue(leafKeyID, "KMS_LEAF_KEY_ID"),
Options: make(map[string]string),
}

// Handle KMS provider options
Expand All @@ -120,7 +126,6 @@ func runCreate(cmd *cobra.Command, args []string) error {
}
}

ctx := context.Background()
km, err := certmaker.InitKMS(ctx, config)
if err != nil {
return fmt.Errorf("failed to initialize KMS: %w", err)
Expand All @@ -130,11 +135,11 @@ func runCreate(cmd *cobra.Command, args []string) error {
if err := certmaker.ValidateTemplatePath(rootTemplatePath); err != nil {
return fmt.Errorf("root template error: %w", err)
}
if err := certmaker.ValidateTemplatePath(intermTemplatePath); err != nil {
return fmt.Errorf("intermediate template error: %w", err)
if err := certmaker.ValidateTemplatePath(leafTemplatePath); err != nil {
return fmt.Errorf("leaf template error: %w", err)
}

return certmaker.CreateCertificates(km, config, rootTemplatePath, intermTemplatePath, rootCertPath, intermCertPath)
return certmaker.CreateCertificates(km, config, rootTemplatePath, leafTemplatePath, rootCertPath, leafCertPath)
}

func main() {
Expand Down
77 changes: 34 additions & 43 deletions pkg/certmaker/certmaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package certmaker
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
Expand All @@ -33,28 +32,30 @@ import (
"go.step.sm/crypto/x509util"
)

// KMSConfig holds config for KMS providers.
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
Type string // KMS provider type: "awskms", "cloudkms", "azurekms"
Region string // AWS region or Cloud location
RootKeyID string // Root CA key identifier
LeafKeyID string // Leaf CA key identifier
Options map[string]string // Provider-specific options
}

// InitKMS initializes KMS provider based on the given config, KMSConfig.
// Supports AWS KMS, Google Cloud KMS, and Azure Key Vault.
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
// Falls back to IntermediateKeyID if root is not set
keyID := config.RootKeyID
if keyID == "" {
keyID = config.IntermediateKeyID
keyID = config.LeafKeyID
}

switch config.Type {
Expand Down Expand Up @@ -83,12 +84,11 @@ func InitKMS(ctx context.Context, config KMSConfig) (apiv1.KeyManager, error) {
// It creates both root and intermediate certificates using the provided templates
// and KMS signing keys.
func CreateCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath, intermediateTemplatePath, rootCertPath, intermCertPath string) error {
// Parse templates
// Parse root template
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",
Expand All @@ -102,52 +102,50 @@ func CreateCertificates(km apiv1.KeyManager, config KMSConfig, rootTemplatePath,
return fmt.Errorf("error creating root signer: %w", err)
}

// Create root certificate
// Create root cert
rootCert, err := x509util.CreateCertificate(rootTmpl, rootTmpl, rootSigner.Public(), rootSigner)
if err != nil {
return fmt.Errorf("error creating root certificate: %w", err)
}
if err := WriteCertificateToFile(rootCert, rootCertPath); err != nil {
return fmt.Errorf("error writing root certificate: %w", err)
}

// Parse intermediate template
// Parse / sign intermediate template
intermediateTmpl, err := ParseTemplate(intermediateTemplatePath, rootCert)
if err != nil {
return fmt.Errorf("error parsing intermediate template: %w", err)
}

intermediateKeyName := config.IntermediateKeyID
intermediateKeyName := config.LeafKeyID
if config.Type == "azurekms" {
intermediateKeyName = fmt.Sprintf("azurekms:vault=%s;name=%s",
config.Options["vault-name"], config.IntermediateKeyID)
config.Options["vault-name"], config.LeafKeyID)
}

intermediateSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: intermediateKeyName,
})
if err != nil {
return fmt.Errorf("error creating intermediate signer: %w", err)
}

// Create intermediate certificate
// Create intermediate/leaf cert
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)
opts := x509.VerifyOptions{
Roots: pool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
}
if _, err := intermediateCert.Verify(opts); err != nil {
return fmt.Errorf("certificate chain verification failed: %w", err)
}

return nil
Expand All @@ -165,11 +163,15 @@ func WriteCertificateToFile(cert *x509.Certificate, filename string) error {
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)
}

certType := "root"
if cert.Subject.OrganizationalUnit != nil && cert.Subject.OrganizationalUnit[0] == "TSA Intermediate CA" {
certType = "intermediate"
}
fmt.Printf("Your %s certificate has been saved in %s.\n", certType, filename)
return nil
}

Expand All @@ -178,8 +180,8 @@ 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")
if config.RootKeyID == "" && config.LeafKeyID == "" {
return fmt.Errorf("at least one of RootKeyID or LeafKeyID must be specified")
}

switch config.Type {
Expand All @@ -191,8 +193,8 @@ func ValidateKMSConfig(config KMSConfig) error {
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/'")
if config.LeafKeyID != "" && !strings.HasPrefix(config.LeafKeyID, "projects/") {
return fmt.Errorf("cloudkms LeafKeyID must start with 'projects/'")
}
case "azurekms":
if config.Options["vault-name"] == "" {
Expand All @@ -211,20 +213,9 @@ 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
}
Loading

0 comments on commit 82768a7

Please sign in to comment.