From aad8f9e5827849203c86061afc88680f727fe3d6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Sep 2020 19:09:46 -0700 Subject: [PATCH] Pass issuer and signer to softCAS options. Remove commented code and initialize CAS properly. Minor fixes in CloudCAS. --- authority/authority.go | 64 ++++++++++++++++++++++----------- authority/config.go | 2 +- authority/tls.go | 67 ++++++++++++++++++---------------- cas/apiv1/extension.go | 5 +++ cas/apiv1/options.go | 28 ++++++++++----- cas/apiv1/registry.go | 8 +++-- cas/apiv1/requests.go | 5 --- cas/apiv1/services.go | 23 ++++-------- cas/cloudcas/cloudcas.go | 12 ++++--- cas/softcas/softcas.go | 78 +++++++++++++++++++++++++++++++++------- 10 files changed, 191 insertions(+), 101 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index d7a450d4..0fc7a71a 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -148,18 +148,6 @@ func (a *Authority) init() error { } } - // Initialize the X.509 CA Service if it has not been set in the options - if a.x509CAService == nil { - var options casapi.Options - if a.config.CAS != nil { - options = *a.config.CAS - } - a.x509CAService, err = cas.New(context.Background(), options) - if err != nil { - return nil - } - } - // Initialize step-ca Database if it's not already initialized with WithDB. // If a.config.DB is nil then a simple, barebones in memory DB will be used. if a.db == nil { @@ -206,15 +194,51 @@ func (a *Authority) init() error { if err != nil { return err } - signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), - }) - if err != nil { - return err - } - a.x509Signer = signer a.x509Issuer = crt + + // Read signer only is the CAS is the default one. + if a.config.CAS.HasType(casapi.SoftCAS) { + signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: []byte(a.config.Password), + }) + if err != nil { + return err + } + a.x509Signer = signer + } + } + + // Initialize the X.509 CA Service if it has not been set in the options + if a.x509CAService == nil { + var options casapi.Options + if a.config.CAS != nil { + options = *a.config.CAS + } + + // Set issuer and signer for default CAS. + if options.HasType(casapi.SoftCAS) { + crt, err := pemutil.ReadCertificate(a.config.IntermediateCert) + if err != nil { + return err + } + + signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: []byte(a.config.Password), + }) + if err != nil { + return err + } + + options.Issuer = crt + options.Signer = signer + } + + a.x509CAService, err = cas.New(context.Background(), options) + if err != nil { + return nil + } } // Decrypt and load SSH keys diff --git a/authority/config.go b/authority/config.go index 1f8374f8..168b360d 100644 --- a/authority/config.go +++ b/authority/config.go @@ -187,7 +187,7 @@ func (c *Config) Validate() error { case c.IntermediateCert == "": return errors.New("crt cannot be empty") - case c.IntermediateKey == "": + case c.IntermediateKey == "" && c.CAS.HasType(cas.SoftCAS): return errors.New("key cannot be empty") case len(c.DNSNames) == 0: diff --git a/authority/tls.go b/authority/tls.go index cc290839..dfb9b583 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -145,32 +145,24 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } - lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(-1 * signOpts.Backdate)) + lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ Template: leaf, - Issuer: a.x509Issuer, - Signer: a.x509Signer, Lifetime: lifetime, + Backdate: signOpts.Backdate, }) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...) } - serverCert := resp.Certificate - // serverCert, err := x509util.CreateCertificate(leaf, a.x509Issuer, csr.PublicKey, a.x509Signer) - // if err != nil { - // return nil, errs.Wrap(http.StatusInternalServerError, err, - // "authority.Sign; error creating certificate", opts...) - // } - - if err = a.db.StoreCertificate(serverCert); err != nil { + if err = a.db.StoreCertificate(resp.Certificate); err != nil { if err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error storing certificate in db", opts...) } } - return []*x509.Certificate{serverCert, a.x509Issuer}, nil + return append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...), nil } // Renew creates a new Certificate identical to the old certificate, except @@ -200,13 +192,12 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // Durations backdate := a.config.AuthorityConfig.Backdate.Duration duration := oldCert.NotAfter.Sub(oldCert.NotBefore) - now := time.Now().UTC() + lifetime := duration - backdate + // Create new certificate from previous values. + // Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS. newCert := &x509.Certificate{ - Issuer: a.x509Issuer.Subject, Subject: oldCert.Subject, - NotBefore: now.Add(-1 * backdate), - NotAfter: now.Add(duration - backdate), KeyUsage: oldCert.KeyUsage, UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions, ExtKeyUsage: oldCert.ExtKeyUsage, @@ -241,10 +232,14 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 } // Copy all extensions except: - // 1. Authority Key Identifier - This one might be different if we rotate the intermediate certificate - // and it will cause a TLS bad certificate error. - // 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier extension will be calculated - // for the new public key by NewLeafProfilewithTemplate() + // + // 1. Authority Key Identifier - This one might be different if we rotate + // the intermediate certificate and it will cause a TLS bad certificate + // error. + // + // 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier + // extension will be calculated for the new public key by + // x509util.CreateCertificate() for _, ext := range oldCert.Extensions { if ext.Id.Equal(oidAuthorityKeyIdentifier) { continue @@ -256,18 +251,22 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext) } - serverCert, err := x509util.CreateCertificate(newCert, a.x509Issuer, newCert.PublicKey, a.x509Signer) + resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{ + Template: newCert, + Lifetime: lifetime, + Backdate: backdate, + }) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...) } - if err = a.db.StoreCertificate(serverCert); err != nil { + if err = a.db.StoreCertificate(resp.Certificate); err != nil { if err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey; error storing certificate in db", opts...) } } - return []*x509.Certificate{serverCert, a.x509Issuer}, nil + return append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...), nil } // RevokeOptions are the options for the Revoke API. @@ -403,30 +402,36 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { certTpl.NotBefore = now.Add(-1 * time.Minute) certTpl.NotAfter = now.Add(24 * time.Hour) - cert, err := x509util.CreateCertificate(certTpl, a.x509Issuer, cr.PublicKey, a.x509Signer) + resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ + Template: certTpl, + Lifetime: 24 * time.Hour, + Backdate: 1 * time.Minute, + }) if err != nil { return fatal(err) } // Generate PEM blocks to create tls.Certificate - crtPEM := pem.EncodeToMemory(&pem.Block{ + pemBlocks := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", - Bytes: cert.Raw, + Bytes: resp.Certificate.Raw, }) - intermediatePEM, err := pemutil.Serialize(a.x509Issuer) - if err != nil { - return fatal(err) + for _, crt := range resp.CertificateChain { + pemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + })...) } keyPEM, err := pemutil.Serialize(priv) if err != nil { return fatal(err) } - tlsCrt, err := tls.X509KeyPair(append(crtPEM, pem.EncodeToMemory(intermediatePEM)...), pem.EncodeToMemory(keyPEM)) + tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM)) if err != nil { return fatal(err) } // Set leaf certificate - tlsCrt.Leaf = cert + tlsCrt.Leaf = resp.Certificate return &tlsCrt, nil } diff --git a/cas/apiv1/extension.go b/cas/apiv1/extension.go index 66da15a0..de341fbb 100644 --- a/cas/apiv1/extension.go +++ b/cas/apiv1/extension.go @@ -8,6 +8,11 @@ import ( "github.com/pkg/errors" ) +var ( + oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} + oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...) +) + // CertificateAuthorityExtension is type used to encode the certificate // authority extension. type CertificateAuthorityExtension struct { diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 7118472e..05b83db2 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -1,7 +1,8 @@ package apiv1 import ( - "strings" + "crypto" + "crypto/x509" "github.com/pkg/errors" ) @@ -14,27 +15,36 @@ type Options struct { // Path to the credentials file used in CloudCAS CredentialsFile string `json:"credentialsFile"` + + // CertificateAuthority reference. In CloudCAS the format is + // `projects/*/locations/*/certificateAuthorities/*`. + Certificateauthority string `json:"certificateAuthority"` + + // Issuer and signer are the issuer certificate and signer used in SoftCAS. + // They are configured in ca.json crt and key properties. + Issuer *x509.Certificate `json:"-"` + Signer crypto.Signer `json:"-"` } // Validate checks the fields in Options. func (o *Options) Validate() error { + var typ Type if o == nil { - return nil + typ = Type(SoftCAS) + } else { + typ = Type(o.Type) } - - switch Type(strings.ToLower(o.Type)) { - case DefaultCAS, SoftCAS, CloudCAS: - default: - return errors.Errorf("unsupported kms type %s", o.Type) + // Check that the type can be loaded. + if _, ok := LoadCertificateAuthorityServiceNewFunc(typ); !ok { + return errors.Errorf("unsupported cas type %s", typ) } - return nil } // HasType returns if the options have the given type. func (o *Options) HasType(t Type) bool { if o == nil { - return SoftCAS == t.String() + return t.String() == SoftCAS } return Type(o.Type).String() == t.String() } diff --git a/cas/apiv1/registry.go b/cas/apiv1/registry.go index 9c4c96ee..b74103b7 100644 --- a/cas/apiv1/registry.go +++ b/cas/apiv1/registry.go @@ -5,7 +5,9 @@ import ( "sync" ) -var registry = new(sync.Map) +var ( + registry = new(sync.Map) +) // CertificateAuthorityServiceNewFunc is the type that represents the method to initialize a new // CertificateAuthorityService. @@ -13,12 +15,12 @@ type CertificateAuthorityServiceNewFunc func(ctx context.Context, opts Options) // Register adds to the registry a method to create a KeyManager of type t. func Register(t Type, fn CertificateAuthorityServiceNewFunc) { - registry.Store(t, fn) + registry.Store(t.String(), fn) } // LoadCertificateAuthorityServiceNewFunc returns the function initialize a KayManager. func LoadCertificateAuthorityServiceNewFunc(t Type) (CertificateAuthorityServiceNewFunc, bool) { - v, ok := registry.Load(t) + v, ok := registry.Load(t.String()) if !ok { return nil, false } diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index bc6770be..9527e16c 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -1,15 +1,12 @@ package apiv1 import ( - "crypto" "crypto/x509" "time" ) type CreateCertificateRequest struct { Template *x509.Certificate - Issuer *x509.Certificate - Signer crypto.Signer Lifetime time.Duration Backdate time.Duration RequestID string @@ -21,8 +18,6 @@ type CreateCertificateResponse struct { type RenewCertificateRequest struct { Template *x509.Certificate - Issuer *x509.Certificate - Signer crypto.Signer Lifetime time.Duration Backdate time.Duration RequestID string diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index f3bc6b16..5c6de1c3 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -1,15 +1,9 @@ package apiv1 import ( - "encoding/asn1" "strings" ) -var ( - oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} - oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...) -) - // CertificateAuthorityService is the interface implemented to support external // certificate authorities. type CertificateAuthorityService interface { @@ -18,27 +12,24 @@ type CertificateAuthorityService interface { RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } -// Type represents the KMS type used. +// Type represents the CAS type used. type Type string const ( // DefaultCAS is a CertificateAuthorityService using software. DefaultCAS = "" // SoftCAS is a CertificateAuthorityService using software. - SoftCAS = "SoftCAS" + SoftCAS = "softcas" // CloudCAS is a CertificateAuthorityService using Google Cloud CAS. - CloudCAS = "CloudCAS" + CloudCAS = "cloudcas" ) -// String returns the given type as a string. All the letters will be lowercase. +// String returns a string from the type. It will always return the lower case +// version of the Type, as we need a standard type to compare and use as the +// registry key. func (t Type) String() string { if t == "" { return SoftCAS } - for _, s := range []string{SoftCAS, CloudCAS} { - if strings.EqualFold(s, string(t)) { - return s - } - } - return string(t) + return strings.ToLower(string(t)) } diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 52608658..0a7a064d 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -45,6 +45,10 @@ type caClient interface{} // New creates a new CertificateAuthorityService implementation using Google // Cloud CAS. func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { + if opts.Certificateauthority == "" { + return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty") + } + var cloudOpts []option.ClientOption if opts.CredentialsFile != "" { cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) @@ -57,7 +61,7 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { return &CloudCAS{ client: client, - certificateAuthority: "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/Smallstep-Test-Intermediate-CA", + certificateAuthority: opts.Certificateauthority, }, nil } @@ -87,9 +91,9 @@ func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { switch { case req.Template == nil: - return nil, errors.New("renewCertificate `template` cannot be nil") + return nil, errors.New("renewCertificateRequest `template` cannot be nil") case req.Lifetime == 0: - return nil, errors.New("renewCertificate `lifetime` cannot be 0") + return nil, errors.New("renewCertificateRequest `lifetime` cannot be 0") } cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID) @@ -106,7 +110,7 @@ func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. // RevokeCertificate a certificate using Google Cloud CAS. func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { if req.Certificate == nil { - return nil, errors.New("revokeCertificate `certificate` cannot be nil") + return nil, errors.New("revokeCertificateRequest `certificate` cannot be nil") } ext, ok := apiv1.FindCertificateAuthorityExtension(req.Certificate) diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index b7e5ddfc..751913a0 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -2,8 +2,11 @@ package softcas import ( "context" + "crypto" "crypto/x509" + "errors" "fmt" + "time" "github.com/smallstep/certificates/cas/apiv1" "go.step.sm/crypto/x509util" @@ -15,19 +18,47 @@ func init() { }) } -// SoftCAS implements a Certificate Authority Service using Golang crypto. -// This is the default CAS used in step-ca. -type SoftCAS struct{} - -// New creates a new CertificateAuthorityService implementation using Golang -// crypto. -func New(ctx context.Context, opts apiv1.Options) (*SoftCAS, error) { - return &SoftCAS{}, nil +var now = func() time.Time { + return time.Now() } -// CreateCertificate signs a new certificate using Golang crypto. +// SoftCAS implements a Certificate Authority Service using Golang or KMS +// crypto. This is the default CAS used in step-ca. +type SoftCAS struct { + Issuer *x509.Certificate + Signer crypto.Signer +} + +// New creates a new CertificateAuthorityService implementation using Golang or KMS +// crypto. +func New(ctx context.Context, opts apiv1.Options) (*SoftCAS, error) { + switch { + case opts.Issuer == nil: + return nil, errors.New("softCAS 'issuer' cannot be nil") + case opts.Signer == nil: + return nil, errors.New("softCAS 'signer' cannot be nil") + } + return &SoftCAS{ + Issuer: opts.Issuer, + Signer: opts.Signer, + }, nil +} + +// CreateCertificate signs a new certificate using Golang or KMS crypto. func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { - cert, err := x509util.CreateCertificate(req.Template, req.Issuer, req.Template.PublicKey, req.Signer) + switch { + case req.Template == nil: + return nil, errors.New("createCertificateRequest `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") + } + + t := now() + req.Template.NotBefore = t.Add(-1 * req.Backdate) + req.Template.NotAfter = t.Add(req.Lifetime) + req.Template.Issuer = c.Issuer.Subject + + cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer) if err != nil { return nil, err } @@ -35,13 +66,36 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 return &apiv1.CreateCertificateResponse{ Certificate: cert, CertificateChain: []*x509.Certificate{ - req.Issuer, + c.Issuer, }, }, nil } +// RenewCertificate signs the given certificate template using Golang or KMS crypto. func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { - return nil, fmt.Errorf("not implemented") + switch { + case req.Template == nil: + return nil, errors.New("createCertificateRequest `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") + } + + t := now() + req.Template.NotBefore = t.Add(-1 * req.Backdate) + req.Template.NotAfter = t.Add(req.Lifetime) + req.Template.Issuer = c.Issuer.Subject + + cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer) + if err != nil { + return nil, err + } + + return &apiv1.RenewCertificateResponse{ + Certificate: cert, + CertificateChain: []*x509.Certificate{ + c.Issuer, + }, + }, nil } // RevokeCertificate revokes the given certificate in step-ca.