Add authority.WithX509SignerFunc

This change adds a new authority option that allows to pass a callback
that returns the certificate chain and signer used to sign X.509
certificates.

This option will be used by Caddy, they renew the intermediate
certificate weekly and there's no other way to replace it without
re-creating the embedded CA.

Fixes #874
This commit is contained in:
Mariano Cano 2022-03-28 17:54:35 -07:00
parent 49de04661b
commit 955d4cf80d
4 changed files with 194 additions and 46 deletions

View File

@ -163,6 +163,22 @@ func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
}
}
// WithX509SignerFunc defines the function used to get the chain of certificates
// and signer used when we sign X.509 certificates.
func WithX509SignerFunc(fn func() ([]*x509.Certificate, crypto.Signer, error)) Option {
return func(a *Authority) error {
srv, err := cas.New(context.Background(), casapi.Options{
Type: casapi.SoftCAS,
CertificateSigner: fn,
})
if err != nil {
return err
}
a.x509CAService = srv
return nil
}
}
// WithSSHUserSigner defines the signer used to sign SSH user certificates.
func WithSSHUserSigner(s crypto.Signer) Option {
return func(a *Authority) error {

View File

@ -31,13 +31,18 @@ type Options struct {
// https://cloud.google.com/docs/authentication.
CredentialsFile string `json:"credentialsFile,omitempty"`
// Certificate and signer are the issuer certificate, along with any other
// bundled certificates to be returned in the chain for consumers, and
// CertificateChain and Signer are the issuer certificate, along with any
// other bundled certificates to be returned in the chain for consumers, and
// signer used in SoftCAS. They are configured in ca.json crt and key
// properties.
CertificateChain []*x509.Certificate `json:"-"`
Signer crypto.Signer `json:"-"`
// CertificateSigner combines CertificateChain and Signer in a callback that
// returns the chain of certificate and signer used to sign X.509
// certificates in SoftCAS.
CertificateSigner func() ([]*x509.Certificate, crypto.Signer, error) `json:"-"`
// IsCreator is set to true when we're creating a certificate authority. It
// is used to skip some validations when initializing a
// CertificateAuthority. This option is used on SoftCAS and CloudCAS.

View File

@ -24,9 +24,10 @@ var now = time.Now
// SoftCAS implements a Certificate Authority Service using Golang or KMS
// crypto. This is the default CAS used in step-ca.
type SoftCAS struct {
CertificateChain []*x509.Certificate
Signer crypto.Signer
KeyManager kms.KeyManager
CertificateChain []*x509.Certificate
Signer crypto.Signer
CertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)
KeyManager kms.KeyManager
}
// New creates a new CertificateAuthorityService implementation using Golang or KMS
@ -34,16 +35,17 @@ type SoftCAS struct {
func New(ctx context.Context, opts apiv1.Options) (*SoftCAS, error) {
if !opts.IsCreator {
switch {
case len(opts.CertificateChain) == 0:
case len(opts.CertificateChain) == 0 && opts.CertificateSigner == nil:
return nil, errors.New("softCAS 'CertificateChain' cannot be nil")
case opts.Signer == nil:
case opts.Signer == nil && opts.CertificateSigner == nil:
return nil, errors.New("softCAS 'signer' cannot be nil")
}
}
return &SoftCAS{
CertificateChain: opts.CertificateChain,
Signer: opts.Signer,
KeyManager: opts.KeyManager,
CertificateChain: opts.CertificateChain,
Signer: opts.Signer,
CertificateSigner: opts.CertificateSigner,
KeyManager: opts.KeyManager,
}, nil
}
@ -57,6 +59,7 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1
}
t := now()
// Provisioners can also set specific values.
if req.Template.NotBefore.IsZero() {
req.Template.NotBefore = t.Add(-1 * req.Backdate)
@ -64,16 +67,21 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1
if req.Template.NotAfter.IsZero() {
req.Template.NotAfter = t.Add(req.Lifetime)
}
req.Template.Issuer = c.CertificateChain[0].Subject
cert, err := createCertificate(req.Template, c.CertificateChain[0], req.Template.PublicKey, c.Signer)
chain, signer, err := c.getCertSigner()
if err != nil {
return nil, err
}
req.Template.Issuer = chain[0].Subject
cert, err := createCertificate(req.Template, chain[0], req.Template.PublicKey, signer)
if err != nil {
return nil, err
}
return &apiv1.CreateCertificateResponse{
Certificate: cert,
CertificateChain: c.CertificateChain,
CertificateChain: chain,
}, nil
}
@ -89,16 +97,21 @@ func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R
t := now()
req.Template.NotBefore = t.Add(-1 * req.Backdate)
req.Template.NotAfter = t.Add(req.Lifetime)
req.Template.Issuer = c.CertificateChain[0].Subject
cert, err := createCertificate(req.Template, c.CertificateChain[0], req.Template.PublicKey, c.Signer)
chain, signer, err := c.getCertSigner()
if err != nil {
return nil, err
}
req.Template.Issuer = chain[0].Subject
cert, err := createCertificate(req.Template, chain[0], req.Template.PublicKey, signer)
if err != nil {
return nil, err
}
return &apiv1.RenewCertificateResponse{
Certificate: cert,
CertificateChain: c.CertificateChain,
CertificateChain: chain,
}, nil
}
@ -106,9 +119,13 @@ func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R
// operation is a no-op as the actual revoke will happen when we store the entry
// in the db.
func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
chain, _, err := c.getCertSigner()
if err != nil {
return nil, err
}
return &apiv1.RevokeCertificateResponse{
Certificate: req.Certificate,
CertificateChain: c.CertificateChain,
CertificateChain: chain,
}, nil
}
@ -179,7 +196,7 @@ func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthori
}, nil
}
// initializeKeyManager initiazes the default key manager if was not given.
// initializeKeyManager initializes the default key manager if was not given.
func (c *SoftCAS) initializeKeyManager() (err error) {
if c.KeyManager == nil {
c.KeyManager, err = kms.New(context.Background(), kmsapi.Options{
@ -189,6 +206,15 @@ func (c *SoftCAS) initializeKeyManager() (err error) {
return
}
// getCertSigner returns the certificate chain and signer to use.
func (c *SoftCAS) getCertSigner() ([]*x509.Certificate, crypto.Signer, error) {
if c.CertificateSigner != nil {
return c.CertificateSigner()
}
return c.CertificateChain, c.Signer, nil
}
// createKey uses the configured kms to create a key.
func (c *SoftCAS) createKey(req *kmsapi.CreateKeyRequest) (*kmsapi.CreateKeyResponse, error) {
if err := c.initializeKeyManager(); err != nil {

View File

@ -73,6 +73,12 @@ var (
testSignedTemplate = mustSign(testTemplate, testIssuer, testNow, testNow.Add(24*time.Hour))
testSignedRootTemplate = mustSign(testRootTemplate, testRootTemplate, testNow, testNow.Add(24*time.Hour))
testSignedIntermediateTemplate = mustSign(testIntermediateTemplate, testSignedRootTemplate, testNow, testNow.Add(24*time.Hour))
testCertificateSigner = func() ([]*x509.Certificate, crypto.Signer, error) {
return []*x509.Certificate{testIssuer}, testSigner, nil
}
testFailCertificateSigner = func() ([]*x509.Certificate, crypto.Signer, error) {
return nil, nil, errTest
}
)
type signatureAlgorithmSigner struct {
@ -186,6 +192,10 @@ func setTeeReader(t *testing.T, w *bytes.Buffer) {
}
func TestNew(t *testing.T) {
assertEqual := func(x, y interface{}) bool {
return reflect.DeepEqual(x, y) || fmt.Sprintf("%#v", x) == fmt.Sprintf("%#v", y)
}
type args struct {
ctx context.Context
opts apiv1.Options
@ -197,6 +207,7 @@ func TestNew(t *testing.T) {
wantErr bool
}{
{"ok", args{context.Background(), apiv1.Options{CertificateChain: []*x509.Certificate{testIssuer}, Signer: testSigner}}, &SoftCAS{CertificateChain: []*x509.Certificate{testIssuer}, Signer: testSigner}, false},
{"ok with callback", args{context.Background(), apiv1.Options{CertificateSigner: testCertificateSigner}}, &SoftCAS{CertificateSigner: testCertificateSigner}, false},
{"fail no issuer", args{context.Background(), apiv1.Options{Signer: testSigner}}, nil, true},
{"fail no signer", args{context.Background(), apiv1.Options{CertificateChain: []*x509.Certificate{testIssuer}}}, nil, true},
}
@ -207,7 +218,7 @@ func TestNew(t *testing.T) {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
if !assertEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
@ -265,8 +276,9 @@ func TestSoftCAS_CreateCertificate(t *testing.T) {
}
type fields struct {
Issuer *x509.Certificate
Signer crypto.Signer
Issuer *x509.Certificate
Signer crypto.Signer
CertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)
}
type args struct {
req *apiv1.CreateCertificateRequest
@ -278,43 +290,53 @@ func TestSoftCAS_CreateCertificate(t *testing.T) {
want *apiv1.CreateCertificateResponse
wantErr bool
}{
{"ok", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
{"ok", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok signature algorithm", fields{testIssuer, saSigner}, args{&apiv1.CreateCertificateRequest{
{"ok signature algorithm", fields{testIssuer, saSigner, nil}, args{&apiv1.CreateCertificateRequest{
Template: &saTemplate, Lifetime: 24 * time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok with notBefore", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
{"ok with notBefore", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{
Template: &tmplNotBefore, Lifetime: 24 * time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok with notBefore+notAfter", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
{"ok with notBefore+notAfter", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{
Template: &tmplWithLifetime, Lifetime: 24 * time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"fail template", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},
{"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{Template: testTemplate}}, nil, true},
{"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
{"ok with callback", fields{nil, nil, testCertificateSigner}, args{&apiv1.CreateCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"fail template", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},
{"fail lifetime", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{Template: testTemplate}}, nil, true},
{"fail CreateCertificate", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{
Template: &tmplNoSerial,
Lifetime: 24 * time.Hour,
}}, nil, true},
{"fail with callback", fields{nil, nil, testFailCertificateSigner}, args{&apiv1.CreateCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &SoftCAS{
CertificateChain: []*x509.Certificate{tt.fields.Issuer},
Signer: tt.fields.Signer,
CertificateChain: []*x509.Certificate{tt.fields.Issuer},
Signer: tt.fields.Signer,
CertificateSigner: tt.fields.CertificateSigner,
}
got, err := c.CreateCertificate(tt.args.req)
if (err != nil) != tt.wantErr {
@ -345,8 +367,9 @@ func TestSoftCAS_RenewCertificate(t *testing.T) {
}
type fields struct {
Issuer *x509.Certificate
Signer crypto.Signer
Issuer *x509.Certificate
Signer crypto.Signer
CertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)
}
type args struct {
req *apiv1.RenewCertificateRequest
@ -358,30 +381,40 @@ func TestSoftCAS_RenewCertificate(t *testing.T) {
want *apiv1.RenewCertificateResponse
wantErr bool
}{
{"ok", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{
{"ok", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, &apiv1.RenewCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok signature algorithm", fields{testIssuer, saSigner}, args{&apiv1.RenewCertificateRequest{
{"ok signature algorithm", fields{testIssuer, saSigner, nil}, args{&apiv1.RenewCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, &apiv1.RenewCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"fail template", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},
{"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Template: testTemplate}}, nil, true},
{"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{
{"ok with callback", fields{nil, nil, testCertificateSigner}, args{&apiv1.RenewCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, &apiv1.RenewCertificateResponse{
Certificate: testSignedTemplate,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"fail template", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},
{"fail lifetime", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{Template: testTemplate}}, nil, true},
{"fail CreateCertificate", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{
Template: &tmplNoSerial,
Lifetime: 24 * time.Hour,
}}, nil, true},
{"fail with callback", fields{nil, nil, testFailCertificateSigner}, args{&apiv1.RenewCertificateRequest{
Template: testTemplate, Lifetime: 24 * time.Hour,
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &SoftCAS{
CertificateChain: []*x509.Certificate{tt.fields.Issuer},
Signer: tt.fields.Signer,
CertificateChain: []*x509.Certificate{tt.fields.Issuer},
Signer: tt.fields.Signer,
CertificateSigner: tt.fields.CertificateSigner,
}
got, err := c.RenewCertificate(tt.args.req)
if (err != nil) != tt.wantErr {
@ -397,8 +430,9 @@ func TestSoftCAS_RenewCertificate(t *testing.T) {
func TestSoftCAS_RevokeCertificate(t *testing.T) {
type fields struct {
Issuer *x509.Certificate
Signer crypto.Signer
Issuer *x509.Certificate
Signer crypto.Signer
CertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)
}
type args struct {
req *apiv1.RevokeCertificateRequest
@ -410,7 +444,7 @@ func TestSoftCAS_RevokeCertificate(t *testing.T) {
want *apiv1.RevokeCertificateResponse
wantErr bool
}{
{"ok", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{
{"ok", fields{testIssuer, testSigner, nil}, args{&apiv1.RevokeCertificateRequest{
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
Reason: "test reason",
ReasonCode: 1,
@ -418,23 +452,37 @@ func TestSoftCAS_RevokeCertificate(t *testing.T) {
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok no cert", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{
{"ok no cert", fields{testIssuer, testSigner, nil}, args{&apiv1.RevokeCertificateRequest{
Reason: "test reason",
ReasonCode: 1,
}}, &apiv1.RevokeCertificateResponse{
Certificate: nil,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok empty", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{}}, &apiv1.RevokeCertificateResponse{
{"ok empty", fields{testIssuer, testSigner, nil}, args{&apiv1.RevokeCertificateRequest{}}, &apiv1.RevokeCertificateResponse{
Certificate: nil,
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"ok with callback", fields{nil, nil, testCertificateSigner}, args{&apiv1.RevokeCertificateRequest{
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
Reason: "test reason",
ReasonCode: 1,
}}, &apiv1.RevokeCertificateResponse{
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
CertificateChain: []*x509.Certificate{testIssuer},
}, false},
{"fail with callback", fields{nil, nil, testFailCertificateSigner}, args{&apiv1.RevokeCertificateRequest{
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
Reason: "test reason",
ReasonCode: 1,
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &SoftCAS{
CertificateChain: []*x509.Certificate{tt.fields.Issuer},
Signer: tt.fields.Signer,
CertificateChain: []*x509.Certificate{tt.fields.Issuer},
Signer: tt.fields.Signer,
CertificateSigner: tt.fields.CertificateSigner,
}
got, err := c.RevokeCertificate(tt.args.req)
if (err != nil) != tt.wantErr {
@ -609,3 +657,56 @@ func TestSoftCAS_CreateCertificateAuthority(t *testing.T) {
})
}
}
func TestSoftCAS_defaultKeyManager(t *testing.T) {
mockNow(t)
type args struct {
req *apiv1.CreateCertificateAuthorityRequest
}
tests := []struct {
name string
args args
wantErr bool
}{
{"ok root", args{&apiv1.CreateCertificateAuthorityRequest{
Type: apiv1.RootCA,
Template: &x509.Certificate{
Subject: pkix.Name{CommonName: "Test Root CA"},
KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
SerialNumber: big.NewInt(1234),
},
Lifetime: 24 * time.Hour,
}}, false},
{"ok intermediate", args{&apiv1.CreateCertificateAuthorityRequest{
Type: apiv1.IntermediateCA,
Template: testIntermediateTemplate,
Lifetime: 24 * time.Hour,
Parent: &apiv1.CreateCertificateAuthorityResponse{
Certificate: testSignedRootTemplate,
Signer: testSigner,
},
}}, false},
{"fail with default key manager", args{&apiv1.CreateCertificateAuthorityRequest{
Type: apiv1.IntermediateCA,
Template: testIntermediateTemplate,
Lifetime: 24 * time.Hour,
Parent: &apiv1.CreateCertificateAuthorityResponse{
Certificate: testSignedRootTemplate,
Signer: &badSigner{},
},
}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &SoftCAS{}
_, err := c.CreateCertificateAuthority(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("SoftCAS.CreateCertificateAuthority() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}