diff --git a/authority/options.go b/authority/options.go index a1238b1d..1c154577 100644 --- a/authority/options.go +++ b/authority/options.go @@ -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 { diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index badad7fc..50c3a2be 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -31,12 +31,20 @@ 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 - // signer used in SoftCAS. They are configured in ca.json crt and key - // properties. + // CertificateChain contains the issuer certificate, along with any other + // bundled certificates to be returned in the chain for consumers. It is + // used used in SoftCAS, and is configured in the crt property of the + // ca.json. CertificateChain []*x509.Certificate `json:"-"` - Signer crypto.Signer `json:"-"` + + // Signer is the private key or a KMS signer for the issuer certificate. It is used in + // SoftCAS and it is configured in the key property of the ca.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 diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 8e67d016..2a97145b 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -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 { diff --git a/cas/softcas/softcas_test.go b/cas/softcas/softcas_test.go index 7d3add4f..b4f5b440 100644 --- a/cas/softcas/softcas_test.go +++ b/cas/softcas/softcas_test.go @@ -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 + } + }) + } +}