Skip to content

Commit

Permalink
feat(certmaker): adds sigstore library for kms signatures and hashiva…
Browse files Browse the repository at this point in the history
…ult support.
  • Loading branch information
ianhundere committed Dec 18, 2024
1 parent 2d28296 commit 3d9f577
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 59 deletions.
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 // indirect
Expand Down
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvUL
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2htVQTBY8nOZpyajYztF0vUvSZTuM=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw=
Expand Down Expand Up @@ -408,8 +404,6 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
Expand Down
210 changes: 159 additions & 51 deletions pkg/certmaker/certmaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,49 @@
//

// Package certmaker implements a certificate creation utility for Fulcio.
// It supports creating root, intermediate, and leaf certs using (AWS, GCP, Azure).
// It supports creating root, intermediate, and leaf certs using (AWS, GCP, Azure, HashiVault).
package certmaker

import (
"bytes"
"context"
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"os"
"strings"

"go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/awskms"
"go.step.sm/crypto/kms/azurekms"
"go.step.sm/crypto/kms/cloudkms"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/kms"
"github.com/sigstore/sigstore/pkg/signature/options"

// Initialize AWS KMS provider
_ "github.com/sigstore/sigstore/pkg/signature/kms/aws"
// Initialize Azure KMS provider
_ "github.com/sigstore/sigstore/pkg/signature/kms/azure"
// Initialize GCP KMS provider
_ "github.com/sigstore/sigstore/pkg/signature/kms/gcp"
// Initialize HashiVault KMS provider
_ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault"
"go.step.sm/crypto/x509util"
)

type signerWrapper struct {
signature.SignerVerifier
}

func (s signerWrapper) Public() crypto.PublicKey {
key, _ := s.PublicKey()
return key
}

func (s signerWrapper) Sign(_ io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) {
return s.SignMessage(bytes.NewReader(digest), options.WithDigest(digest))
}

// KMSConfig holds config for KMS providers.
type KMSConfig struct {
Type string
Expand All @@ -45,58 +68,94 @@ type KMSConfig struct {
}

// 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) {
func InitKMS(ctx context.Context, config KMSConfig) (signature.SignerVerifier, 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: "",
}

// Falls back to LeafKeyID if root is not set
keyID := config.RootKeyID
if keyID == "" {
keyID = config.LeafKeyID
}

var sv signature.SignerVerifier
var err error

switch config.Type {
case "awskms":
opts.URI = fmt.Sprintf("awskms:///%s?region=%s", keyID, config.Region)
return awskms.New(ctx, opts)
case "gcpkms":
opts.Type = apiv1.Type("cloudkms")
opts.URI = fmt.Sprintf("cloudkms:%s", keyID)
if credFile, ok := config.Options["credentials-file"]; ok {
if _, err := os.Stat(credFile); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("credentials file not found: %s", credFile)
}
return nil, fmt.Errorf("error accessing credentials file: %w", err)
}
opts.URI += fmt.Sprintf("?credentials-file=%s", credFile)
ref := fmt.Sprintf("awskms:///%s", keyID)
if config.Region != "" {
os.Setenv("AWS_REGION", config.Region)
}
sv, err = kms.Get(ctx, ref, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("failed to initialize AWS KMS: %w", err)
}
km, err := cloudkms.New(ctx, opts)

case "gcpkms":
ref := fmt.Sprintf("gcpkms://%s", keyID)
sv, err = kms.Get(ctx, ref, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("failed to initialize GCP KMS: %w", err)
}
return km, nil

case "azurekms":
opts.URI = keyID
if config.Options["tenant-id"] != "" {
opts.URI += fmt.Sprintf("?tenant-id=%s", config.Options["tenant-id"])
keyURI := keyID
if strings.HasPrefix(keyID, "azurekms:name=") {
nameStart := strings.Index(keyID, "name=") + 5
vaultIndex := strings.Index(keyID, ";vault=")
if vaultIndex != -1 {
keyName := strings.TrimSpace(keyID[nameStart:vaultIndex])
vaultName := strings.TrimSpace(keyID[vaultIndex+7:])
keyURI = fmt.Sprintf("azurekms://%s.vault.azure.net/%s", vaultName, keyName)
}
}
if config.Options != nil && config.Options["tenant-id"] != "" {
os.Setenv("AZURE_TENANT_ID", config.Options["tenant-id"])
os.Setenv("AZURE_ADDITIONALLY_ALLOWED_TENANTS", "*")
}
return azurekms.New(ctx, opts)
os.Setenv("AZURE_AUTHORITY_HOST", "https://login.microsoftonline.com/")

sv, err = kms.Get(ctx, keyURI, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("failed to initialize Azure KMS: %w", err)
}

case "hashivault":
keyURI := fmt.Sprintf("hashivault://%s", keyID)
if config.Options != nil {
if token := config.Options["token"]; token != "" {
os.Setenv("VAULT_TOKEN", token)
}
if addr := config.Options["address"]; addr != "" {
os.Setenv("VAULT_ADDR", addr)
}
}

sv, err = kms.Get(ctx, keyURI, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("failed to initialize HashiVault KMS: %w", err)
}

default:
return nil, fmt.Errorf("unsupported KMS type: %s", config.Type)
}

if err != nil {
return nil, fmt.Errorf("failed to get KMS signer: %w", err)
}
if sv == nil {
return nil, fmt.Errorf("KMS returned nil signer")
}

return sv, nil
}

