diff --git a/authority/authority.go b/authority/authority.go index 25b40350..0730fe5e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -12,10 +12,11 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/kms" + kmsapi "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/pemutil" - "github.com/smallstep/cli/crypto/x509util" "golang.org/x/crypto/ssh" ) @@ -25,21 +26,30 @@ const ( // Authority implements the Certificate Authority internal interface. type Authority struct { - config *Config - rootX509Certs []*x509.Certificate - intermediateIdentity *x509util.Identity + config *Config + keyManager kms.KeyManager + provisioners *provisioner.Collection + db db.AuthDB + + // X509 CA + rootX509Certs []*x509.Certificate + federatedX509Certs []*x509.Certificate + x509Signer crypto.Signer + x509Issuer *x509.Certificate + certificates *sync.Map + + // SSH CA sshCAUserCertSignKey ssh.Signer sshCAHostCertSignKey ssh.Signer sshCAUserCerts []ssh.PublicKey sshCAHostCerts []ssh.PublicKey sshCAUserFederatedCerts []ssh.PublicKey sshCAHostFederatedCerts []ssh.PublicKey - certificates *sync.Map - startTime time.Time - provisioners *provisioner.Collection - db db.AuthDB + // Do not re-initialize - initOnce bool + initOnce bool + startTime time.Time + // Custom functions sshBastionFunc func(user, hostname string) (*Bastion, error) sshCheckHostFunc func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error) @@ -59,12 +69,19 @@ func New(config *Config, opts ...Option) (*Authority, error) { certificates: new(sync.Map), provisioners: provisioner.NewCollection(config.getAudiences()), } - for _, opt := range opts { - opt(a) + + // Apply options. + for _, fn := range opts { + if err := fn(a); err != nil { + return nil, err + } } + + // Initialize authority from options or configuration. if err := a.init(); err != nil { return nil, err } + return a, nil } @@ -76,6 +93,19 @@ func (a *Authority) init() error { } var err error + + // Initialize key manager if it has not been set in the options. + if a.keyManager == nil { + var options kmsapi.Options + if a.config.KMS != nil { + options = *a.config.KMS + } + a.keyManager, err = kms.New(context.Background(), options) + if err != nil { + return err + } + } + // 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 { @@ -84,50 +114,62 @@ func (a *Authority) init() error { } } - // Load the root certificates and add them to the certificate store - a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root)) - for i, path := range a.config.Root { - crt, err := pemutil.ReadCertificate(path) - if err != nil { - return err + // Read root certificates and store them in the certificates map. + if len(a.rootX509Certs) == 0 { + a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root)) + for i, path := range a.config.Root { + crt, err := pemutil.ReadCertificate(path) + if err != nil { + return err + } + a.rootX509Certs[i] = crt } - // Add root certificate to the certificate map + } + for _, crt := range a.rootX509Certs { sum := sha256.Sum256(crt.Raw) a.certificates.Store(hex.EncodeToString(sum[:]), crt) - a.rootX509Certs[i] = crt } - // Add federated roots - for _, path := range a.config.FederatedRoots { - crt, err := pemutil.ReadCertificate(path) - if err != nil { - return err + // Read federated certificates and store them in the certificates map. + if len(a.federatedX509Certs) == 0 { + a.federatedX509Certs = make([]*x509.Certificate, len(a.config.FederatedRoots)) + for i, path := range a.config.FederatedRoots { + crt, err := pemutil.ReadCertificate(path) + if err != nil { + return err + } + a.federatedX509Certs[i] = crt } + } + for _, crt := range a.federatedX509Certs { sum := sha256.Sum256(crt.Raw) a.certificates.Store(hex.EncodeToString(sum[:]), crt) } - // Decrypt and load intermediate public / private key pair. - if len(a.config.Password) > 0 { - a.intermediateIdentity, err = x509util.LoadIdentityFromDisk( - a.config.IntermediateCert, - a.config.IntermediateKey, - pemutil.WithPassword([]byte(a.config.Password)), - ) + // Read intermediate and create X509 signer. + if a.x509Signer == nil { + crt, err := pemutil.ReadCertificate(a.config.IntermediateCert) if err != nil { return err } - } else { - a.intermediateIdentity, err = x509util.LoadIdentityFromDisk(a.config.IntermediateCert, a.config.IntermediateKey) + 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 } // Decrypt and load SSH keys if a.config.SSH != nil { if a.config.SSH.HostKey != "" { - signer, err := parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) + signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.SSH.HostKey, + Password: []byte(a.config.Password), + }) if err != nil { return err } @@ -140,7 +182,10 @@ func (a *Authority) init() error { a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, a.sshCAHostCertSignKey.PublicKey()) } if a.config.SSH.UserKey != "" { - signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) + signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.SSH.UserKey, + Password: []byte(a.config.Password), + }) if err != nil { return err } @@ -245,19 +290,3 @@ func (a *Authority) GetDatabase() db.AuthDB { func (a *Authority) Shutdown() error { return a.db.Shutdown() } - -func parseCryptoSigner(filename, password string) (crypto.Signer, error) { - var opts []pemutil.Options - if password != "" { - opts = append(opts, pemutil.WithPassword([]byte(password))) - } - key, err := pemutil.Read(filename, opts...) - if err != nil { - return nil, err - } - signer, ok := key.(crypto.Signer) - if !ok { - return nil, errors.Errorf("key %s of type %T cannot be used for signing operations", filename, key) - } - return signer, nil -} diff --git a/authority/authority_test.go b/authority/authority_test.go index e6a65453..058a4c25 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -137,7 +137,8 @@ func TestAuthorityNew(t *testing.T) { assert.Equals(t, auth.rootX509Certs[0], root) assert.True(t, auth.initOnce) - assert.NotNil(t, auth.intermediateIdentity) + assert.NotNil(t, auth.x509Signer) + assert.NotNil(t, auth.x509Issuer) for _, p := range tc.config.AuthorityConfig.Provisioners { var _p provisioner.Interface _p, ok = auth.provisioners.Load(p.GetID()) diff --git a/authority/config.go b/authority/config.go index 75f55a12..ceb2ea89 100644 --- a/authority/config.go +++ b/authority/config.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + kms "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" @@ -54,6 +55,7 @@ type Config struct { IntermediateKey string `json:"key"` Address string `json:"address"` DNSNames []string `json:"dnsNames"` + KMS *kms.Options `json:"kms,omitempty"` SSH *SSHConfig `json:"ssh,omitempty"` Logger json.RawMessage `json:"logger,omitempty"` DB *db.Config `json:"db,omitempty"` @@ -179,6 +181,11 @@ func (c *Config) Validate() error { c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation } + // Validate KMS options, nil is ok. + if err := c.KMS.Validate(); err != nil { + return err + } + // Validate ssh: nil is ok if err := c.SSH.Validate(); err != nil { return err diff --git a/authority/options.go b/authority/options.go index 10f0ec1a..2d655a2b 100644 --- a/authority/options.go +++ b/authority/options.go @@ -2,45 +2,54 @@ package authority import ( "context" + "crypto" "crypto/x509" + "encoding/pem" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/kms" "github.com/smallstep/certificates/sshutil" + "golang.org/x/crypto/ssh" ) // Option sets options to the Authority. -type Option func(*Authority) +type Option func(*Authority) error // WithDatabase sets an already initialized authority database to a new // authority. This option is intended to be use on graceful reloads. func WithDatabase(db db.AuthDB) Option { - return func(a *Authority) { + return func(a *Authority) error { a.db = db + return nil } } // WithGetIdentityFunc sets a custom function to retrieve the identity from // an external resource. func WithGetIdentityFunc(fn func(p provisioner.Interface, email string) (*provisioner.Identity, error)) Option { - return func(a *Authority) { + return func(a *Authority) error { a.getIdentityFunc = fn + return nil } } // WithSSHBastionFunc sets a custom function to get the bastion for a // given user-host pair. func WithSSHBastionFunc(fn func(user, host string) (*Bastion, error)) Option { - return func(a *Authority) { + return func(a *Authority) error { a.sshBastionFunc = fn + return nil } } // WithSSHGetHosts sets a custom function to get the bastion for a // given user-host pair. func WithSSHGetHosts(fn func(cert *x509.Certificate) ([]sshutil.Host, error)) Option { - return func(a *Authority) { + return func(a *Authority) error { a.sshGetHostsFunc = fn + return nil } } @@ -48,7 +57,127 @@ func WithSSHGetHosts(fn func(cert *x509.Certificate) ([]sshutil.Host, error)) Op // step ssh enabled. The token is used to validate the request, while the roots // are used to validate the token. func WithSSHCheckHost(fn func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error)) Option { - return func(a *Authority) { + return func(a *Authority) error { a.sshCheckHostFunc = fn + return nil } } + +// WithKeyManager defines the key manager used to get and create keys, and sign +// certificates. +func WithKeyManager(k kms.KeyManager) Option { + return func(a *Authority) error { + a.keyManager = k + return nil + } +} + +// WithX509Signer defines the signer used to sign X509 certificates. +func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option { + return func(a *Authority) error { + a.x509Issuer = crt + a.x509Signer = s + return nil + } +} + +// WithSSHUserSigner defines the signer used to sign SSH user certificates. +func WithSSHUserSigner(s crypto.Signer) Option { + return func(a *Authority) error { + signer, err := ssh.NewSignerFromSigner(s) + if err != nil { + return errors.Wrap(err, "error creating ssh user signer") + } + a.sshCAUserCertSignKey = signer + // Append public key to list of user certs + pub := signer.PublicKey() + a.sshCAUserCerts = append(a.sshCAUserCerts, pub) + a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, pub) + return nil + } +} + +// WithSSHHostSigner defines the signer used to sign SSH host certificates. +func WithSSHHostSigner(s crypto.Signer) Option { + return func(a *Authority) error { + signer, err := ssh.NewSignerFromSigner(s) + if err != nil { + return errors.Wrap(err, "error creating ssh host signer") + } + a.sshCAHostCertSignKey = signer + // Append public key to list of host certs + pub := signer.PublicKey() + a.sshCAHostCerts = append(a.sshCAHostCerts, pub) + a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, pub) + return nil + } +} + +// WithX509RootCerts is an option that allows to define the list of root +// certificates to use. This option will replace any root certificate defined +// before. +func WithX509RootCerts(rootCerts ...*x509.Certificate) Option { + return func(a *Authority) error { + a.rootX509Certs = rootCerts + return nil + } +} + +// WithX509FederatedCerts is an option that allows to define the list of +// federated certificates. This option will replace any federated certificate +// defined before. +func WithX509FederatedCerts(certs ...*x509.Certificate) Option { + return func(a *Authority) error { + a.federatedX509Certs = certs + return nil + } +} + +// WithX509RootBundle is an option that allows to define the list of root +// certificates. This option will replace any root certificate defined before. +func WithX509RootBundle(pemCerts []byte) Option { + return func(a *Authority) error { + certs, err := readCertificateBundle(pemCerts) + if err != nil { + return err + } + a.rootX509Certs = certs + return nil + } +} + +// WithX509FederatedBundle is an option that allows to define the list of +// federated certificates. This option will replace any federated certificate +// defined before. +func WithX509FederatedBundle(pemCerts []byte) Option { + return func(a *Authority) error { + certs, err := readCertificateBundle(pemCerts) + if err != nil { + return err + } + a.federatedX509Certs = certs + return nil + } +} + +func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) { + var block *pem.Block + var certs []*x509.Certificate + for len(pemCerts) > 0 { + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + certs = append(certs, cert) + } + return certs, nil +} diff --git a/authority/tls.go b/authority/tls.go index 03a9ec33..4480314c 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -64,7 +64,6 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti opts = []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)} mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)} certValidators = []provisioner.CertificateValidator{} - issIdentity = a.intermediateIdentity ) // Set backdate with the configured value @@ -89,7 +88,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti return nil, errs.Wrap(http.StatusBadRequest, err, "authority.Sign; invalid certificate request", opts...) } - leaf, err := x509util.NewLeafProfileWithCSR(csr, issIdentity.Crt, issIdentity.Key, mods...) + leaf, err := x509util.NewLeafProfileWithCSR(csr, a.x509Issuer, a.x509Signer, mods...) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...) } @@ -112,12 +111,6 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti "authority.Sign; error parsing new leaf certificate", opts...) } - caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) - if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, - "authority.Sign; error parsing intermediate certificate", opts...) - } - if err = a.db.StoreCertificate(serverCert); err != nil { if err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, @@ -125,7 +118,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti } } - return []*x509.Certificate{serverCert, caCert}, nil + return []*x509.Certificate{serverCert, a.x509Issuer}, nil } // Renew creates a new Certificate identical to the old certificate, except @@ -138,9 +131,6 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...) } - // Issuer - issIdentity := a.intermediateIdentity - // Durations backdate := a.config.AuthorityConfig.Backdate.Duration duration := oldCert.NotAfter.Sub(oldCert.NotBefore) @@ -148,7 +138,7 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error newCert := &x509.Certificate{ PublicKey: oldCert.PublicKey, - Issuer: issIdentity.Crt.Subject, + Issuer: a.x509Issuer.Subject, Subject: oldCert.Subject, NotBefore: now.Add(-1 * backdate), NotAfter: now.Add(duration - backdate), @@ -188,8 +178,7 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error } } - leaf, err := x509util.NewLeafProfileWithTemplate(newCert, - issIdentity.Crt, issIdentity.Key) + leaf, err := x509util.NewLeafProfileWithTemplate(newCert, a.x509Issuer, a.x509Signer) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...) } @@ -204,11 +193,6 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew; error parsing new server certificate", opts...) } - caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) - if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, - "authority.Renew; error parsing intermediate certificate", opts...) - } if err = a.db.StoreCertificate(serverCert); err != nil { if err != db.ErrNotImplemented { @@ -216,7 +200,7 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error } } - return []*x509.Certificate{serverCert, caCert}, nil + return []*x509.Certificate{serverCert, a.x509Issuer}, nil } // RevokeOptions are the options for the Revoke API. @@ -320,8 +304,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { - profile, err := x509util.NewLeafProfile("Step Online CA", - a.intermediateIdentity.Crt, a.intermediateIdentity.Key, + profile, err := x509util.NewLeafProfile("Step Online CA", a.x509Issuer, a.x509Signer, x509util.WithHosts(strings.Join(a.config.DNSNames, ","))) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") @@ -344,7 +327,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { // Load the x509 key pair (combining server and intermediate blocks) // to a tls.Certificate. - intermediatePEM, err := pemutil.Serialize(a.intermediateIdentity.Crt) + intermediatePEM, err := pemutil.Serialize(a.x509Issuer) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") } diff --git a/authority/tls_test.go b/authority/tls_test.go index f946022f..722338d3 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -2,6 +2,7 @@ package authority import ( "context" + "crypto" "crypto/rand" "crypto/sha1" "crypto/x509" @@ -156,7 +157,7 @@ func TestAuthority_Sign(t *testing.T) { }, "fail create cert": func(t *testing.T) *signTest { _a := testAuthority(t) - _a.intermediateIdentity.Key = nil + _a.x509Signer = nil csr := getCSR(t, priv) return &signTest{ auth: _a, @@ -303,7 +304,7 @@ ZYtQ9Ot36qc= hash := sha1.Sum(pubBytes) assert.Equals(t, leaf.SubjectKeyId, hash[:]) - assert.Equals(t, leaf.AuthorityKeyId, a.intermediateIdentity.Crt.SubjectKeyId) + assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId) // Verify Provisioner OID found := 0 @@ -322,7 +323,7 @@ ZYtQ9Ot36qc= } assert.Equals(t, found, 1) - realIntermediate, err := x509.ParseCertificate(a.intermediateIdentity.Crt.Raw) + realIntermediate, err := x509.ParseCertificate(a.x509Issuer.Raw) assert.FatalError(t, err) assert.Equals(t, intermediate, realIntermediate) } @@ -353,8 +354,7 @@ func TestAuthority_Renew(t *testing.T) { NotAfter: provisioner.NewTimeDuration(na1), } - leaf, err := x509util.NewLeafProfile("renew", a.intermediateIdentity.Crt, - a.intermediateIdentity.Key, + leaf, err := x509util.NewLeafProfile("renew", a.x509Issuer, a.x509Signer, x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), 0), withDefaultASN1DN(a.config.AuthorityConfig.Template), x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"), @@ -365,8 +365,7 @@ func TestAuthority_Renew(t *testing.T) { cert, err := x509.ParseCertificate(certBytes) assert.FatalError(t, err) - leafNoRenew, err := x509util.NewLeafProfile("norenew", a.intermediateIdentity.Crt, - a.intermediateIdentity.Key, + leafNoRenew, err := x509util.NewLeafProfile("norenew", a.x509Issuer, a.x509Signer, x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), 0), withDefaultASN1DN(a.config.AuthorityConfig.Template), x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"), @@ -387,7 +386,7 @@ func TestAuthority_Renew(t *testing.T) { tests := map[string]func() (*renewTest, error){ "fail-create-cert": func() (*renewTest, error) { _a := testAuthority(t) - _a.intermediateIdentity.Key = nil + _a.x509Signer = nil return &renewTest{ auth: _a, cert: cert, @@ -425,8 +424,8 @@ func TestAuthority_Renew(t *testing.T) { assert.FatalError(t, err) _a := testAuthority(t) - _a.intermediateIdentity.Key = newIntermediateProfile.SubjectPrivateKey() - _a.intermediateIdentity.Crt = newIntermediateCert + _a.x509Signer = newIntermediateProfile.SubjectPrivateKey().(crypto.Signer) + _a.x509Issuer = newIntermediateCert return &renewTest{ auth: _a, cert: cert, @@ -494,8 +493,8 @@ func TestAuthority_Renew(t *testing.T) { assert.Equals(t, leaf.SubjectKeyId, hash[:]) // We did not change the intermediate before renewing. - if a.intermediateIdentity.Crt.SerialNumber == tc.auth.intermediateIdentity.Crt.SerialNumber { - assert.Equals(t, leaf.AuthorityKeyId, a.intermediateIdentity.Crt.SubjectKeyId) + if a.x509Issuer.SerialNumber == tc.auth.x509Issuer.SerialNumber { + assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { found := false @@ -511,7 +510,7 @@ func TestAuthority_Renew(t *testing.T) { } } else { // We did change the intermediate before renewing. - assert.Equals(t, leaf.AuthorityKeyId, tc.auth.intermediateIdentity.Crt.SubjectKeyId) + assert.Equals(t, leaf.AuthorityKeyId, tc.auth.x509Issuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { // The authority key id extension should be different b/c the intermediates are different. @@ -535,7 +534,7 @@ func TestAuthority_Renew(t *testing.T) { } } - realIntermediate, err := x509.ParseCertificate(tc.auth.intermediateIdentity.Crt.Raw) + realIntermediate, err := x509.ParseCertificate(tc.auth.x509Issuer.Raw) assert.FatalError(t, err) assert.Equals(t, intermediate, realIntermediate) } diff --git a/go.mod b/go.mod index 004c50d7..69a64a1e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module github.com/smallstep/certificates go 1.13 require ( + cloud.google.com/go v0.51.0 github.com/Masterminds/sprig/v3 v3.0.0 github.com/go-chi/chi v4.0.2+incompatible + github.com/google/go-cmp v0.4.0 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 @@ -15,6 +18,9 @@ require ( github.com/urfave/cli v1.22.2 golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + google.golang.org/api v0.15.0 + google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb + google.golang.org/grpc v1.26.0 gopkg.in/square/go-jose.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index b37f253c..5f1248ac 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,24 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -97,6 +109,7 @@ github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= github.com/go-critic/go-critic v0.4.0 h1:sXD3pix0wDemuPuSlrXpJNNYXlUiKiysLrtPVQmxkzI= github.com/go-critic/go-critic v0.4.0/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= @@ -135,8 +148,11 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -192,11 +208,15 @@ github.com/google/certificate-transparency-go v1.1.0/go.mod h1:i+Q7XY+ArBveOUT36 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/monologue v0.0.0-20190606152607-4b11a32b5934/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk= github.com/google/monologue v0.0.0-20191220140058-35abc9683a6c/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/trillian v1.2.2-0.20190612132142-05461f4df60a/go.mod h1:YPmUVn5NGwgnDUgqlVyFGMTgaWlnSvH7W5p+NdOG8UA= github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1:WgL3XZ3pA8/9cm7yxqWrZE6iZkESB2ItGxy5Fo6k2lk= @@ -204,6 +224,8 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -223,6 +245,7 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -237,6 +260,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= @@ -266,6 +290,7 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible h1:GfzE+uq7odDW7nOmp1QWuilLEK7kJf8i84XcIfk3mKA= github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -299,6 +324,7 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.0.2 h1:CIBkOawOtzJNE0B+EpRiUBzuVW7JEQAwdwhSS6YhIeg= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -538,6 +564,8 @@ go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSF go.etcd.io/etcd v3.3.18+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -552,6 +580,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= @@ -560,14 +589,24 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -584,6 +623,7 @@ golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -595,6 +635,8 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -620,12 +662,16 @@ golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7V golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e h1:LwyF2AFISC9nVbS6MgzsaQNSUsRXI49GS+YQ5KX/QH0= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -650,6 +696,7 @@ golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -657,36 +704,55 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911151314-feee8acb394c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200106190116-7be0a674c9fc h1:MR2F33ipDGog0C4eMhU6u9o3q6c3dvYis2aG6Jl12Wg= golang.org/x/tools v0.0.0-20200106190116-7be0a674c9fc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190605220351-eb0b1bdb6ae6/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb h1:ADPHZzpzM4tk4V4S5cnCrr5SwzvlrPRmqqCuJDB8UTs= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -694,6 +760,7 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -737,6 +804,7 @@ mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskX mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2 h1:K7wru2CfJGumS5hkiguQ0Rb9ebKM2Jo8s5d4Jm9lFaM= mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2/go.mod h1:rCqoQrfAmpTX/h2APczwM7UymU/uvaOluiVPIYCSY/k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go new file mode 100644 index 00000000..a46db037 --- /dev/null +++ b/kms/apiv1/options.go @@ -0,0 +1,68 @@ +package apiv1 + +import ( + "strings" + + "github.com/pkg/errors" +) + +// ErrNotImplemented +type ErrNotImplemented struct { + msg string +} + +func (e ErrNotImplemented) Error() string { + if e.msg != "" { + return e.msg + } + return "not implemented" +} + +// Type represents the KMS type used. +type Type string + +const ( + // DefaultKMS is a KMS implementation using software. + DefaultKMS Type = "" + // SoftKMS is a KMS implementation using software. + SoftKMS Type = "softkms" + // CloudKMS is a KMS implementation using Google's Cloud KMS. + CloudKMS Type = "cloudkms" + // AmazonKMS is a KMS implementation using Amazon AWS KMS. + AmazonKMS Type = "awskms" + // PKCS11 is a KMS implementation using the PKCS11 standard. + PKCS11 Type = "pkcs11" +) + +type Options struct { + // The type of the KMS to use. + Type string `json:"type"` + + // Path to the credentials file used in CloudKMS. + CredentialsFile string `json:"credentialsFile"` + + // Path to the module used with PKCS11 KMS. + Module string `json:"module"` + + // Pin used to access the PKCS11 module. + Pin string `json:"pin"` +} + +// Validate checks the fields in Options. +func (o *Options) Validate() error { + if o == nil { + return nil + } + + switch Type(strings.ToLower(o.Type)) { + case DefaultKMS, SoftKMS, CloudKMS: + case AmazonKMS: + return ErrNotImplemented{"support for AmazonKMS is not yet implemented"} + case PKCS11: + return ErrNotImplemented{"support for PKCS11 is not yet implemented"} + default: + return errors.Errorf("unsupported kms type %s", o.Type) + } + + return nil +} diff --git a/kms/apiv1/options_test.go b/kms/apiv1/options_test.go new file mode 100644 index 00000000..645b63b1 --- /dev/null +++ b/kms/apiv1/options_test.go @@ -0,0 +1,51 @@ +package apiv1 + +import ( + "testing" +) + +func TestOptions_Validate(t *testing.T) { + tests := []struct { + name string + options *Options + wantErr bool + }{ + {"nil", nil, false}, + {"softkms", &Options{Type: "softkms"}, false}, + {"cloudkms", &Options{Type: "cloudkms"}, false}, + {"awskms", &Options{Type: "awskms"}, true}, + {"pkcs11", &Options{Type: "pkcs11"}, true}, + {"unsupported", &Options{Type: "unsupported"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.options.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestErrNotImplemented_Error(t *testing.T) { + type fields struct { + msg string + } + tests := []struct { + name string + fields fields + want string + }{ + {"default", fields{}, "not implemented"}, + {"custom", fields{"custom message: not implemented"}, "custom message: not implemented"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := ErrNotImplemented{ + msg: tt.fields.msg, + } + if got := e.Error(); got != tt.want { + t.Errorf("ErrNotImplemented.Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go new file mode 100644 index 00000000..35c2fcae --- /dev/null +++ b/kms/apiv1/requests.go @@ -0,0 +1,126 @@ +package apiv1 + +import ( + "crypto" + "fmt" +) + +// ProtectionLevel specifies on some KMS how cryptographic operations are +// performed. +type ProtectionLevel int + +const ( + // Protection level not specified. + UnspecifiedProtectionLevel ProtectionLevel = iota + // Crypto operations are performed in software. + Software + // Crypto operations are performed in a Hardware Security Module. + HSM +) + +// String returns a string representation of p. +func (p ProtectionLevel) String() string { + switch p { + case UnspecifiedProtectionLevel: + return "unspecified" + case Software: + return "software" + case HSM: + return "hsm" + default: + return fmt.Sprintf("unknown(%d)", p) + } +} + +// SignatureAlgorithm used for cryptographic signing. +type SignatureAlgorithm int + +const ( + // Not specified. + UnspecifiedSignAlgorithm SignatureAlgorithm = iota + // RSASSA-PKCS1-v1_5 key and a SHA256 digest. + SHA256WithRSA + // RSASSA-PKCS1-v1_5 key and a SHA384 digest. + SHA384WithRSA + // RSASSA-PKCS1-v1_5 key and a SHA512 digest. + SHA512WithRSA + // RSASSA-PSS key with a SHA256 digest. + SHA256WithRSAPSS + // RSASSA-PSS key with a SHA384 digest. + SHA384WithRSAPSS + // RSASSA-PSS key with a SHA512 digest. + SHA512WithRSAPSS + // ECDSA on the NIST P-256 curve with a SHA256 digest. + ECDSAWithSHA256 + // ECDSA on the NIST P-384 curve with a SHA384 digest. + ECDSAWithSHA384 + // ECDSA on the NIST P-521 curve with a SHA512 digest. + ECDSAWithSHA512 + // EdDSA on Curve25519 with a SHA512 digest. + PureEd25519 +) + +// String returns a string representation of s. +func (s SignatureAlgorithm) String() string { + switch s { + case UnspecifiedSignAlgorithm: + return "unspecified" + case SHA256WithRSA: + return "SHA256-RSA" + case SHA384WithRSA: + return "SHA384-RSA" + case SHA512WithRSA: + return "SHA512-RSA" + case SHA256WithRSAPSS: + return "SHA256-RSAPSS" + case SHA384WithRSAPSS: + return "SHA384-RSAPSS" + case SHA512WithRSAPSS: + return "SHA512-RSAPSS" + case ECDSAWithSHA256: + return "ECDSA-SHA256" + case ECDSAWithSHA384: + return "ECDSA-SHA384" + case ECDSAWithSHA512: + return "ECDSA-SHA512" + case PureEd25519: + return "Ed25519" + default: + return fmt.Sprintf("unknown(%d)", s) + } +} + +// GetPublicKeyRequest is the parameter used in the kms.GetPublicKey method. +type GetPublicKeyRequest struct { + Name string +} + +// CreateKeyRequest is the parameter used in the kms.CreateKey method. +type CreateKeyRequest struct { + Name string + SignatureAlgorithm SignatureAlgorithm + Bits int + + // ProtectionLevel specifies how cryptographic operations are performed. + // Used by: cloudkms + ProtectionLevel ProtectionLevel +} + +// CreateKeyResponse is the response value of the kms.CreateKey method. +type CreateKeyResponse struct { + Name string + PublicKey crypto.PublicKey + PrivateKey crypto.PrivateKey + CreateSignerRequest CreateSignerRequest +} + +// CreateSignerRequest is the parameter used in the kms.CreateSigner method. +type CreateSignerRequest struct { + Signer crypto.Signer + SigningKey string + SigningKeyPEM []byte + TokenLabel string + PublicKey string + PublicKeyPEM []byte + Password []byte +} diff --git a/kms/apiv1/requests_test.go b/kms/apiv1/requests_test.go new file mode 100644 index 00000000..b378e631 --- /dev/null +++ b/kms/apiv1/requests_test.go @@ -0,0 +1,51 @@ +package apiv1 + +import "testing" + +func TestProtectionLevel_String(t *testing.T) { + tests := []struct { + name string + p ProtectionLevel + want string + }{ + {"unspecified", UnspecifiedProtectionLevel, "unspecified"}, + {"software", Software, "software"}, + {"hsm", HSM, "hsm"}, + {"unknown", ProtectionLevel(100), "unknown(100)"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.String(); got != tt.want { + t.Errorf("ProtectionLevel.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSignatureAlgorithm_String(t *testing.T) { + tests := []struct { + name string + s SignatureAlgorithm + want string + }{ + {"UnspecifiedSignAlgorithm", UnspecifiedSignAlgorithm, "unspecified"}, + {"SHA256WithRSA", SHA256WithRSA, "SHA256-RSA"}, + {"SHA384WithRSA", SHA384WithRSA, "SHA384-RSA"}, + {"SHA512WithRSA", SHA512WithRSA, "SHA512-RSA"}, + {"SHA256WithRSAPSS", SHA256WithRSAPSS, "SHA256-RSAPSS"}, + {"SHA384WithRSAPSS", SHA384WithRSAPSS, "SHA384-RSAPSS"}, + {"SHA512WithRSAPSS", SHA512WithRSAPSS, "SHA512-RSAPSS"}, + {"ECDSAWithSHA256", ECDSAWithSHA256, "ECDSA-SHA256"}, + {"ECDSAWithSHA384", ECDSAWithSHA384, "ECDSA-SHA384"}, + {"ECDSAWithSHA512", ECDSAWithSHA512, "ECDSA-SHA512"}, + {"PureEd25519", PureEd25519, "Ed25519"}, + {"unknown", SignatureAlgorithm(100), "unknown(100)"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.String(); got != tt.want { + t.Errorf("SignatureAlgorithm.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go new file mode 100644 index 00000000..a2332d5f --- /dev/null +++ b/kms/cloudkms/cloudkms.go @@ -0,0 +1,280 @@ +package cloudkms + +import ( + "context" + "crypto" + "strings" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + cloudkms "cloud.google.com/go/kms/apiv1" + gax "github.com/googleapis/gax-go/v2" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" + "google.golang.org/api/option" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +// protectionLevelMapping maps step protection levels with cloud kms ones. +var protectionLevelMapping = map[apiv1.ProtectionLevel]kmspb.ProtectionLevel{ + apiv1.UnspecifiedProtectionLevel: kmspb.ProtectionLevel_PROTECTION_LEVEL_UNSPECIFIED, + apiv1.Software: kmspb.ProtectionLevel_SOFTWARE, + apiv1.HSM: kmspb.ProtectionLevel_HSM, +} + +// signatureAlgorithmMapping is a mapping between the step signature algorithm, +// and bits for RSA keys, with cloud kms one. +// +// Cloud KMS does not support SHA384WithRSA, SHA384WithRSAPSS, SHA384WithRSAPSS, +// ECDSAWithSHA512, and PureEd25519. +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ + apiv1.UnspecifiedSignAlgorithm: kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED, + apiv1.SHA256WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256, + 2048: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, + 3072: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + }, + apiv1.SHA512WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + }, + apiv1.SHA256WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, + 2048: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256, + 3072: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256, + }, + apiv1.SHA512WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512, + }, + apiv1.ECDSAWithSHA256: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + apiv1.ECDSAWithSHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, +} + +// KeyManagementClient defines the methods on KeyManagementClient that this +// package will use. This interface will be used for unit testing. +type KeyManagementClient interface { + Close() error + GetPublicKey(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error) + AsymmetricSign(context.Context, *kmspb.AsymmetricSignRequest, ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) + CreateCryptoKey(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error) + GetKeyRing(context.Context, *kmspb.GetKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error) + CreateKeyRing(context.Context, *kmspb.CreateKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error) + CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) +} + +// CloudKMS implements a KMS using Google's Cloud apiv1. +type CloudKMS struct { + client KeyManagementClient +} + +// New creates a new CloudKMS configured with a new client. +func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { + var cloudOpts []option.ClientOption + if opts.CredentialsFile != "" { + cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) + } + + client, err := cloudkms.NewKeyManagementClient(ctx, cloudOpts...) + if err != nil { + return nil, err + } + + return &CloudKMS{ + client: client, + }, nil +} + +// NewCloudKMS creates a CloudKMS with a given client. +func NewCloudKMS(client KeyManagementClient) *CloudKMS { + return &CloudKMS{ + client: client, + } +} + +// Close closes the connection of the Cloud KMS client. +func (k *CloudKMS) Close() error { + if err := k.client.Close(); err != nil { + return errors.Wrap(err, "cloudKMS Close failed") + } + return nil +} + +// CreateSigner returns a new cloudkms signer configured with the given signing +// key name. +func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + if req.SigningKey == "" { + return nil, errors.New("signing key cannot be empty") + } + + return NewSigner(k.client, req.SigningKey), nil +} + +// CreateKey creates in Google's Cloud KMS a new asymmetric key for signing. +func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + if req.Name == "" { + return nil, errors.New("createKeyRequest 'name' cannot be empty") + } + + protectionLevel, ok := protectionLevelMapping[req.ProtectionLevel] + if !ok { + return nil, errors.Errorf("cloudKMS does not support protection level '%s'", req.ProtectionLevel) + } + + var signatureAlgorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm + v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm] + if !ok { + return nil, errors.Errorf("cloudKMS does not support signature algorithm '%s'", req.SignatureAlgorithm) + } + switch v := v.(type) { + case kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm: + signatureAlgorithm = v + case map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm: + if signatureAlgorithm, ok = v[req.Bits]; !ok { + return nil, errors.Errorf("cloudKMS does not support signature algorithm '%s' with '%d' bits", req.SignatureAlgorithm, req.Bits) + } + default: + return nil, errors.Errorf("unexpected error: this should not happen") + } + + var crytoKeyName string + + // Split `projects/PROJECT_ID/locations/global/keyRings/RING_ID/cryptoKeys/KEY_ID` + // to `projects/PROJECT_ID/locations/global/keyRings/RING_ID` and `KEY_ID`. + keyRing, keyID := Parent(req.Name) + if err := k.createKeyRingIfNeeded(keyRing); err != nil { + return nil, err + } + + ctx, cancel := defaultContext() + defer cancel() + + // Create private key in CloudKMS. + response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ + Parent: keyRing, + CryptoKeyId: keyID, + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ + ProtectionLevel: protectionLevel, + Algorithm: signatureAlgorithm, + }, + }, + }) + if err != nil { + if status.Code(err) != codes.AlreadyExists { + return nil, errors.Wrap(err, "cloudKMS CreateCryptoKey failed") + } + // Create a new version if the key already exists. + // + // Note that it will have the same purpose, protection level and + // algorithm than as previous one. + req := &kmspb.CreateCryptoKeyVersionRequest{ + Parent: req.Name, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + State: kmspb.CryptoKeyVersion_ENABLED, + }, + } + response, err := k.client.CreateCryptoKeyVersion(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS CreateCryptoKeyVersion failed") + } + crytoKeyName = response.Name + } else { + crytoKeyName = response.Name + "/cryptoKeyVersions/1" + } + + // Retrieve public key to add it to the response. + pk, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{ + Name: crytoKeyName, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS GetPublicKey failed") + } + + return &apiv1.CreateKeyResponse{ + Name: crytoKeyName, + PublicKey: pk, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: crytoKeyName, + }, + }, nil +} + +func (k *CloudKMS) createKeyRingIfNeeded(name string) error { + ctx, cancel := defaultContext() + defer cancel() + + _, err := k.client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{ + Name: name, + }) + if err == nil { + return nil + } + + parent, child := Parent(name) + _, err = k.client.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{ + Parent: parent, + KeyRingId: child, + }) + if err != nil && status.Code(err) != codes.AlreadyExists { + return errors.Wrap(err, "cloudKMS CreateKeyRing failed") + } + + return nil +} + +// GetPublicKey gets from Google's Cloud KMS a public key by name. Key names +// follow the pattern: +// projects/([^/]+)/locations/([a-zA-Z0-9_-]{1,63})/keyRings/([a-zA-Z0-9_-]{1,63})/cryptoKeys/([a-zA-Z0-9_-]{1,63})/cryptoKeyVersions/([a-zA-Z0-9_-]{1,63}) +func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + if req.Name == "" { + return nil, errors.New("createKeyRequest 'name' cannot be empty") + } + + ctx, cancel := defaultContext() + defer cancel() + + response, err := k.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ + Name: req.Name, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS GetPublicKey failed") + } + + pk, err := pemutil.ParseKey([]byte(response.Pem)) + if err != nil { + return nil, err + } + + return pk, nil +} + +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} + +// Parent splits a string in the format `key/value/key2/value2` in a parent and +// child, for the previous string it will return `key/value` and `value2`. +func Parent(name string) (string, string) { + a, b := parent(name) + a, _ = parent(a) + return a, b +} + +func parent(name string) (string, string) { + i := strings.LastIndex(name, "/") + switch i { + case -1: + return "", name + case 0: + return "", name[i+1:] + default: + return name[:i], name[i+1:] + } +} diff --git a/kms/cloudkms/cloudkms_test.go b/kms/cloudkms/cloudkms_test.go new file mode 100644 index 00000000..b4f92fa6 --- /dev/null +++ b/kms/cloudkms/cloudkms_test.go @@ -0,0 +1,376 @@ +package cloudkms + +import ( + "context" + "crypto" + "fmt" + "io/ioutil" + "os" + "reflect" + "testing" + + gax "github.com/googleapis/gax-go/v2" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestParent(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want string + want1 string + }{ + {"zero", args{"child"}, "", "child"}, + {"one", args{"parent/child"}, "", "child"}, + {"two", args{"grandparent/parent/child"}, "grandparent", "child"}, + {"three", args{"great-grandparent/grandparent/parent/child"}, "great-grandparent/grandparent", "child"}, + {"empty", args{""}, "", ""}, + {"root", args{"/"}, "", ""}, + {"child", args{"/child"}, "", "child"}, + {"parent", args{"parent/"}, "", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := Parent(tt.args.name) + if got != tt.want { + t.Errorf("Parent() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Parent() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestNew(t *testing.T) { + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + skipOnCI bool + args args + want *CloudKMS + wantErr bool + }{ + {"fail authentication", true, args{context.Background(), apiv1.Options{}}, nil, true}, + {"fail credentials", false, args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.skipOnCI && os.Getenv("CI") == "true" { + t.SkipNow() + } + + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewCloudKMS(t *testing.T) { + type args struct { + client KeyManagementClient + } + tests := []struct { + name string + args args + want *CloudKMS + }{ + {"ok", args{&MockClient{}}, &CloudKMS{&MockClient{}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewCloudKMS(tt.args.client); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewCloudKMS() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCloudKMS_Close(t *testing.T) { + type fields struct { + client KeyManagementClient + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{&MockClient{close: func() error { return nil }}}, false}, + {"fail", fields{&MockClient{close: func() error { return fmt.Errorf("an error") }}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &CloudKMS{ + client: tt.fields.client, + } + if err := k.Close(); (err != nil) != tt.wantErr { + t.Errorf("CloudKMS.Close() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCloudKMS_CreateSigner(t *testing.T) { + keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1" + type fields struct { + client KeyManagementClient + } + type args struct { + req *apiv1.CreateSignerRequest + } + tests := []struct { + name string + fields fields + args args + want crypto.Signer + wantErr bool + }{ + {"ok", fields{&MockClient{}}, args{&apiv1.CreateSignerRequest{SigningKey: keyName}}, &Signer{client: &MockClient{}, signingKey: keyName}, false}, + {"fail", fields{&MockClient{}}, args{&apiv1.CreateSignerRequest{SigningKey: ""}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &CloudKMS{ + client: tt.fields.client, + } + got, err := k.CreateSigner(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudKMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudKMS.CreateSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCloudKMS_CreateKey(t *testing.T) { + keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c" + testError := fmt.Errorf("an error") + alreadyExists := status.Error(codes.AlreadyExists, "already exists") + + pemBytes, err := ioutil.ReadFile("testdata/pub.pem") + if err != nil { + t.Fatal(err) + } + pk, err := pemutil.ParseKey(pemBytes) + if err != nil { + t.Fatal(err) + } + + type fields struct { + client KeyManagementClient + } + type args struct { + req *apiv1.CreateKeyRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.CreateKeyResponse + wantErr bool + }{ + {"ok", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return &kmspb.KeyRing{}, nil + }, + createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) { + return &kmspb.CryptoKey{Name: keyName}, nil + }, + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string(pemBytes)}, nil + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, + &apiv1.CreateKeyResponse{Name: keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: keyName + "/cryptoKeyVersions/1"}}, false}, + {"ok new key ring", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return nil, testError + }, + createKeyRing: func(_ context.Context, _ *kmspb.CreateKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return nil, alreadyExists + }, + createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) { + return &kmspb.CryptoKey{Name: keyName}, nil + }, + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string(pemBytes)}, nil + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.Software, SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 3072}}, + &apiv1.CreateKeyResponse{Name: keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: keyName + "/cryptoKeyVersions/1"}}, false}, + {"ok new key version", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return &kmspb.KeyRing{}, nil + }, + createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) { + return nil, alreadyExists + }, + createCryptoKeyVersion: func(_ context.Context, _ *kmspb.CreateCryptoKeyVersionRequest, _ ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return &kmspb.CryptoKeyVersion{Name: keyName + "/cryptoKeyVersions/2"}, nil + }, + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string(pemBytes)}, nil + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, + &apiv1.CreateKeyResponse{Name: keyName + "/cryptoKeyVersions/2", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: keyName + "/cryptoKeyVersions/2"}}, false}, + {"fail name", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{}}, nil, true}, + {"fail protection level", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.ProtectionLevel(100)}}, nil, true}, + {"fail signature algorithm", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.Software, SignatureAlgorithm: apiv1.SignatureAlgorithm(100)}}, nil, true}, + {"fail number of bits", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.Software, SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 1024}}, + nil, true}, + {"fail create key ring", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return nil, testError + }, + createKeyRing: func(_ context.Context, _ *kmspb.CreateKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return nil, testError + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, + nil, true}, + {"fail create key", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return &kmspb.KeyRing{}, nil + }, + createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) { + return nil, testError + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, + nil, true}, + {"fail create key version", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return &kmspb.KeyRing{}, nil + }, + createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) { + return nil, alreadyExists + }, + createCryptoKeyVersion: func(_ context.Context, _ *kmspb.CreateCryptoKeyVersionRequest, _ ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return nil, testError + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, + nil, true}, + {"fail get public key", fields{ + &MockClient{ + getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) { + return &kmspb.KeyRing{}, nil + }, + createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) { + return &kmspb.CryptoKey{Name: keyName}, nil + }, + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return nil, testError + }, + }}, + args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, + nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &CloudKMS{ + client: tt.fields.client, + } + got, err := k.CreateKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudKMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudKMS.CreateKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCloudKMS_GetPublicKey(t *testing.T) { + keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1" + testError := fmt.Errorf("an error") + + pemBytes, err := ioutil.ReadFile("testdata/pub.pem") + if err != nil { + t.Fatal(err) + } + pk, err := pemutil.ParseKey(pemBytes) + if err != nil { + t.Fatal(err) + } + + type fields struct { + client KeyManagementClient + } + type args struct { + req *apiv1.GetPublicKeyRequest + } + tests := []struct { + name string + fields fields + args args + want crypto.PublicKey + wantErr bool + }{ + {"ok", fields{ + &MockClient{ + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string(pemBytes)}, nil + }, + }}, + args{&apiv1.GetPublicKeyRequest{Name: keyName}}, pk, false}, + {"fail name", fields{&MockClient{}}, args{&apiv1.GetPublicKeyRequest{}}, nil, true}, + {"fail get public key", fields{ + &MockClient{ + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return nil, testError + }, + }}, + args{&apiv1.GetPublicKeyRequest{Name: keyName}}, nil, true}, + {"fail parse pem", fields{ + &MockClient{ + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string("bad pem")}, nil + }, + }}, + args{&apiv1.GetPublicKeyRequest{Name: keyName}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &CloudKMS{ + client: tt.fields.client, + } + got, err := k.GetPublicKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudKMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudKMS.GetPublicKey() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/cloudkms/mock_test.go b/kms/cloudkms/mock_test.go new file mode 100644 index 00000000..7617bd85 --- /dev/null +++ b/kms/cloudkms/mock_test.go @@ -0,0 +1,46 @@ +package cloudkms + +import ( + "context" + + gax "github.com/googleapis/gax-go/v2" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +type MockClient struct { + close func() error + getPublicKey func(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error) + asymmetricSign func(context.Context, *kmspb.AsymmetricSignRequest, ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) + createCryptoKey func(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error) + getKeyRing func(context.Context, *kmspb.GetKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error) + createKeyRing func(context.Context, *kmspb.CreateKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error) + createCryptoKeyVersion func(context.Context, *kmspb.CreateCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) +} + +func (m *MockClient) Close() error { + return m.close() +} + +func (m *MockClient) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) { + return m.getPublicKey(ctx, req, opts...) +} + +func (m *MockClient) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) { + return m.asymmetricSign(ctx, req, opts...) +} + +func (m *MockClient) CreateCryptoKey(ctx context.Context, req *kmspb.CreateCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { + return m.createCryptoKey(ctx, req, opts...) +} + +func (m *MockClient) GetKeyRing(ctx context.Context, req *kmspb.GetKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) { + return m.getKeyRing(ctx, req, opts...) +} + +func (m *MockClient) CreateKeyRing(ctx context.Context, req *kmspb.CreateKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) { + return m.createKeyRing(ctx, req, opts...) +} + +func (m *MockClient) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return m.createCryptoKeyVersion(ctx, req, opts...) +} diff --git a/kms/cloudkms/signer.go b/kms/cloudkms/signer.go new file mode 100644 index 00000000..b9232ca4 --- /dev/null +++ b/kms/cloudkms/signer.go @@ -0,0 +1,78 @@ +package cloudkms + +import ( + "crypto" + "io" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/pemutil" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +// Signer implements a crypto.Signer using Google's Cloud KMS. +type Signer struct { + client KeyManagementClient + signingKey string +} + +func NewSigner(c KeyManagementClient, signingKey string) *Signer { + return &Signer{ + client: c, + signingKey: signingKey, + } +} + +// Public returns the public key of this signer or an error. +func (s *Signer) Public() crypto.PublicKey { + ctx, cancel := defaultContext() + defer cancel() + + response, err := s.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ + Name: s.signingKey, + }) + if err != nil { + return errors.Wrap(err, "cloudKMS GetPublicKey failed") + } + + pk, err := pemutil.ParseKey([]byte(response.Pem)) + if err != nil { + return err + } + + return pk +} + +// Sign signs digest with the private key stored in Google's Cloud KMS. +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + req := &kmspb.AsymmetricSignRequest{ + Name: s.signingKey, + Digest: &kmspb.Digest{}, + } + + switch h := opts.HashFunc(); h { + case crypto.SHA256: + req.Digest.Digest = &kmspb.Digest_Sha256{ + Sha256: digest, + } + case crypto.SHA384: + req.Digest.Digest = &kmspb.Digest_Sha384{ + Sha384: digest, + } + case crypto.SHA512: + req.Digest.Digest = &kmspb.Digest_Sha512{ + Sha512: digest, + } + default: + return nil, errors.Errorf("unsupported hash function %v", h) + } + + ctx, cancel := defaultContext() + defer cancel() + + response, err := s.client.AsymmetricSign(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS AsymmetricSign failed") + } + + return response.Signature, nil +} diff --git a/kms/cloudkms/signer_test.go b/kms/cloudkms/signer_test.go new file mode 100644 index 00000000..9a05e131 --- /dev/null +++ b/kms/cloudkms/signer_test.go @@ -0,0 +1,148 @@ +package cloudkms + +import ( + "context" + "crypto" + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "reflect" + "testing" + + gax "github.com/googleapis/gax-go/v2" + "github.com/smallstep/cli/crypto/pemutil" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +func Test_newSigner(t *testing.T) { + type args struct { + c KeyManagementClient + signingKey string + } + tests := []struct { + name string + args args + want *Signer + }{ + {"ok", args{&MockClient{}, "signingKey"}, &Signer{client: &MockClient{}, signingKey: "signingKey"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewSigner(tt.args.c, tt.args.signingKey); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_signer_Public(t *testing.T) { + keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1" + testError := fmt.Errorf("an error") + + pemBytes, err := ioutil.ReadFile("testdata/pub.pem") + if err != nil { + t.Fatal(err) + } + pk, err := pemutil.ParseKey(pemBytes) + if err != nil { + t.Fatal(err) + } + + type fields struct { + client KeyManagementClient + signingKey string + } + tests := []struct { + name string + fields fields + want crypto.PublicKey + wantErr bool + }{ + {"ok", fields{&MockClient{ + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string(pemBytes)}, nil + }, + }, keyName}, pk, false}, + {"fail get public key", fields{&MockClient{ + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return nil, testError + }, + }, keyName}, nil, true}, + {"fail parse pem", fields{ + &MockClient{ + getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + return &kmspb.PublicKey{Pem: string("bad pem")}, nil + }, + }, keyName}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + client: tt.fields.client, + signingKey: tt.fields.signingKey, + } + got := s.Public() + if _, ok := got.(error); ok != tt.wantErr { + t.Errorf("signer.Public() error = %v, wantErr %v", got, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("signer.Public() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_signer_Sign(t *testing.T) { + keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1" + okClient := &MockClient{ + asymmetricSign: func(_ context.Context, _ *kmspb.AsymmetricSignRequest, _ ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) { + return &kmspb.AsymmetricSignResponse{Signature: []byte("ok signature")}, nil + }, + } + failClient := &MockClient{ + asymmetricSign: func(_ context.Context, _ *kmspb.AsymmetricSignRequest, _ ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) { + return nil, fmt.Errorf("an error") + }, + } + + type fields struct { + client KeyManagementClient + signingKey string + } + type args struct { + rand io.Reader + digest []byte + opts crypto.SignerOpts + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + {"ok sha256", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA256}, []byte("ok signature"), false}, + {"ok sha384", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA384}, []byte("ok signature"), false}, + {"ok sha512", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA512}, []byte("ok signature"), false}, + {"fail MD5", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.MD5}, nil, true}, + {"fail asymmetric sign", fields{failClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA256}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + client: tt.fields.client, + signingKey: tt.fields.signingKey, + } + got, err := s.Sign(tt.args.rand, tt.args.digest, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("signer.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("signer.Sign() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/cloudkms/testdata/pub.pem b/kms/cloudkms/testdata/pub.pem new file mode 100644 index 00000000..e31e583e --- /dev/null +++ b/kms/cloudkms/testdata/pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1 +veT13hakPZF9YzaNVZgujqK3d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w== +-----END PUBLIC KEY----- diff --git a/kms/kms.go b/kms/kms.go new file mode 100644 index 00000000..209783e5 --- /dev/null +++ b/kms/kms.go @@ -0,0 +1,36 @@ +package kms + +import ( + "context" + "crypto" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/cloudkms" + "github.com/smallstep/certificates/kms/softkms" +) + +// KeyManager is the interface implemented by all the KMS. +type KeyManager interface { + GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) + CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) + CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) + Close() error +} + +// New initializes a new KMS from the given type. +func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + + switch apiv1.Type(strings.ToLower(opts.Type)) { + case apiv1.DefaultKMS, apiv1.SoftKMS: + return softkms.New(ctx, opts) + case apiv1.CloudKMS: + return cloudkms.New(ctx, opts) + default: + return nil, errors.Errorf("unsupported kms type '%s'", opts.Type) + } +} diff --git a/kms/kms_test.go b/kms/kms_test.go new file mode 100644 index 00000000..f377072f --- /dev/null +++ b/kms/kms_test.go @@ -0,0 +1,51 @@ +package kms + +import ( + "context" + "os" + "reflect" + "testing" + + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/cloudkms" + "github.com/smallstep/certificates/kms/softkms" +) + +func TestNew(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + skipOnCI bool + args args + want KeyManager + wantErr bool + }{ + {"softkms", false, args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false}, + {"default", false, args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, false}, + {"cloudkms", true, args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, true}, // fails because not credentials + {"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, nil, true}, // not yet supported + {"pkcs11", false, args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not yet supported + {"fail validation", false, args{ctx, apiv1.Options{Type: "foobar"}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.skipOnCI && os.Getenv("CI") == "true" { + t.SkipNow() + } + + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if reflect.TypeOf(got) != reflect.TypeOf(tt.want) { + t.Errorf("New() = %T, want %T", got, tt.want) + } + }) + } +} diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go new file mode 100644 index 00000000..fb38a1c5 --- /dev/null +++ b/kms/softkms/softkms.go @@ -0,0 +1,134 @@ +package softkms + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/crypto/pemutil" +) + +type algorithmAttributes struct { + Type string + Curve string +} + +// DefaultRSAKeySize is the default size for RSA keys. +const DefaultRSAKeySize = 3072 + +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{ + apiv1.UnspecifiedSignAlgorithm: {"EC", "P-256"}, + apiv1.SHA256WithRSA: {"RSA", ""}, + apiv1.SHA384WithRSA: {"RSA", ""}, + apiv1.SHA512WithRSA: {"RSA", ""}, + apiv1.SHA256WithRSAPSS: {"RSA", ""}, + apiv1.SHA384WithRSAPSS: {"RSA", ""}, + apiv1.SHA512WithRSAPSS: {"RSA", ""}, + apiv1.ECDSAWithSHA256: {"EC", "P-256"}, + apiv1.ECDSAWithSHA384: {"EC", "P-384"}, + apiv1.ECDSAWithSHA512: {"EC", "P-521"}, + apiv1.PureEd25519: {"OKP", "Ed25519"}, +} + +// generateKey is used for testing purposes. +var generateKey = func(kty, crv string, size int) (interface{}, interface{}, error) { + if kty == "RSA" && size == 0 { + size = DefaultRSAKeySize + } + return keys.GenerateKeyPair(kty, crv, size) +} + +// SoftKMS is a key manager that uses keys stored in disk. +type SoftKMS struct{} + +// New returns a new SoftKMS. +func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { + return &SoftKMS{}, nil +} + +// Close is a noop that just returns nil. +func (k *SoftKMS) Close() error { + return nil +} + +// CreateSigner returns a new signer configured with the given signing key. +func (k *SoftKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + var opts []pemutil.Options + if req.Password != nil { + opts = append(opts, pemutil.WithPassword(req.Password)) + } + + switch { + case req.Signer != nil: + return req.Signer, nil + case len(req.SigningKeyPEM) != 0: + v, err := pemutil.ParseKey(req.SigningKeyPEM, opts...) + if err != nil { + return nil, err + } + sig, ok := v.(crypto.Signer) + if !ok { + return nil, errors.New("signingKeyPEM is not a crypto.Signer") + } + return sig, nil + case req.SigningKey != "": + v, err := pemutil.Read(req.SigningKey, opts...) + if err != nil { + return nil, err + } + sig, ok := v.(crypto.Signer) + if !ok { + return nil, errors.New("signingKey is not a crypto.Signer") + } + return sig, nil + default: + return nil, errors.New("failed to load softKMS: please define signingKeyPEM or signingKey") + } +} + +func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm] + if !ok { + return nil, errors.Errorf("softKMS does not support signature algorithm '%s'", req.SignatureAlgorithm) + } + + pub, priv, err := generateKey(v.Type, v.Curve, req.Bits) + if err != nil { + return nil, err + } + signer, ok := priv.(crypto.Signer) + if !ok { + return nil, errors.Errorf("softKMS createKey result is not a crypto.Signer: type %T", priv) + } + + return &apiv1.CreateKeyResponse{ + Name: req.Name, + PublicKey: pub, + PrivateKey: priv, + CreateSignerRequest: apiv1.CreateSignerRequest{ + Signer: signer, + }, + }, nil +} + +func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + v, err := pemutil.Read(req.Name) + if err != nil { + return nil, err + } + + switch vv := v.(type) { + case *x509.Certificate: + return vv.PublicKey, nil + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + return vv, nil + default: + return nil, errors.Errorf("unsupported public key type %T", v) + } +} diff --git a/kms/softkms/softkms_test.go b/kms/softkms/softkms_test.go new file mode 100644 index 00000000..44dccaa9 --- /dev/null +++ b/kms/softkms/softkms_test.go @@ -0,0 +1,312 @@ +package softkms + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "reflect" + "testing" + + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" +) + +func TestNew(t *testing.T) { + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + want *SoftKMS + wantErr bool + }{ + {"ok", args{context.Background(), apiv1.Options{}}, &SoftKMS{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSoftKMS_Close(t *testing.T) { + tests := []struct { + name string + wantErr bool + }{ + {"ok", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SoftKMS{} + if err := k.Close(); (err != nil) != tt.wantErr { + t.Errorf("SoftKMS.Close() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSoftKMS_CreateSigner(t *testing.T) { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pemBlock, err := pemutil.Serialize(pk) + if err != nil { + t.Fatal(err) + } + pemBlockPassword, err := pemutil.Serialize(pk, pemutil.WithPassword([]byte("pass"))) + if err != nil { + t.Fatal(err) + } + + // Read and decode file using standard packages + b, err := ioutil.ReadFile("testdata/priv.pem") + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(b) + block.Bytes, err = x509.DecryptPEMBlock(block, []byte("pass")) + if err != nil { + t.Fatal(err) + } + pk2, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + t.Fatal(err) + } + + // Create a public PEM + b, err = x509.MarshalPKIXPublicKey(pk.Public()) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: b, + }) + + type args struct { + req *apiv1.CreateSignerRequest + } + tests := []struct { + name string + args args + want crypto.Signer + wantErr bool + }{ + {"signer", args{&apiv1.CreateSignerRequest{Signer: pk}}, pk, false}, + {"pem", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pem.EncodeToMemory(pemBlock)}}, pk, false}, + {"pem password", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pem.EncodeToMemory(pemBlockPassword), Password: []byte("pass")}}, pk, false}, + {"file", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/priv.pem", Password: []byte("pass")}}, pk2, false}, + {"fail", args{&apiv1.CreateSignerRequest{}}, nil, true}, + {"fail bad pem", args{&apiv1.CreateSignerRequest{SigningKeyPEM: []byte("bad pem")}}, nil, true}, + {"fail bad password", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/priv.pem", Password: []byte("bad-pass")}}, nil, true}, + {"fail not a signer", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pub}}, nil, true}, + {"fail not a signer from file", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/pub.pem"}}, nil, true}, + {"fail missing", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/missing"}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SoftKMS{} + got, err := k.CreateSigner(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SoftKMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SoftKMS.CreateSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func restoreGenerateKey() func() { + oldGenerateKey := generateKey + return func() { + generateKey = oldGenerateKey + } +} + +func TestSoftKMS_CreateKey(t *testing.T) { + fn := restoreGenerateKey() + defer fn() + + p256, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + rsa2048, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + edpub, edpriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + type args struct { + req *apiv1.CreateKeyRequest + } + type params struct { + kty string + crv string + size int + } + tests := []struct { + name string + args args + generateKey func() (interface{}, interface{}, error) + want *apiv1.CreateKeyResponse + wantParams params + wantErr bool + }{ + {"p256", args{&apiv1.CreateKeyRequest{Name: "p256", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) { + return p256.Public(), p256, nil + }, &apiv1.CreateKeyResponse{Name: "p256", PublicKey: p256.Public(), PrivateKey: p256, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: p256}}, params{"EC", "P-256", 0}, false}, + {"rsa", args{&apiv1.CreateKeyRequest{Name: "rsa3072", SignatureAlgorithm: apiv1.SHA256WithRSA}}, func() (interface{}, interface{}, error) { + return rsa2048.Public(), rsa2048, nil + }, &apiv1.CreateKeyResponse{Name: "rsa3072", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 0}, false}, + {"rsa2048", args{&apiv1.CreateKeyRequest{Name: "rsa2048", SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 2048}}, func() (interface{}, interface{}, error) { + return rsa2048.Public(), rsa2048, nil + }, &apiv1.CreateKeyResponse{Name: "rsa2048", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 2048}, false}, + {"rsaPSS2048", args{&apiv1.CreateKeyRequest{Name: "rsa2048", SignatureAlgorithm: apiv1.SHA256WithRSAPSS, Bits: 2048}}, func() (interface{}, interface{}, error) { + return rsa2048.Public(), rsa2048, nil + }, &apiv1.CreateKeyResponse{Name: "rsa2048", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 2048}, false}, + {"ed25519", args{&apiv1.CreateKeyRequest{Name: "ed25519", SignatureAlgorithm: apiv1.PureEd25519}}, func() (interface{}, interface{}, error) { + return edpub, edpriv, nil + }, &apiv1.CreateKeyResponse{Name: "ed25519", PublicKey: edpub, PrivateKey: edpriv, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: edpriv}}, params{"OKP", "Ed25519", 0}, false}, + {"default", args{&apiv1.CreateKeyRequest{Name: "default"}}, func() (interface{}, interface{}, error) { + return p256.Public(), p256, nil + }, &apiv1.CreateKeyResponse{Name: "default", PublicKey: p256.Public(), PrivateKey: p256, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: p256}}, params{"EC", "P-256", 0}, false}, + {"fail algorithm", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.SignatureAlgorithm(100)}}, func() (interface{}, interface{}, error) { + return p256.Public(), p256, nil + }, nil, params{}, true}, + {"fail generate key", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) { + return nil, nil, fmt.Errorf("an error") + }, nil, params{"EC", "P-256", 0}, true}, + {"fail no signer", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) { + return 1, 2, nil + }, nil, params{"EC", "P-256", 0}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SoftKMS{} + generateKey = func(kty, crv string, size int) (interface{}, interface{}, error) { + if tt.wantParams.kty != kty { + t.Errorf("GenerateKey() kty = %s, want %s", kty, tt.wantParams.kty) + } + if tt.wantParams.crv != crv { + t.Errorf("GenerateKey() crv = %s, want %s", crv, tt.wantParams.crv) + } + if tt.wantParams.size != size { + t.Errorf("GenerateKey() size = %d, want %d", size, tt.wantParams.size) + } + return tt.generateKey() + } + + got, err := k.CreateKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SoftKMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SoftKMS.CreateKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSoftKMS_GetPublicKey(t *testing.T) { + b, err := ioutil.ReadFile("testdata/pub.pem") + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(b) + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + t.Fatal(err) + } + + type args struct { + req *apiv1.GetPublicKeyRequest + } + tests := []struct { + name string + args args + want crypto.PublicKey + wantErr bool + }{ + {"key", args{&apiv1.GetPublicKeyRequest{Name: "testdata/pub.pem"}}, pub, false}, + {"cert", args{&apiv1.GetPublicKeyRequest{Name: "testdata/cert.crt"}}, pub, false}, + {"fail not exists", args{&apiv1.GetPublicKeyRequest{Name: "testdata/missing"}}, nil, true}, + {"fail type", args{&apiv1.GetPublicKeyRequest{Name: "testdata/cert.key"}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SoftKMS{} + got, err := k.GetPublicKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SoftKMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SoftKMS.GetPublicKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_generateKey(t *testing.T) { + type args struct { + kty string + crv string + size int + } + tests := []struct { + name string + args args + wantType interface{} + wantType1 interface{} + wantErr bool + }{ + {"rsa2048", args{"RSA", "", 0}, &rsa.PublicKey{}, &rsa.PrivateKey{}, false}, + {"rsa2048", args{"RSA", "", 2048}, &rsa.PublicKey{}, &rsa.PrivateKey{}, false}, + {"p256", args{"EC", "P-256", 0}, &ecdsa.PublicKey{}, &ecdsa.PrivateKey{}, false}, + {"ed25519", args{"OKP", "Ed25519", 0}, ed25519.PublicKey{}, ed25519.PrivateKey{}, false}, + {"fail kty", args{"FOO", "", 0}, nil, nil, true}, + {"fail crv", args{"EC", "P-123", 0}, nil, nil, true}, + {"fail size", args{"RSA", "", 1}, nil, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := generateKey(tt.args.kty, tt.args.crv, tt.args.size) + if (err != nil) != tt.wantErr { + t.Errorf("generateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if reflect.TypeOf(got) != reflect.TypeOf(tt.wantType) { + t.Errorf("generateKey() got = %T, want %T", got, tt.wantType) + } + if reflect.TypeOf(got1) != reflect.TypeOf(tt.wantType1) { + t.Errorf("generateKey() got1 = %T, want %T", got1, tt.wantType1) + } + }) + } +} diff --git a/kms/softkms/testdata/cert.crt b/kms/softkms/testdata/cert.crt new file mode 100644 index 00000000..d6f02b21 --- /dev/null +++ b/kms/softkms/testdata/cert.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBpzCCAU2gAwIBAgIQWaY8KIDAfak8aYljelf8eTAKBggqhkjOPQQDAjAdMRsw +GQYDVQQDExJ0ZXN0LnNtYWxsc3RlcC5jb20wHhcNMjAwMTE2MDAwNDU4WhcNMjAw +MTE3MDAwNDU4WjAdMRswGQYDVQQDExJ0ZXN0LnNtYWxsc3RlcC5jb20wWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAATlU8P9blFefSWuzYx2g215NJn6yHW95PXeFqQ9 +kX1jNo1VmC6Oord3We37iM8QJT4QP9ZDUaAVmJUZSjd+W8H/o28wbTAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW +BBTn0wonKkm2lLRNYZrKhUukiynvqzAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl +cC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAJ5XqryBIY1X4fl/9l0isV69eQfA0Qo5 +1mjervUcEnOWAiBsmN4frz5YVw7i4UXChVBeZLZfJOKvn5eyh2gEzoq1+w== +-----END CERTIFICATE----- diff --git a/kms/softkms/testdata/cert.key b/kms/softkms/testdata/cert.key new file mode 100644 index 00000000..187713cd --- /dev/null +++ b/kms/softkms/testdata/cert.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICB6lIrMa9fVQJtdAYS4qmdYQ1BHJsEQDx8zxL38gA8toAoGCCqGSM49 +AwEHoUQDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1veT13hakPZF9YzaNVZgujqK3 +d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w== +-----END EC PRIVATE KEY----- diff --git a/kms/softkms/testdata/priv.pem b/kms/softkms/testdata/priv.pem new file mode 100644 index 00000000..81116ce7 --- /dev/null +++ b/kms/softkms/testdata/priv.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,1fcec5dfbf3327f61bfe5ab6ae8a0626 + +V39b/pNHMbP80TXSHLsUY6UOTCzf3KwIxvj1e7S9brNMJJc9b3UiloMBJIYBkl00 +NKI8JU4jSlcerR58DqsTHIELiX6a+RJLe3/iR2/5Gru+CmmWJ68jQu872WCgh6Ms +o8TzhyGx74ETmdKn5CdtylsnKMa9heW3tBLFAbNCgKc= +-----END EC PRIVATE KEY----- diff --git a/kms/softkms/testdata/pub.pem b/kms/softkms/testdata/pub.pem new file mode 100644 index 00000000..e31e583e --- /dev/null +++ b/kms/softkms/testdata/pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1 +veT13hakPZF9YzaNVZgujqK3d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w== +-----END PUBLIC KEY-----