Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
go.opentelemetry.io/otel v1.31.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
go.opentelemetry.io/otel/sdk v1.31.0
go.step.sm/crypto v0.45.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
go.uber.org/zap/exp v0.3.0
Expand Down Expand Up @@ -145,7 +146,6 @@ require (
go.opentelemetry.io/otel/trace v1.31.0
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.24.0 // indirect
Expand Down
11 changes: 8 additions & 3 deletions modules/caddypki/adminapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,15 @@ func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
if err != nil {
return
}
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
if err != nil {
return

for _, interCert := range ca.IntermediateCertificateChain() {
pemBytes, err := pemEncodeCert(interCert.Raw)
if err != nil {
return nil, nil, err
}
inter = append(inter, pemBytes...)
}

return
}

Expand Down
53 changes: 36 additions & 17 deletions modules/caddypki/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ type CA struct {
// and module provisioning.
ID string `json:"-"`

storage certmagic.Storage
root, inter *x509.Certificate
interKey any // TODO: should we just store these as crypto.Signer?
mu *sync.RWMutex
storage certmagic.Storage
root *x509.Certificate
interChain []*x509.Certificate
interKey any // TODO: should we just store these as crypto.Signer?
mu *sync.RWMutex

rootCertPath string // mainly used for logging purposes if trusting
log *zap.Logger
Expand Down Expand Up @@ -129,14 +130,16 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
}

// load the certs and key that will be used for signing
var rootCert, interCert *x509.Certificate
var rootCert *x509.Certificate
var rootCertChain, interCertChain []*x509.Certificate
var rootKey, interKey crypto.Signer
var err error
if ca.Root != nil {
if ca.Root.Format == "" || ca.Root.Format == "pem_file" {
ca.rootCertPath = ca.Root.Certificate
}
rootCert, rootKey, err = ca.Root.Load()
rootCertChain, rootKey, err = ca.Root.Load()
rootCert = rootCertChain[0]
} else {
ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
rootCert, rootKey, err = ca.loadOrGenRoot()
Expand All @@ -145,16 +148,16 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
return err
}
if ca.Intermediate != nil {
interCert, interKey, err = ca.Intermediate.Load()
interCertChain, interKey, err = ca.Intermediate.Load()
} else {
interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
interCertChain, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
}
if err != nil {
return err
}

ca.mu.Lock()
ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey
ca.root, ca.interChain, ca.interKey = rootCert, interCertChain, interKey
ca.mu.Unlock()

return nil
Expand All @@ -180,7 +183,15 @@ func (ca CA) RootKey() (any, error) {
func (ca CA) IntermediateCertificate() *x509.Certificate {
ca.mu.RLock()
defer ca.mu.RUnlock()
return ca.inter
return ca.interChain[0]
}

// IntermediateCertificateChain returns the CA's intermediate
// certificate chain.
func (ca CA) IntermediateCertificateChain() []*x509.Certificate {
ca.mu.RLock()
defer ca.mu.RUnlock()
return ca.interChain
}

// IntermediateKey returns the CA's intermediate private key.
Expand Down Expand Up @@ -218,13 +229,14 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit
// sure it's always fresh, because the intermediate may
// renew while Caddy is running (medium lifetime)
signerOption = authority.WithX509SignerFunc(func() ([]*x509.Certificate, crypto.Signer, error) {
issuerCert := ca.IntermediateCertificate()
issuerChain := ca.IntermediateCertificateChain()
issuerCert := issuerChain[0]
issuerKey := ca.IntermediateKey().(crypto.Signer)
ca.log.Debug("using intermediate signer",
zap.String("serial", issuerCert.SerialNumber.String()),
zap.String("not_before", issuerCert.NotBefore.String()),
zap.String("not_after", issuerCert.NotAfter.String()))
return []*x509.Certificate{issuerCert}, issuerKey, nil
return issuerChain, issuerKey, nil
})
}

Expand All @@ -250,7 +262,11 @@ func (ca *CA) NewAuthority(authorityConfig AuthorityConfig) (*authority.Authorit

func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err error) {
if ca.Root != nil {
return ca.Root.Load()
rootChain, rootSigner, err := ca.Root.Load()
if err != nil {
return nil, nil, err
}
return rootChain[0], rootSigner, nil
}
rootCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyRootCert())
if err != nil {
Expand Down Expand Up @@ -312,7 +328,8 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey crypto.Signer, err e
return rootCert, rootKey, nil
}

func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCertChain []*x509.Certificate, interKey crypto.Signer, err error) {
var interCert *x509.Certificate
interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert())
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
Expand All @@ -324,10 +341,12 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
if err != nil {
return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err)
}

interCertChain = append(interCertChain, interCert)
}

if interCert == nil {
interCert, err = pemDecodeSingleCert(interCertPEM)
if len(interCertChain) == 0 {
interCertChain, err = pemDecodeCertificateChain(interCertPEM)
if err != nil {
return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err)
}
Expand All @@ -344,7 +363,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
}
}

return interCert, interKey, nil
return interCertChain, interKey, nil
}

func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
Expand Down
58 changes: 55 additions & 3 deletions modules/caddypki/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ package caddypki
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"

"github.com/caddyserver/certmagic"
"go.step.sm/crypto/pemutil"
)

func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
Expand All @@ -39,6 +44,15 @@ func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
return x509.ParseCertificate(pemBlock.Bytes)
}

func pemDecodeCertificateChain(pemDER []byte) ([]*x509.Certificate, error) {
chain, err := pemutil.ParseCertificateBundle(pemDER)
if err != nil {
return nil, fmt.Errorf("failed parsing certificate chain: %w", err)
}

return chain, nil
}

func pemEncodeCert(der []byte) ([]byte, error) {
return pemEncode("CERTIFICATE", der)
}
Expand Down Expand Up @@ -71,14 +85,14 @@ type KeyPair struct {
}

// Load loads the certificate and key.
func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
func (kp KeyPair) Load() ([]*x509.Certificate, crypto.Signer, error) {
switch kp.Format {
case "", "pem_file":
certData, err := os.ReadFile(kp.Certificate)
if err != nil {
return nil, nil, err
}
cert, err := pemDecodeSingleCert(certData)
chain, err := pemDecodeCertificateChain(certData)
if err != nil {
return nil, nil, err
}
Expand All @@ -93,11 +107,49 @@ func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
if err != nil {
return nil, nil, err
}
if err := verifyKeysMatch(chain[0], key); err != nil {
return nil, nil, err
}
}

return cert, key, nil
return chain, key, nil

default:
return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format)
}
}

// verifyKeysMatch verifies that the public key in the [x509.Certificate] matches
// the public key of the [crypto.Signer].
func verifyKeysMatch(crt *x509.Certificate, signer crypto.Signer) error {
switch pub := crt.PublicKey.(type) {
case *rsa.PublicKey:
pk, ok := signer.Public().(*rsa.PublicKey)
if !ok {
return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
}
if !pub.Equal(pk) {
return errors.New("private key does not match issuer public key")
}
case *ecdsa.PublicKey:
pk, ok := signer.Public().(*ecdsa.PublicKey)
if !ok {
return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
}
if !pub.Equal(pk) {
return errors.New("private key does not match issuer public key")
}
case ed25519.PublicKey:
pk, ok := signer.Public().(ed25519.PublicKey)
if !ok {
return fmt.Errorf("private key type %T does not match issuer public key type %T", signer.Public(), pub)
}
if !pub.Equal(pk) {
return errors.New("private key does not match issuer public key")
}
default:
return fmt.Errorf("unsupported key type: %T", pub)
}

return nil
}
Loading
Loading