// CreateCertificates creates certificates using the provided KMS and templates.
// It creates 3 certificates (root -> intermediate -> leaf) if intermediateKeyID is provided,
// otherwise creates just 2 certs (root -> leaf).
func CreateCertificates(km apiv1.KeyManager, config KMSConfig,
func CreateCertificates(sv signature.SignerVerifier, config KMSConfig,
rootTemplatePath, leafTemplatePath string,
rootCertPath, leafCertPath string,
intermediateKeyID, intermediateTemplatePath, intermediateCertPath string) error {
Expand All @@ -107,14 +166,13 @@ func CreateCertificates(km apiv1.KeyManager, config KMSConfig,
return fmt.Errorf("error parsing root template: %w", err)
}

rootSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: config.RootKeyID,
})
// Get public key from signer
rootPubKey, err := sv.PublicKey()
if err != nil {
return fmt.Errorf("error creating root signer: %w", err)
return fmt.Errorf("error getting root public key: %w", err)
}

rootCert, err := x509util.CreateCertificate(rootTmpl, rootTmpl, rootSigner.Public(), rootSigner)
rootCert, err := x509util.CreateCertificate(rootTmpl, rootTmpl, rootPubKey, signerWrapper{sv})
if err != nil {
return fmt.Errorf("error creating root certificate: %w", err)
}
Expand All @@ -133,14 +191,20 @@ func CreateCertificates(km apiv1.KeyManager, config KMSConfig,
return fmt.Errorf("error parsing intermediate template: %w", err)
}

intermediateSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: intermediateKeyID,
})
// Initialize new KMS for intermediate key
intermediateConfig := config
intermediateConfig.RootKeyID = intermediateKeyID
intermediateSV, err := InitKMS(context.Background(), intermediateConfig)
if err != nil {
return fmt.Errorf("error initializing intermediate KMS: %w", err)
}

intermediatePubKey, err := intermediateSV.PublicKey()
if err != nil {
return fmt.Errorf("error creating intermediate signer: %w", err)
return fmt.Errorf("error getting intermediate public key: %w", err)
}

intermediateCert, err := x509util.CreateCertificate(intermediateTmpl, rootCert, intermediateSigner.Public(), rootSigner)
intermediateCert, err := x509util.CreateCertificate(intermediateTmpl, rootCert, intermediatePubKey, signerWrapper{sv})
if err != nil {
return fmt.Errorf("error creating intermediate certificate: %w", err)
}
Expand All @@ -150,10 +214,10 @@ func CreateCertificates(km apiv1.KeyManager, config KMSConfig,
}

signingCert = intermediateCert
signingKey = intermediateSigner
signingKey = signerWrapper{intermediateSV}
} else {
signingCert = rootCert
signingKey = rootSigner
signingKey = signerWrapper{sv}
}

// Create leaf cert
Expand All @@ -162,14 +226,20 @@ func CreateCertificates(km apiv1.KeyManager, config KMSConfig,
return fmt.Errorf("error parsing leaf template: %w", err)
}

leafSigner, err := km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: config.LeafKeyID,
})
// Initialize new KMS for leaf key
leafConfig := config
leafConfig.RootKeyID = config.LeafKeyID
leafSV, err := InitKMS(context.Background(), leafConfig)
if err != nil {
return fmt.Errorf("error creating leaf signer: %w", err)
return fmt.Errorf("error initializing leaf KMS: %w", err)
}

leafCert, err := x509util.CreateCertificate(leafTmpl, signingCert, leafSigner.Public(), signingKey)
leafPubKey, err := leafSV.PublicKey()
if err != nil {
return fmt.Errorf("error getting leaf public key: %w", err)
}

leafCert, err := x509util.CreateCertificate(leafTmpl, signingCert, leafPubKey, signingKey)
if err != nil {
return fmt.Errorf("error creating leaf certificate: %w", err)
}
Expand Down Expand Up @@ -229,19 +299,20 @@ func ValidateKMSConfig(config KMSConfig) error {
if keyID == "" {
return nil
}
if strings.HasPrefix(keyID, "arn:aws:kms:") {
switch {
case strings.HasPrefix(keyID, "arn:aws:kms:"):
parts := strings.Split(keyID, ":")
if len(parts) < 6 {
return fmt.Errorf("invalid AWS KMS ARN format for %s", keyType)
}
if parts[3] != config.Region {
return fmt.Errorf("region in ARN (%s) does not match configured region (%s)", parts[3], config.Region)
}
} else if strings.HasPrefix(keyID, "alias/") {
case strings.HasPrefix(keyID, "alias/"):
if strings.TrimPrefix(keyID, "alias/") == "" {
return fmt.Errorf("alias name cannot be empty for %s", keyType)
}
} else {
default:
return fmt.Errorf("awskms %s must start with 'arn:aws:kms:' or 'alias/'", keyType)
}
return nil
Expand Down Expand Up @@ -327,6 +398,43 @@ func ValidateKMSConfig(config KMSConfig) error {
return err
}

case "hashivault":
// HashiVault KMS validation
if config.Options == nil {
return fmt.Errorf("options map is required for HashiVault KMS")
}
if config.Options["address"] == "" {
return fmt.Errorf("address is required for HashiVault KMS")
}
if config.Options["token"] == "" {
return fmt.Errorf("token is required for HashiVault KMS")
}
validateHashiVaultKeyID := func(keyID, keyType string) error {
if keyID == "" {
return nil
}
parts := strings.Split(keyID, "/")
if len(parts) < 3 {
return fmt.Errorf("hashivault %s must be in format: transit/keys/keyname", keyType)
}
if parts[0] != "transit" || parts[1] != "keys" {
return fmt.Errorf("hashivault %s must start with 'transit/keys/'", keyType)
}
if parts[2] == "" {
return fmt.Errorf("key name cannot be empty for %s", keyType)
}
return nil
}
if err := validateHashiVaultKeyID(config.RootKeyID, "RootKeyID"); err != nil {
return err
}
if err := validateHashiVaultKeyID(config.IntermediateKeyID, "IntermediateKeyID"); err != nil {
return err
}
if err := validateHashiVaultKeyID(config.LeafKeyID, "LeafKeyID"); err != nil {
return err
}

default:
return fmt.Errorf("unsupported KMS type: %s", config.Type)
}
Expand Down

0 comments on commit 3d9f577

Please sign in to comment.