From dc6ffb7670cec32cfb5b1a75c1810ee80c9b0a7b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 24 Sep 2019 19:12:13 -0700 Subject: [PATCH 001/163] Add initial implementation of ssh config. --- api/ssh.go | 47 ++++++++++++++++++++++++++++++++++++++++++ authority/authority.go | 17 +++++++++++---- authority/ssh.go | 47 ++++++++++++++++++++++++------------------ 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 7bcae7cf..456f239a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "golang.org/x/crypto/ssh" ) @@ -15,6 +16,7 @@ import ( type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) + SSHConfig() (*authority.SSHConfiguration, error) } // SignSSHRequest is the request body of an SSH certificate request. @@ -34,6 +36,13 @@ type SignSSHResponse struct { AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` } +// SSHConfigResponse represents the response object that returns the SSH user +// and host keys. +type SSHConfigResponse struct { + UserKey *SSHPublicKey `json:"userKey,omitempty"` + HostKey *SSHPublicKey `json:"hostKey,omitempty"` +} + // SSHCertificate represents the response SSH certificate. type SSHCertificate struct { *ssh.Certificate `json:"omitempty"` @@ -49,6 +58,21 @@ func (c SSHCertificate) MarshalJSON() ([]byte, error) { return []byte(`"` + s + `"`), nil } +// SSHPublicKey represents a public key in a response object. +type SSHPublicKey struct { + ssh.PublicKey +} + +// MarshalJSON implements the json.Marshaler interface. Returns a quoted, +// base64 encoded, openssh wire format version of the public key. +func (p *SSHPublicKey) MarshalJSON() ([]byte, error) { + if p == nil || p.PublicKey == nil { + return []byte("null"), nil + } + s := base64.StdEncoding.EncodeToString(p.PublicKey.Marshal()) + return []byte(`"` + s + `"`), nil +} + // UnmarshalJSON implements the json.Unmarshaler interface. The certificate is // expected to be a quoted, base64 encoded, openssh wire formatted block of bytes. func (c *SSHCertificate) UnmarshalJSON(data []byte) error { @@ -157,3 +181,26 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { AddUserCertificate: addUserCertificate, }) } + +// SSHConfig is an HTTP handler that returns the SSH public keys for user and +// host certificates. +func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { + config, err := h.Authority.SSHConfig() + if err != nil { + WriteError(w, NotFound(err)) + return + } + + var host, user *SSHPublicKey + if config.HostKey != nil { + host = &SSHPublicKey{config.HostKey} + } + if config.UserKey != nil { + user = &SSHPublicKey{config.UserKey} + } + + JSON(w, &SSHConfigResponse{ + HostKey: host, + UserKey: user, + }) +} diff --git a/authority/authority.go b/authority/authority.go index 03fd1a99..1ddcf13c 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -13,6 +13,7 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/x509util" + "golang.org/x/crypto/ssh" ) const ( @@ -24,8 +25,8 @@ type Authority struct { config *Config rootX509Certs []*x509.Certificate intermediateIdentity *x509util.Identity - sshCAUserCertSignKey crypto.Signer - sshCAHostCertSignKey crypto.Signer + sshCAUserCertSignKey ssh.Signer + sshCAHostCertSignKey ssh.Signer certificates *sync.Map startTime time.Time provisioners *provisioner.Collection @@ -125,16 +126,24 @@ func (a *Authority) init() error { // Decrypt and load SSH keys if a.config.SSH != nil { if a.config.SSH.HostKey != "" { - a.sshCAHostCertSignKey, err = parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) + signer, err := parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) if err != nil { return err } + a.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(signer) + if err != nil { + return errors.Wrap(err, "error creating ssh signer") + } } if a.config.SSH.UserKey != "" { - a.sshCAUserCertSignKey, err = parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) + signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) if err != nil { return err } + a.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(signer) + if err != nil { + return errors.Wrap(err, "error creating ssh signer") + } } } diff --git a/authority/ssh.go b/authority/ssh.go index 2f69b3ca..dc7ebe0c 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -24,6 +24,30 @@ const ( SSHAddUserCommand = "sudo useradd -m ; nc -q0 localhost 22" ) +// SSHConfiguration is the return type for SSHConfig. +type SSHConfiguration struct { + UserKey ssh.PublicKey + HostKey ssh.PublicKey +} + +// SSHConfig returns the SSH User and Host public keys. +func (a *Authority) SSHConfig() (*SSHConfiguration, error) { + var config SSHConfiguration + if a.sshCAUserCertSignKey != nil { + config.UserKey = a.sshCAUserCertSignKey.PublicKey() + } + if a.sshCAHostCertSignKey != nil { + config.HostKey = a.sshCAHostCertSignKey.PublicKey() + } + if config.UserKey == nil && config.HostKey == nil { + return nil, &apiError{ + err: errors.New("sshConfig: ssh is not configured"), + code: http.StatusNotFound, + } + } + return &config, nil +} + // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var mods []provisioner.SSHCertificateModifier @@ -95,12 +119,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign code: http.StatusNotImplemented, } } - if signer, err = ssh.NewSignerFromSigner(a.sshCAUserCertSignKey); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error creating signer"), - code: http.StatusInternalServerError, - } - } + signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { return nil, &apiError{ @@ -108,12 +127,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign code: http.StatusNotImplemented, } } - if signer, err = ssh.NewSignerFromSigner(a.sshCAHostCertSignKey); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error creating signer"), - code: http.StatusInternalServerError, - } - } + signer = a.sshCAHostCertSignKey default: return nil, &apiError{ err: errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType), @@ -180,14 +194,7 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) } } - signer, err := ssh.NewSignerFromSigner(a.sshCAUserCertSignKey) - if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSHProxy: error creating signer"), - code: http.StatusInternalServerError, - } - } - + signer := a.sshCAUserCertSignKey principal := subject.ValidPrincipals[0] addUserPrincipal := a.getAddUserPrincipal() From fe3149cf5289dbfedb9c963828e5e2b945fe2c5b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 26 Sep 2019 13:22:07 -0700 Subject: [PATCH 002/163] Add endpoint to return the SSH public keys. Related to smallstep/ca-component#195 --- api/api.go | 5 ++- api/api_test.go | 8 ++++ api/ssh.go | 100 ++++++++++++++++++++++++++++------------------ api/ssh_test.go | 61 ++++++++++++++++++++++++++-- authority/ssh.go | 18 ++++----- ca/client.go | 20 +++++++++- ca/client_test.go | 73 +++++++++++++++++++++++++++++++++ 7 files changed, 232 insertions(+), 53 deletions(-) diff --git a/api/api.go b/api/api.go index d1ff7d1d..68c843bf 100644 --- a/api/api.go +++ b/api/api.go @@ -250,9 +250,12 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) r.MethodFunc("GET", "/roots", h.Roots) r.MethodFunc("GET", "/federation", h.Federation) + // SSH CA + r.MethodFunc("GET", "/ssh/sign", h.SignSSH) + r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) + // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) - // SSH CA r.MethodFunc("POST", "/sign-ssh", h.SignSSH) } diff --git a/api/api_test.go b/api/api_test.go index a253f4cd..3fc93589 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -512,6 +512,7 @@ type mockAuthority struct { getEncryptedKey func(kid string) (string, error) getRoots func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error) + getSSHKeys func() (*authority.SSHKeys, error) } // TODO: remove once Authorize is deprecated. @@ -617,6 +618,13 @@ func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { return m.ret1.([]*x509.Certificate), m.err } +func (m *mockAuthority) GetSSHKeys() (*authority.SSHKeys, error) { + if m.getSSHKeys != nil { + return m.getSSHKeys() + } + return m.ret1.(*authority.SSHKeys), m.err +} + func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority diff --git a/api/ssh.go b/api/ssh.go index 456f239a..edc49a10 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -16,7 +16,7 @@ import ( type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) - SSHConfig() (*authority.SSHConfiguration, error) + GetSSHKeys() (*authority.SSHKeys, error) } // SignSSHRequest is the request body of an SSH certificate request. @@ -30,15 +30,29 @@ type SignSSHRequest struct { AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` } +// Validate validates the SignSSHRequest. +func (s *SignSSHRequest) Validate() error { + switch { + case s.CertType != "" && s.CertType != provisioner.SSHUserCert && s.CertType != provisioner.SSHHostCert: + return errors.Errorf("unknown certType %s", s.CertType) + case len(s.PublicKey) == 0: + return errors.New("missing or empty publicKey") + case len(s.OTT) == 0: + return errors.New("missing or empty ott") + default: + return nil + } +} + // SignSSHResponse is the response object that returns the SSH certificate. type SignSSHResponse struct { Certificate SSHCertificate `json:"crt"` AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` } -// SSHConfigResponse represents the response object that returns the SSH user -// and host keys. -type SSHConfigResponse struct { +// SSHKeysResponse represents the response object that returns the SSH user and +// host keys. +type SSHKeysResponse struct { UserKey *SSHPublicKey `json:"userKey,omitempty"` HostKey *SSHPublicKey `json:"hostKey,omitempty"` } @@ -58,21 +72,6 @@ func (c SSHCertificate) MarshalJSON() ([]byte, error) { return []byte(`"` + s + `"`), nil } -// SSHPublicKey represents a public key in a response object. -type SSHPublicKey struct { - ssh.PublicKey -} - -// MarshalJSON implements the json.Marshaler interface. Returns a quoted, -// base64 encoded, openssh wire format version of the public key. -func (p *SSHPublicKey) MarshalJSON() ([]byte, error) { - if p == nil || p.PublicKey == nil { - return []byte("null"), nil - } - s := base64.StdEncoding.EncodeToString(p.PublicKey.Marshal()) - return []byte(`"` + s + `"`), nil -} - // UnmarshalJSON implements the json.Unmarshaler interface. The certificate is // expected to be a quoted, base64 encoded, openssh wire formatted block of bytes. func (c *SSHCertificate) UnmarshalJSON(data []byte) error { @@ -100,18 +99,43 @@ func (c *SSHCertificate) UnmarshalJSON(data []byte) error { return nil } -// Validate validates the SignSSHRequest. -func (s *SignSSHRequest) Validate() error { - switch { - case s.CertType != "" && s.CertType != provisioner.SSHUserCert && s.CertType != provisioner.SSHHostCert: - return errors.Errorf("unknown certType %s", s.CertType) - case len(s.PublicKey) == 0: - return errors.New("missing or empty publicKey") - case len(s.OTT) == 0: - return errors.New("missing or empty ott") - default: +// SSHPublicKey represents a public key in a response object. +type SSHPublicKey struct { + ssh.PublicKey +} + +// MarshalJSON implements the json.Marshaler interface. Returns a quoted, +// base64 encoded, openssh wire format version of the public key. +func (p *SSHPublicKey) MarshalJSON() ([]byte, error) { + if p == nil || p.PublicKey == nil { + return []byte("null"), nil + } + s := base64.StdEncoding.EncodeToString(p.PublicKey.Marshal()) + return []byte(`"` + s + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. The public key is +// expected to be a quoted, base64 encoded, openssh wire formatted block of +// bytes. +func (p *SSHPublicKey) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return errors.Wrap(err, "error decoding ssh public key") + } + if s == "" { + p.PublicKey = nil return nil } + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return errors.Wrap(err, "error decoding ssh public key") + } + pub, err := ssh.ParsePublicKey(data) + if err != nil { + return errors.Wrap(err, "error parsing ssh public key") + } + p.PublicKey = pub + return nil } // SignSSH is an HTTP handler that reads an SignSSHRequest with a one-time-token @@ -182,24 +206,24 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { }) } -// SSHConfig is an HTTP handler that returns the SSH public keys for user and -// host certificates. -func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { - config, err := h.Authority.SSHConfig() +// SSHKeys is an HTTP handler that returns the SSH public keys for user and host +// certificates. +func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { + keys, err := h.Authority.GetSSHKeys() if err != nil { WriteError(w, NotFound(err)) return } var host, user *SSHPublicKey - if config.HostKey != nil { - host = &SSHPublicKey{config.HostKey} + if keys.HostKey != nil { + host = &SSHPublicKey{PublicKey: keys.HostKey} } - if config.UserKey != nil { - user = &SSHPublicKey{config.UserKey} + if keys.UserKey != nil { + user = &SSHPublicKey{PublicKey: keys.UserKey} } - JSON(w, &SSHConfigResponse{ + JSON(w, &SSHKeysResponse{ HostKey: host, UserKey: user, }) diff --git a/api/ssh_test.go b/api/ssh_test.go index 9deb5c88..55a0db90 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" "golang.org/x/crypto/ssh" @@ -296,23 +297,75 @@ func Test_caHandler_SignSSH(t *testing.T) { }, }).(*caHandler) - req := httptest.NewRequest("POST", "http://example.com/sign-ssh", bytes.NewReader(tt.req)) + req := httptest.NewRequest("POST", "http://example.com/ssh/sign", bytes.NewReader(tt.req)) w := httptest.NewRecorder() h.SignSSH(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { - t.Errorf("caHandler.Root StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + t.Errorf("caHandler.SignSSH StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) } body, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - t.Errorf("caHandler.Root unexpected error = %v", err) + t.Errorf("caHandler.SignSSH unexpected error = %v", err) } if tt.statusCode < http.StatusBadRequest { if !bytes.Equal(bytes.TrimSpace(body), tt.body) { - t.Errorf("caHandler.Root Body = %s, wants %s", body, tt.body) + t.Errorf("caHandler.SignSSH Body = %s, wants %s", body, tt.body) + } + } + }) + } +} + +func Test_caHandler_SSHKeys(t *testing.T) { + user, err := ssh.NewPublicKey(sshUserKey.Public()) + assert.FatalError(t, err) + userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) + + host, err := ssh.NewPublicKey(sshHostKey.Public()) + assert.FatalError(t, err) + hostB64 := base64.StdEncoding.EncodeToString(host.Marshal()) + + tests := []struct { + name string + keys *authority.SSHKeys + keysErr error + body []byte + statusCode int + }{ + {"ok", &authority.SSHKeys{HostKey: host, UserKey: user}, nil, []byte(fmt.Sprintf(`{"userKey":"%s","hostKey":"%s"}`, userB64, hostB64)), http.StatusOK}, + {"user", &authority.SSHKeys{UserKey: user}, nil, []byte(fmt.Sprintf(`{"userKey":"%s"}`, userB64)), http.StatusOK}, + {"host", &authority.SSHKeys{HostKey: host}, nil, []byte(fmt.Sprintf(`{"hostKey":"%s"}`, hostB64)), http.StatusOK}, + {"error", nil, fmt.Errorf("an error"), nil, http.StatusNotFound}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ + getSSHKeys: func() (*authority.SSHKeys, error) { + return tt.keys, tt.keysErr + }, + }).(*caHandler) + + req := httptest.NewRequest("GET", "http://example.com/ssh/keys", http.NoBody) + w := httptest.NewRecorder() + h.SSHKeys(logging.NewResponseLogger(w), req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.SSHKeys StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.SSHKeys unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), tt.body) { + t.Errorf("caHandler.SSHKeys Body = %s, wants %s", body, tt.body) } } }) diff --git a/authority/ssh.go b/authority/ssh.go index dc7ebe0c..c83ce88b 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -24,28 +24,28 @@ const ( SSHAddUserCommand = "sudo useradd -m ; nc -q0 localhost 22" ) -// SSHConfiguration is the return type for SSHConfig. -type SSHConfiguration struct { +// SSHKeys represents the SSH User and Host public keys. +type SSHKeys struct { UserKey ssh.PublicKey HostKey ssh.PublicKey } -// SSHConfig returns the SSH User and Host public keys. -func (a *Authority) SSHConfig() (*SSHConfiguration, error) { - var config SSHConfiguration +// GetSSHKeys returns the SSH User and Host public keys. +func (a *Authority) GetSSHKeys() (*SSHKeys, error) { + var keys SSHKeys if a.sshCAUserCertSignKey != nil { - config.UserKey = a.sshCAUserCertSignKey.PublicKey() + keys.UserKey = a.sshCAUserCertSignKey.PublicKey() } if a.sshCAHostCertSignKey != nil { - config.HostKey = a.sshCAHostCertSignKey.PublicKey() + keys.HostKey = a.sshCAHostCertSignKey.PublicKey() } - if config.UserKey == nil && config.HostKey == nil { + if keys.UserKey == nil && keys.HostKey == nil { return nil, &apiError{ err: errors.New("sshConfig: ssh is not configured"), code: http.StatusNotFound, } } - return &config, nil + return &keys, nil } // SignSSH creates a signed SSH certificate with the given public key and options. diff --git a/ca/client.go b/ca/client.go index 826bee7f..fc964b89 100644 --- a/ca/client.go +++ b/ca/client.go @@ -380,7 +380,7 @@ func (c *Client) SignSSH(req *api.SignSSHRequest) (*api.SignSSHResponse, error) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } - u := c.endpoint.ResolveReference(&url.URL{Path: "/sign-ssh"}) + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/sign"}) resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) @@ -527,6 +527,24 @@ func (c *Client) Federation() (*api.FederationResponse, error) { return &federation, nil } +// SSHKeys performs the get ssh keys request to the CA and returns the +// api.SSHKeysResponse struct. +func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"}) + resp, err := c.client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var keys api.SSHKeysResponse + if err := readJSON(resp.Body, &keys); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &keys, nil +} + // RootFingerprint is a helper method that returns the current root fingerprint. // It does an health connection and gets the fingerprint from the TLS verified // chains. diff --git a/ca/client_test.go b/ca/client_test.go index dd9f7228..58ee36d1 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -2,6 +2,9 @@ package ca import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" @@ -17,6 +20,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/crypto/x509util" + "golang.org/x/crypto/ssh" ) const ( @@ -96,6 +100,14 @@ DCbKzWTW8lqVdp9Kyf7XEhhc2R8C5w== -----END CERTIFICATE REQUEST-----` ) +func mustKey() *ecdsa.PrivateKey { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + return priv +} + func parseCertificate(data string) *x509.Certificate { block, _ := pem.Decode([]byte(data)) if block == nil { @@ -710,6 +722,67 @@ func TestClient_Federation(t *testing.T) { } } +func TestClient_SSHKeys(t *testing.T) { + key, err := ssh.NewPublicKey(mustKey().Public()) + if err != nil { + t.Fatal(err) + } + + ok := &api.SSHKeysResponse{ + HostKey: &api.SSHPublicKey{PublicKey: key}, + UserKey: &api.SSHPublicKey{PublicKey: key}, + } + notFound := api.NotFound(fmt.Errorf("Not Found")) + + tests := []struct { + name string + response interface{} + responseCode int + wantErr bool + }{ + {"ok", ok, 200, false}, + {"not found", notFound, 404, true}, + } + + srv := httptest.NewServer(nil) + defer srv.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport)) + if err != nil { + t.Errorf("NewClient() error = %v", err) + return + } + + srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + api.JSONStatus(w, tt.response, tt.responseCode) + }) + + got, err := c.SSHKeys() + if (err != nil) != tt.wantErr { + fmt.Printf("%+v", err) + t.Errorf("Client.SSHKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + + switch { + case err != nil: + if got != nil { + t.Errorf("Client.SSHKeys() = %v, want nil", got) + } + if !reflect.DeepEqual(err, tt.response) { + t.Errorf("Client.SSHKeys() error = %v, want %v", err, tt.response) + } + default: + if !reflect.DeepEqual(got, tt.response) { + t.Errorf("Client.SSHKeys() = %v, want %v", got, tt.response) + } + } + }) + } +} + func Test_parseEndpoint(t *testing.T) { expected1 := &url.URL{Scheme: "https", Host: "ca.smallstep.com"} expected2 := &url.URL{Scheme: "https", Host: "ca.smallstep.com", Path: "/1.0/sign"} From 9b40ce2e3f7aa5d22e75e50ac6855f96182ade4a Mon Sep 17 00:00:00 2001 From: Alan Christopher Thomas Date: Tue, 10 Sep 2019 16:29:03 -0600 Subject: [PATCH 003/163] Rough wiring for basics of connecting to onboarding flow --- cmd/step-ca/main.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 22b7905d..3e47d52f 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -35,49 +35,33 @@ func init() { // appHelpTemplate contains the modified template for the main app var appHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} - ## USAGE {{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .Commands}} {{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Description}} - ## DESCRIPTION {{.Description}}{{end}}{{if .VisibleCommands}} - ## COMMANDS - {{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} ||| |---|---|{{range .VisibleCommands}} | **{{join .Names ", "}}** | {{.Usage}} |{{end}} {{end}}{{if .VisibleFlags}}{{end}} - ## OPTIONS - {{range $index, $option := .VisibleFlags}}{{if $index}} {{end}}{{$option}} {{end}}{{end}}{{if .Copyright}}{{if len .Authors}} - ## AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} {{end}}{{$author}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} - ## ONLINE - This documentation is available online at https://smallstep.com/docs/certificates - ## VERSION - {{.Version}}{{end}}{{end}} - ## COPYRIGHT - {{.Copyright}} - ## FEEDBACK ` + html.UnescapeString("&#"+strconv.Itoa(128525)+";") + " " + html.UnescapeString("&#"+strconv.Itoa(127867)+";") + ` - The **step-ca** utility is not instrumented for usage statistics. It does not phone home. But your feedback is extremely valuable. Any information you can provide regarding how you’re using **step-ca** helps. Please send us a sentence or two, good or bad: **feedback@smallstep.com** or join https://gitter.im/smallstep/community. @@ -105,30 +89,21 @@ func main() { app.UsageText = `**step-ca** [**--password-file**=] [**--help**] [**--version**]` app.Description = `**step-ca** runs the Step Online Certificate Authority (Step CA) using the given configuration. - See the README.md for more detailed configuration documentation. - ## POSITIONAL ARGUMENTS - : File that configures the operation of the Step CA; this file is generated when you initialize the Step CA using 'step ca init' - ## EXIT CODES - This command will run indefinitely on success and return \>0 if any error occurs. - ## EXAMPLES - These examples assume that you have already initialized your PKI by running 'step ca init'. If you have not completed this step please see the 'Getting Started' section of the README. - Run the Step CA and prompt for password: ''' $ step-ca $STEPPATH/config/ca.json ''' - Run the Step CA and read the password from a file - this is useful for automating deployment: ''' From c6a577235601cebc30080b077bb6d42746a3055b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 27 Sep 2019 19:05:53 -0700 Subject: [PATCH 004/163] Fix tests. --- authority/ssh_test.go | 73 ++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 37a9a8f7..ff0bb23c 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -1,7 +1,6 @@ package authority import ( - "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -81,6 +80,8 @@ func TestAuthority_SignSSH(t *testing.T) { assert.FatalError(t, err) signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.FatalError(t, err) + signer, err := ssh.NewSignerFromKey(signKey) + assert.FatalError(t, err) userOptions := sshTestModifier{ CertType: ssh.UserCert, @@ -92,8 +93,8 @@ func TestAuthority_SignSSH(t *testing.T) { now := time.Now() type fields struct { - sshCAUserCertSignKey crypto.Signer - sshCAHostCertSignKey crypto.Signer + sshCAUserCertSignKey ssh.Signer + sshCAHostCertSignKey ssh.Signer } type args struct { key ssh.PublicKey @@ -113,27 +114,27 @@ func TestAuthority_SignSSH(t *testing.T) { want want wantErr bool }{ - {"ok-user", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions}}, want{CertType: ssh.UserCert}, false}, - {"ok-host", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{hostOptions}}, want{CertType: ssh.HostCert}, false}, - {"ok-opts-type-user", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-type-host", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert}, false}, - {"ok-opts-principals", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, - {"ok-opts-principals", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, - {"ok-opts-valid-after", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false}, - {"ok-opts-valid-before", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false}, - {"ok-cert-validator", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-cert-modifier", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-validator", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, - {"ok-opts-modifier", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, - {"fail-opts-type", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{CertType: "foo"}, []provisioner.SignOption{}}, want{}, true}, - {"fail-cert-validator", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("an error")}}, want{}, true}, - {"fail-cert-modifier", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("an error")}}, want{}, true}, - {"fail-opts-validator", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("an error")}}, want{}, true}, - {"fail-opts-modifier", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("an error")}}, want{}, true}, - {"fail-bad-sign-options", fields{signKey, signKey}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, "wrong type"}}, want{}, true}, - {"fail-no-user-key", fields{nil, signKey}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{}, true}, - {"fail-no-host-key", fields{signKey, nil}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{}, true}, - {"fail-bad-type", fields{signKey, nil}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{sshTestModifier{CertType: 0}}}, want{}, true}, + {"ok-user", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions}}, want{CertType: ssh.UserCert}, false}, + {"ok-host", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{hostOptions}}, want{CertType: ssh.HostCert}, false}, + {"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert}, false}, + {"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, + {"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, + {"ok-opts-valid-after", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false}, + {"ok-opts-valid-before", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false}, + {"ok-cert-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-cert-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, + {"ok-opts-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, + {"fail-opts-type", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "foo"}, []provisioner.SignOption{}}, want{}, true}, + {"fail-cert-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("an error")}}, want{}, true}, + {"fail-cert-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("an error")}}, want{}, true}, + {"fail-opts-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("an error")}}, want{}, true}, + {"fail-opts-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("an error")}}, want{}, true}, + {"fail-bad-sign-options", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, "wrong type"}}, want{}, true}, + {"fail-no-user-key", fields{nil, signer}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{}, true}, + {"fail-no-host-key", fields{signer, nil}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{}, true}, + {"fail-bad-type", fields{signer, nil}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{sshTestModifier{CertType: 0}}}, want{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -168,10 +169,12 @@ func TestAuthority_SignSSHAddUser(t *testing.T) { assert.FatalError(t, err) signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.FatalError(t, err) + signer, err := ssh.NewSignerFromKey(signKey) + assert.FatalError(t, err) type fields struct { - sshCAUserCertSignKey crypto.Signer - sshCAHostCertSignKey crypto.Signer + sshCAUserCertSignKey ssh.Signer + sshCAHostCertSignKey ssh.Signer addUserPrincipal string addUserCommand string } @@ -209,15 +212,15 @@ func TestAuthority_SignSSHAddUser(t *testing.T) { want want wantErr bool }{ - {"ok", fields{signKey, signKey, "", ""}, args{pub, validCert}, validWant, false}, - {"ok-no-host-key", fields{signKey, nil, "", ""}, args{pub, validCert}, validWant, false}, - {"ok-custom-principal", fields{signKey, signKey, "my-principal", ""}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"user"}}}, want{CertType: ssh.UserCert, Principals: []string{"my-principal"}, ForceCommand: "sudo useradd -m user; nc -q0 localhost 22"}, false}, - {"ok-custom-command", fields{signKey, signKey, "", "foo "}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"user"}}}, want{CertType: ssh.UserCert, Principals: []string{"provisioner"}, ForceCommand: "foo user user"}, false}, - {"ok-custom-principal-and-command", fields{signKey, signKey, "my-principal", "foo "}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"user"}}}, want{CertType: ssh.UserCert, Principals: []string{"my-principal"}, ForceCommand: "foo user user"}, false}, - {"fail-no-user-key", fields{nil, signKey, "", ""}, args{pub, validCert}, want{}, true}, - {"fail-no-user-cert", fields{signKey, signKey, "", ""}, args{pub, &ssh.Certificate{CertType: ssh.HostCert, ValidPrincipals: []string{"foo"}}}, want{}, true}, - {"fail-no-principals", fields{signKey, signKey, "", ""}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{}}}, want{}, true}, - {"fail-many-principals", fields{signKey, signKey, "", ""}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"foo", "bar"}}}, want{}, true}, + {"ok", fields{signer, signer, "", ""}, args{pub, validCert}, validWant, false}, + {"ok-no-host-key", fields{signer, nil, "", ""}, args{pub, validCert}, validWant, false}, + {"ok-custom-principal", fields{signer, signer, "my-principal", ""}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"user"}}}, want{CertType: ssh.UserCert, Principals: []string{"my-principal"}, ForceCommand: "sudo useradd -m user; nc -q0 localhost 22"}, false}, + {"ok-custom-command", fields{signer, signer, "", "foo "}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"user"}}}, want{CertType: ssh.UserCert, Principals: []string{"provisioner"}, ForceCommand: "foo user user"}, false}, + {"ok-custom-principal-and-command", fields{signer, signer, "my-principal", "foo "}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"user"}}}, want{CertType: ssh.UserCert, Principals: []string{"my-principal"}, ForceCommand: "foo user user"}, false}, + {"fail-no-user-key", fields{nil, signer, "", ""}, args{pub, validCert}, want{}, true}, + {"fail-no-user-cert", fields{signer, signer, "", ""}, args{pub, &ssh.Certificate{CertType: ssh.HostCert, ValidPrincipals: []string{"foo"}}}, want{}, true}, + {"fail-no-principals", fields{signer, signer, "", ""}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{}}}, want{}, true}, + {"fail-many-principals", fields{signer, signer, "", ""}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{"foo", "bar"}}}, want{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 083e203c464d23531a309e92c6e41a2d6dbe245d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 30 Sep 2019 15:10:23 -0700 Subject: [PATCH 005/163] Fix HTTP method for /ssh/sign --- api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 68c843bf..4cebfce1 100644 --- a/api/api.go +++ b/api/api.go @@ -251,7 +251,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("GET", "/roots", h.Roots) r.MethodFunc("GET", "/federation", h.Federation) // SSH CA - r.MethodFunc("GET", "/ssh/sign", h.SignSSH) + r.MethodFunc("POST", "/ssh/sign", h.SignSSH) r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) // For compatibility with old code: From 7b8bb6deb4998dc51924ccea3f839577216b79ee Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 3 Oct 2019 19:03:38 -0700 Subject: [PATCH 006/163] Add initial support for ssh config. Related to smallstep/cli#170 --- api/api.go | 2 + api/api_test.go | 9 +++ api/ssh.go | 63 +++++++++++++++ authority/authority.go | 19 +++++ authority/config.go | 34 +++++--- authority/ssh.go | 42 +++++++++- ca/client.go | 22 +++++ templates/templates.go | 180 +++++++++++++++++++++++++++++++++++++++++ templates/values.go | 15 ++++ 9 files changed, 372 insertions(+), 14 deletions(-) create mode 100644 templates/templates.go create mode 100644 templates/values.go diff --git a/api/api.go b/api/api.go index 4cebfce1..6029557c 100644 --- a/api/api.go +++ b/api/api.go @@ -253,6 +253,8 @@ func (h *caHandler) Route(r Router) { // SSH CA r.MethodFunc("POST", "/ssh/sign", h.SignSSH) r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) + r.MethodFunc("POST", "/ssh/config", h.SSHConfig) + r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) diff --git a/api/api_test.go b/api/api_test.go index 3fc93589..50916464 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -29,6 +29,7 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/jose" "golang.org/x/crypto/ssh" @@ -513,6 +514,7 @@ type mockAuthority struct { getRoots func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error) getSSHKeys func() (*authority.SSHKeys, error) + getSSHConfig func(typ string) ([]templates.Output, error) } // TODO: remove once Authorize is deprecated. @@ -625,6 +627,13 @@ func (m *mockAuthority) GetSSHKeys() (*authority.SSHKeys, error) { return m.ret1.(*authority.SSHKeys), m.err } +func (m *mockAuthority) GetSSHConfig(typ string) ([]templates.Output, error) { + if m.getSSHConfig != nil { + return m.getSSHConfig(typ) + } + return m.ret1.([]templates.Output), m.err +} + func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority diff --git a/api/ssh.go b/api/ssh.go index edc49a10..14c3b7c9 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/templates" "golang.org/x/crypto/ssh" ) @@ -17,6 +18,7 @@ type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) GetSSHKeys() (*authority.SSHKeys, error) + GetSSHConfig(typ string) ([]templates.Output, error) } // SignSSHRequest is the request body of an SSH certificate request. @@ -138,6 +140,34 @@ func (p *SSHPublicKey) UnmarshalJSON(data []byte) error { return nil } +// Template represents the output of a template. +type Template = templates.Output + +// SSHConfigRequest is the request body used to get the SSH configuration +// templates. +type SSHConfigRequest struct { + Type string `json:"type"` +} + +// Validate checks the values of the SSHConfigurationRequest. +func (r *SSHConfigRequest) Validate() error { + switch r.Type { + case "": + r.Type = provisioner.SSHUserCert + return nil + case provisioner.SSHUserCert, provisioner.SSHHostCert: + return nil + default: + return errors.Errorf("unsupported type %s", r.Type) + } +} + +// SSHConfigResponse is the response that returns the rendered templates. +type SSHConfigResponse struct { + UserTemplates []Template `json:"userTemplates,omitempty"` + HostTemplates []Template `json:"hostTemplates,omitempty"` +} + // SignSSH is an HTTP handler that reads an SignSSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. @@ -228,3 +258,36 @@ func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { UserKey: user, }) } + +// SSHConfig is an HTTP handler that returns rendered templates for ssh clients +// and servers. +func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { + var body SSHConfigRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + if err := body.Validate(); err != nil { + WriteError(w, BadRequest(err)) + return + } + + ts, err := h.Authority.GetSSHConfig(body.Type) + if err != nil { + WriteError(w, InternalServerError(err)) + return + } + + var config SSHConfigResponse + switch body.Type { + case provisioner.SSHUserCert: + config.UserTemplates = ts + case provisioner.SSHHostCert: + config.UserTemplates = ts + default: + WriteError(w, InternalServerError(errors.New("it should hot get here"))) + return + } + + JSON(w, config) +} diff --git a/authority/authority.go b/authority/authority.go index 1ddcf13c..1e4c3466 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,6 +8,8 @@ import ( "sync" "time" + "github.com/smallstep/certificates/templates" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -154,6 +156,23 @@ func (a *Authority) init() error { } } + // Configure protected template variables: + if t := a.config.Templates; t != nil { + if t.Variables == nil { + t.Variables = make(map[string]interface{}) + } + var vars templates.Step + if a.config.SSH != nil { + if a.sshCAHostCertSignKey != nil { + vars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey() + } + if a.sshCAUserCertSignKey != nil { + vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey() + } + } + t.Variables["Step"] = vars + } + // JWT numeric dates are seconds. a.startTime = time.Now().Truncate(time.Second) // Set flag indicating that initialization has been completed, and should diff --git a/authority/config.go b/authority/config.go index 99fdf457..a53507dd 100644 --- a/authority/config.go +++ b/authority/config.go @@ -7,6 +7,8 @@ import ( "os" "time" + "github.com/smallstep/certificates/templates" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -46,19 +48,20 @@ var ( // Config represents the CA configuration and it's mapped to a JSON object. type Config struct { - Root multiString `json:"root"` - FederatedRoots []string `json:"federatedRoots"` - IntermediateCert string `json:"crt"` - IntermediateKey string `json:"key"` - Address string `json:"address"` - DNSNames []string `json:"dnsNames"` - SSH *SSHConfig `json:"ssh,omitempty"` - Logger json.RawMessage `json:"logger,omitempty"` - DB *db.Config `json:"db,omitempty"` - Monitoring json.RawMessage `json:"monitoring,omitempty"` - AuthorityConfig *AuthConfig `json:"authority,omitempty"` - TLS *tlsutil.TLSOptions `json:"tls,omitempty"` - Password string `json:"password,omitempty"` + Root multiString `json:"root"` + FederatedRoots []string `json:"federatedRoots"` + IntermediateCert string `json:"crt"` + IntermediateKey string `json:"key"` + Address string `json:"address"` + DNSNames []string `json:"dnsNames"` + SSH *SSHConfig `json:"ssh,omitempty"` + Logger json.RawMessage `json:"logger,omitempty"` + DB *db.Config `json:"db,omitempty"` + Monitoring json.RawMessage `json:"monitoring,omitempty"` + AuthorityConfig *AuthConfig `json:"authority,omitempty"` + TLS *tlsutil.TLSOptions `json:"tls,omitempty"` + Password string `json:"password,omitempty"` + Templates *templates.Templates `json:"templates,omitempty"` } // AuthConfig represents the configuration options for the authority. @@ -181,6 +184,11 @@ func (c *Config) Validate() error { c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation } + // Validate templates: nil is ok + if err := c.Templates.Validate(); err != nil { + return err + } + return c.AuthorityConfig.Validate(c.getAudiences()) } diff --git a/authority/ssh.go b/authority/ssh.go index c83ce88b..68b52c9d 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -6,6 +6,8 @@ import ( "net/http" "strings" + "github.com/smallstep/certificates/templates" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/crypto/randutil" @@ -41,13 +43,51 @@ func (a *Authority) GetSSHKeys() (*SSHKeys, error) { } if keys.UserKey == nil && keys.HostKey == nil { return nil, &apiError{ - err: errors.New("sshConfig: ssh is not configured"), + err: errors.New("getSSHKeys: ssh is not configured"), code: http.StatusNotFound, } } return &keys, nil } +// GetSSHConfig returns rendered templates for clients (user) or servers (host). +func (a *Authority) GetSSHConfig(typ string) ([]templates.Output, error) { + if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil { + return nil, &apiError{ + err: errors.New("getSSHConfig: ssh is not configured"), + code: http.StatusNotFound, + } + } + + var ts []templates.Template + switch typ { + case provisioner.SSHUserCert: + if a.config.Templates != nil && a.config.Templates.SSH != nil { + ts = a.config.Templates.SSH.User + } + case provisioner.SSHHostCert: + if a.config.Templates != nil && a.config.Templates.SSH != nil { + ts = a.config.Templates.SSH.Host + } + default: + return nil, &apiError{ + err: errors.Errorf("getSSHConfig: type %s is not valid", typ), + code: http.StatusBadRequest, + } + } + + // Render templates. + output := []templates.Output{} + for _, t := range ts { + o, err := t.Output(a.config.Templates.Variables) + if err != nil { + return nil, err + } + output = append(output, o) + } + return output, nil +} + // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var mods []provisioner.SSHCertificateModifier diff --git a/ca/client.go b/ca/client.go index fc964b89..bbce5ee8 100644 --- a/ca/client.go +++ b/ca/client.go @@ -545,6 +545,28 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { return &keys, nil } +// SSHConfig performs the POST request to the CA to get the ssh configuration +// templates. +func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/config"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var config api.SSHConfigResponse + if err := readJSON(resp.Body, &config); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &config, nil +} + // RootFingerprint is a helper method that returns the current root fingerprint. // It does an health connection and gets the fingerprint from the TLS verified // chains. diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 00000000..740ddc1c --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,180 @@ +package templates + +import ( + "bytes" + "fmt" + "io/ioutil" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/pkg/errors" +) + +// TemplateType defines how a template will be written in disk. +type TemplateType string + +const ( + // Snippet will mark a template as a part of a file. + Snippet TemplateType = "snippet" + // File will mark a templates as a full file. + File TemplateType = "file" +) + +// Output represents the text representation of a rendered template. +type Output struct { + Name string `json:"name"` + Type TemplateType `json:"type"` + Comment string `json:"comment"` + Path string `json:"path"` + Content []byte `json:"content"` +} + +// Templates is a collection of templates and variables. +type Templates struct { + SSH *SSHTemplates `json:"ssh,omitempty"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +// Validate returns an error if a template is not valid. +func (t *Templates) Validate() (err error) { + if t == nil { + return nil + } + + // Validate members + if err = t.SSH.Validate(); err != nil { + return + } + + // Do not allow "Step" + if t.Variables != nil { + if _, ok := t.Variables["Step"]; ok { + return errors.New("templates variables cannot contain 'step' as a property") + } + } + return nil +} + +// LoadAll preloads all templates in memory. It returns an error if an error is +// found parsing at least one template. +func LoadAll(t *Templates) (err error) { + if t.SSH != nil { + for _, tt := range t.SSH.User { + if err = tt.Load(); err != nil { + return err + } + } + for _, tt := range t.SSH.Host { + if err = tt.Load(); err != nil { + return err + } + } + } + return nil +} + +// SSHTemplates contains the templates defining ssh configuration files. +type SSHTemplates struct { + User []Template `json:"user"` + Host []Template `json:"host"` +} + +// Validate returns an error if a template is not valid. +func (t *SSHTemplates) Validate() (err error) { + if t == nil { + return nil + } + for _, tt := range t.User { + if err = tt.Validate(); err != nil { + return + } + } + for _, tt := range t.Host { + if err = tt.Validate(); err != nil { + return + } + } + return +} + +// Template represents on template file. +type Template struct { + *template.Template + Name string `json:"name"` + Type TemplateType `json:"type"` + TemplatePath string `json:"template"` + Path string `json:"path"` + Comment string `json:"comment"` +} + +// Validate returns an error if the template is not valid. +func (t *Template) Validate() error { + switch { + case t == nil: + return nil + case t.Name == "": + return errors.New("template name cannot be empty") + case t.TemplatePath == "": + return errors.New("template template cannot be empty") + case t.Path == "": + return errors.New("template path cannot be empty") + } + + // Defaults + if t.Type == "" { + t.Type = Snippet + } + if t.Comment == "" { + t.Comment = "#" + } + + return nil +} + +// Load loads the template in memory, returns an error if the parsing of the +// template fails. +func (t *Template) Load() error { + if t.Template == nil { + b, err := ioutil.ReadFile(t.TemplatePath) + if err != nil { + return errors.Wrapf(err, "error reading %s", t.TemplatePath) + } + tmpl, err := template.New(t.Name).Funcs(sprig.TxtFuncMap()).Parse(string(b)) + if err != nil { + return errors.Wrapf(err, "error parsing %s", t.TemplatePath) + } + t.Template = tmpl + } + return nil +} + +// Render executes the template with the given data and returns the rendered +// version. +func (t *Template) Render(data interface{}) ([]byte, error) { + if err := t.Load(); err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + fmt.Println(data) + if err := t.Execute(buf, data); err != nil { + return nil, errors.Wrapf(err, "error executing %s", t.TemplatePath) + } + return buf.Bytes(), nil +} + +// Output renders the template and returns a template.Output struct or an error. +func (t *Template) Output(data interface{}) (Output, error) { + b, err := t.Render(data) + if err != nil { + return Output{}, err + } + + return Output{ + Name: t.Name, + Type: t.Type, + Comment: t.Comment, + Path: t.Path, + Content: b, + }, nil +} diff --git a/templates/values.go b/templates/values.go new file mode 100644 index 00000000..995c2998 --- /dev/null +++ b/templates/values.go @@ -0,0 +1,15 @@ +package templates + +import ( + "golang.org/x/crypto/ssh" +) + +// Step represents the default variables available in the CA. +type Step struct { + SSH StepSSH +} + +type StepSSH struct { + HostKey ssh.PublicKey + UserKey ssh.PublicKey +} From caa2174efc1800eae3c8662f9178b46194136650 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 4 Oct 2019 17:08:42 -0700 Subject: [PATCH 007/163] Add support for user data in templates. --- api/api_test.go | 6 +++--- api/ssh.go | 7 ++++--- authority/authority.go | 6 +++--- authority/ssh.go | 22 +++++++++++++++++----- templates/templates.go | 17 +++++++++-------- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 50916464..b276bcf6 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -514,7 +514,7 @@ type mockAuthority struct { getRoots func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error) getSSHKeys func() (*authority.SSHKeys, error) - getSSHConfig func(typ string) ([]templates.Output, error) + getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) } // TODO: remove once Authorize is deprecated. @@ -627,9 +627,9 @@ func (m *mockAuthority) GetSSHKeys() (*authority.SSHKeys, error) { return m.ret1.(*authority.SSHKeys), m.err } -func (m *mockAuthority) GetSSHConfig(typ string) ([]templates.Output, error) { +func (m *mockAuthority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) { if m.getSSHConfig != nil { - return m.getSSHConfig(typ) + return m.getSSHConfig(typ, data) } return m.ret1.([]templates.Output), m.err } diff --git a/api/ssh.go b/api/ssh.go index 14c3b7c9..f8142356 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -18,7 +18,7 @@ type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) GetSSHKeys() (*authority.SSHKeys, error) - GetSSHConfig(typ string) ([]templates.Output, error) + GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) } // SignSSHRequest is the request body of an SSH certificate request. @@ -146,7 +146,8 @@ type Template = templates.Output // SSHConfigRequest is the request body used to get the SSH configuration // templates. type SSHConfigRequest struct { - Type string `json:"type"` + Type string `json:"type"` + Data map[string]string `json:"data"` } // Validate checks the values of the SSHConfigurationRequest. @@ -272,7 +273,7 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { return } - ts, err := h.Authority.GetSSHConfig(body.Type) + ts, err := h.Authority.GetSSHConfig(body.Type, body.Data) if err != nil { WriteError(w, InternalServerError(err)) return diff --git a/authority/authority.go b/authority/authority.go index 1e4c3466..b3f5fb94 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -158,8 +158,8 @@ func (a *Authority) init() error { // Configure protected template variables: if t := a.config.Templates; t != nil { - if t.Variables == nil { - t.Variables = make(map[string]interface{}) + if t.Data == nil { + t.Data = make(map[string]interface{}) } var vars templates.Step if a.config.SSH != nil { @@ -170,7 +170,7 @@ func (a *Authority) init() error { vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey() } } - t.Variables["Step"] = vars + t.Data["Step"] = vars } // JWT numeric dates are seconds. diff --git a/authority/ssh.go b/authority/ssh.go index 68b52c9d..22cdd0f4 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -6,10 +6,9 @@ import ( "net/http" "strings" - "github.com/smallstep/certificates/templates" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/randutil" "golang.org/x/crypto/ssh" ) @@ -51,7 +50,7 @@ func (a *Authority) GetSSHKeys() (*SSHKeys, error) { } // GetSSHConfig returns rendered templates for clients (user) or servers (host). -func (a *Authority) GetSSHConfig(typ string) ([]templates.Output, error) { +func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) { if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil { return nil, &apiError{ err: errors.New("getSSHConfig: ssh is not configured"), @@ -76,10 +75,23 @@ func (a *Authority) GetSSHConfig(typ string) ([]templates.Output, error) { } } - // Render templates. + // Merge user and default data + var mergedData map[string]interface{} + + if len(data) == 0 { + mergedData = a.config.Templates.Data + } else { + mergedData = make(map[string]interface{}, len(a.config.Templates.Data)+1) + mergedData["User"] = data + for k, v := range a.config.Templates.Data { + mergedData[k] = v + } + } + + // Render templates output := []templates.Output{} for _, t := range ts { - o, err := t.Output(a.config.Templates.Variables) + o, err := t.Output(mergedData) if err != nil { return nil, err } diff --git a/templates/templates.go b/templates/templates.go index 740ddc1c..1ff5cdf2 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -2,7 +2,6 @@ package templates import ( "bytes" - "fmt" "io/ioutil" "text/template" @@ -31,8 +30,8 @@ type Output struct { // Templates is a collection of templates and variables. type Templates struct { - SSH *SSHTemplates `json:"ssh,omitempty"` - Variables map[string]interface{} `json:"variables,omitempty"` + SSH *SSHTemplates `json:"ssh,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` } // Validate returns an error if a template is not valid. @@ -46,10 +45,13 @@ func (t *Templates) Validate() (err error) { return } - // Do not allow "Step" - if t.Variables != nil { - if _, ok := t.Variables["Step"]; ok { - return errors.New("templates variables cannot contain 'step' as a property") + // Do not allow "Step" and "User" + if t.Data != nil { + if _, ok := t.Data["Step"]; ok { + return errors.New("templates variables cannot contain 'Step' as a property") + } + if _, ok := t.Data["User"]; ok { + return errors.New("templates variables cannot contain 'User' as a property") } } return nil @@ -156,7 +158,6 @@ func (t *Template) Render(data interface{}) ([]byte, error) { } buf := new(bytes.Buffer) - fmt.Println(data) if err := t.Execute(buf, data); err != nil { return nil, errors.Wrapf(err, "error executing %s", t.TemplatePath) } From e84489775ba1cc9058ff01c2c3b5407faafb968a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Oct 2019 18:09:41 -0700 Subject: [PATCH 008/163] Add support for multiple ssh roots. Fixes #125 --- api/api.go | 1 + api/ssh.go | 50 ++++++++++++++++++------ authority/authority.go | 48 ++++++++++++++++++----- authority/config.go | 13 +++---- authority/ssh.go | 86 ++++++++++++++++++++++++++++++++++-------- ca/client.go | 20 +++++++++- 6 files changed, 173 insertions(+), 45 deletions(-) diff --git a/api/api.go b/api/api.go index 6029557c..47cc118f 100644 --- a/api/api.go +++ b/api/api.go @@ -253,6 +253,7 @@ func (h *caHandler) Route(r Router) { // SSH CA r.MethodFunc("POST", "/ssh/sign", h.SignSSH) r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) + r.MethodFunc("GET", "/ssh/federation", h.SSHFederatedKeys) r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) diff --git a/api/ssh.go b/api/ssh.go index f8142356..8d0b421d 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -18,6 +18,7 @@ type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) GetSSHKeys() (*authority.SSHKeys, error) + GetSSHFederatedKeys() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) } @@ -55,8 +56,8 @@ type SignSSHResponse struct { // SSHKeysResponse represents the response object that returns the SSH user and // host keys. type SSHKeysResponse struct { - UserKey *SSHPublicKey `json:"userKey,omitempty"` - HostKey *SSHPublicKey `json:"hostKey,omitempty"` + UserKeys []SSHPublicKey `json:"userKey,omitempty"` + HostKeys []SSHPublicKey `json:"hostKey,omitempty"` } // SSHCertificate represents the response SSH certificate. @@ -241,23 +242,50 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { // certificates. func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHKeys() + if err != nil { + WriteError(w, InternalServerError(err)) + return + } + + if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { + WriteError(w, NotFound(errors.New("no keys found"))) + return + } + + resp := new(SSHKeysResponse) + for _, k := range keys.HostKeys { + resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k}) + } + for _, k := range keys.UserKeys { + resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k}) + } + + JSON(w, resp) +} + +// SSHFederatedKeys is an HTTP handler that returns the federated SSH public +// keys for user and host certificates. +func (h *caHandler) SSHFederatedKeys(w http.ResponseWriter, r *http.Request) { + keys, err := h.Authority.GetSSHFederatedKeys() if err != nil { WriteError(w, NotFound(err)) return } - var host, user *SSHPublicKey - if keys.HostKey != nil { - host = &SSHPublicKey{PublicKey: keys.HostKey} + if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { + WriteError(w, NotFound(errors.New("no keys found"))) + return + } + + resp := new(SSHKeysResponse) + for _, k := range keys.HostKeys { + resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k}) } - if keys.UserKey != nil { - user = &SSHPublicKey{PublicKey: keys.UserKey} + for _, k := range keys.UserKeys { + resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k}) } - JSON(w, &SSHKeysResponse{ - HostKey: host, - UserKey: user, - }) + JSON(w, resp) } // SSHConfig is an HTTP handler that returns rendered templates for ssh clients diff --git a/authority/authority.go b/authority/authority.go index b3f5fb94..88c829c6 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -24,15 +24,19 @@ const ( // Authority implements the Certificate Authority internal interface. type Authority struct { - config *Config - rootX509Certs []*x509.Certificate - intermediateIdentity *x509util.Identity - sshCAUserCertSignKey ssh.Signer - sshCAHostCertSignKey ssh.Signer - certificates *sync.Map - startTime time.Time - provisioners *provisioner.Collection - db db.AuthDB + config *Config + rootX509Certs []*x509.Certificate + intermediateIdentity *x509util.Identity + 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 } @@ -136,6 +140,9 @@ func (a *Authority) init() error { if err != nil { return errors.Wrap(err, "error creating ssh signer") } + // Append public key to list of host certs + a.sshCAHostCerts = append(a.sshCAHostCerts, a.sshCAHostCertSignKey.PublicKey()) + a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, a.sshCAHostCertSignKey.PublicKey()) } if a.config.SSH.UserKey != "" { signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) @@ -146,6 +153,29 @@ func (a *Authority) init() error { if err != nil { return errors.Wrap(err, "error creating ssh signer") } + // Append public key to list of user certs + a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAHostCertSignKey.PublicKey()) + a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey()) + } + + // Append other public keys + for _, key := range a.config.SSH.Keys { + switch key.Type { + case provisioner.SSHHostCert: + if key.Federated { + a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, key.PublicKey()) + } else { + a.sshCAHostCerts = append(a.sshCAHostCerts, key.PublicKey()) + } + case provisioner.SSHUserCert: + if key.Federated { + a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, key.PublicKey()) + } else { + a.sshCAUserCerts = append(a.sshCAUserCerts, key.PublicKey()) + } + default: + return errors.Errorf("unsupported type %s", key.Type) + } } } diff --git a/authority/config.go b/authority/config.go index a53507dd..30343b5f 100644 --- a/authority/config.go +++ b/authority/config.go @@ -104,14 +104,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { return nil } -// SSHConfig contains the user and host keys. -type SSHConfig struct { - HostKey string `json:"hostKey"` - UserKey string `json:"userKey"` - AddUserPrincipal string `json:"addUserPrincipal"` - AddUserCommand string `json:"addUserCommand"` -} - // LoadConfiguration parses the given filename in JSON format and returns the // configuration struct. func LoadConfiguration(filename string) (*Config, error) { @@ -184,6 +176,11 @@ func (c *Config) Validate() error { c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation } + // Validate ssh: nil is ok + if err := c.SSH.Validate(); err != nil { + return err + } + // Validate templates: nil is ok if err := c.Templates.Validate(); err != nil { return err diff --git a/authority/ssh.go b/authority/ssh.go index 22cdd0f4..33f00cec 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -10,6 +10,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/randutil" + "github.com/smallstep/cli/jose" "golang.org/x/crypto/ssh" ) @@ -25,28 +26,81 @@ const ( SSHAddUserCommand = "sudo useradd -m ; nc -q0 localhost 22" ) +// SSHConfig contains the user and host keys. +type SSHConfig struct { + HostKey string `json:"hostKey"` + UserKey string `json:"userKey"` + Keys []*SSHPublicKey `json:"keys,omitempty"` + AddUserPrincipal string `json:"addUserPrincipal"` + AddUserCommand string `json:"addUserCommand"` +} + +// Validate checks the fields in SSHConfig. +func (c *SSHConfig) Validate() error { + if c == nil { + return nil + } + for _, k := range c.Keys { + if err := k.Validate(); err != nil { + return err + } + } + return nil +} + +// SSHPublicKey contains a public key used by federated CAs to keep old signing +// keys for this ca. +type SSHPublicKey struct { + Type string `json:"type"` + Federated bool `json:"federated"` + Key jose.JSONWebKey `json:"key"` + publicKey ssh.PublicKey +} + +// Validate checks the fields in SSHPublicKey. +func (k *SSHPublicKey) Validate() error { + switch { + case k.Type == "": + return errors.New("type cannot be empty") + case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert: + return errors.Errorf("invalid type %s, it must be user or host", k.Type) + case !k.Key.IsPublic(): + return errors.New("invalid key type, it must be a public key") + } + + key, err := ssh.NewPublicKey(k.Key.Key) + if err != nil { + return errors.Wrap(err, "error creating ssh key") + } + k.publicKey = key + return nil +} + +// PublicKey returns the ssh public key. +func (k *SSHPublicKey) PublicKey() ssh.PublicKey { + return k.publicKey +} + // SSHKeys represents the SSH User and Host public keys. type SSHKeys struct { - UserKey ssh.PublicKey - HostKey ssh.PublicKey + UserKeys []ssh.PublicKey + HostKeys []ssh.PublicKey } // GetSSHKeys returns the SSH User and Host public keys. func (a *Authority) GetSSHKeys() (*SSHKeys, error) { - var keys SSHKeys - if a.sshCAUserCertSignKey != nil { - keys.UserKey = a.sshCAUserCertSignKey.PublicKey() - } - if a.sshCAHostCertSignKey != nil { - keys.HostKey = a.sshCAHostCertSignKey.PublicKey() - } - if keys.UserKey == nil && keys.HostKey == nil { - return nil, &apiError{ - err: errors.New("getSSHKeys: ssh is not configured"), - code: http.StatusNotFound, - } - } - return &keys, nil + return &SSHKeys{ + HostKeys: a.sshCAHostCerts, + UserKeys: a.sshCAUserCerts, + }, nil +} + +// GetSSHFederatedKeys returns the public keys for federated SSH signers. +func (a *Authority) GetSSHFederatedKeys() (*SSHKeys, error) { + return &SSHKeys{ + HostKeys: a.sshCAHostFederatedCerts, + UserKeys: a.sshCAUserFederatedCerts, + }, nil } // GetSSHConfig returns rendered templates for clients (user) or servers (host). diff --git a/ca/client.go b/ca/client.go index bbce5ee8..45e1e7ce 100644 --- a/ca/client.go +++ b/ca/client.go @@ -527,7 +527,7 @@ func (c *Client) Federation() (*api.FederationResponse, error) { return &federation, nil } -// SSHKeys performs the get ssh keys request to the CA and returns the +// SSHKeys performs the get /ssh/keys request to the CA and returns the // api.SSHKeysResponse struct. func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"}) @@ -545,6 +545,24 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { return &keys, nil } +// SSHFederation performs the get /ssh/federation request to the CA and returns +// the api.SSHKeysResponse struct. +func (c *Client) SSHFederation() (*api.SSHKeysResponse, error) { + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"}) + resp, err := c.client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var keys api.SSHKeysResponse + if err := readJSON(resp.Body, &keys); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &keys, nil +} + // SSHConfig performs the POST request to the CA to get the ssh configuration // templates. func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) { From a50d59338e026a4eb285be675b0af0aebb93b8af Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Oct 2019 18:35:28 -0700 Subject: [PATCH 009/163] Rename SSH methods. --- api/api.go | 8 +++--- api/api_test.go | 16 +++++++++--- api/ssh.go | 46 +++++++++++++++++----------------- api/ssh_test.go | 27 ++++++++++---------- authority/ssh.go | 8 +++--- ca/client.go | 64 +++++++++++++++++++++++------------------------ ca/client_test.go | 10 ++++---- 7 files changed, 94 insertions(+), 85 deletions(-) diff --git a/api/api.go b/api/api.go index 47cc118f..6c4f760d 100644 --- a/api/api.go +++ b/api/api.go @@ -251,15 +251,15 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("GET", "/roots", h.Roots) r.MethodFunc("GET", "/federation", h.Federation) // SSH CA - r.MethodFunc("POST", "/ssh/sign", h.SignSSH) - r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) - r.MethodFunc("GET", "/ssh/federation", h.SSHFederatedKeys) + r.MethodFunc("POST", "/ssh/sign", h.SSHSign) + r.MethodFunc("GET", "/ssh/roots", h.SSHRoots) + r.MethodFunc("GET", "/ssh/federation", h.SSHFederation) r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) - r.MethodFunc("POST", "/sign-ssh", h.SignSSH) + r.MethodFunc("POST", "/sign-ssh", h.SSHSign) } // Health is an HTTP handler that returns the status of the server. diff --git a/api/api_test.go b/api/api_test.go index b276bcf6..9f43c851 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -513,7 +513,8 @@ type mockAuthority struct { getEncryptedKey func(kid string) (string, error) getRoots func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error) - getSSHKeys func() (*authority.SSHKeys, error) + getSSHRoots func() (*authority.SSHKeys, error) + getSSHFederation func() (*authority.SSHKeys, error) getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) } @@ -620,9 +621,16 @@ func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { return m.ret1.([]*x509.Certificate), m.err } -func (m *mockAuthority) GetSSHKeys() (*authority.SSHKeys, error) { - if m.getSSHKeys != nil { - return m.getSSHKeys() +func (m *mockAuthority) GetSSHRoots() (*authority.SSHKeys, error) { + if m.getSSHRoots != nil { + return m.getSSHRoots() + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *mockAuthority) GetSSHFederation() (*authority.SSHKeys, error) { + if m.getSSHFederation != nil { + return m.getSSHFederation() } return m.ret1.(*authority.SSHKeys), m.err } diff --git a/api/ssh.go b/api/ssh.go index 8d0b421d..e34174db 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -17,13 +17,13 @@ import ( type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) - GetSSHKeys() (*authority.SSHKeys, error) - GetSSHFederatedKeys() (*authority.SSHKeys, error) + GetSSHRoots() (*authority.SSHKeys, error) + GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) } -// SignSSHRequest is the request body of an SSH certificate request. -type SignSSHRequest struct { +// SSHSignRequest is the request body of an SSH certificate request. +type SSHSignRequest struct { PublicKey []byte `json:"publicKey"` //base64 encoded OTT string `json:"ott"` CertType string `json:"certType,omitempty"` @@ -33,8 +33,8 @@ type SignSSHRequest struct { AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` } -// Validate validates the SignSSHRequest. -func (s *SignSSHRequest) Validate() error { +// Validate validates the SSHSignRequest. +func (s *SSHSignRequest) Validate() error { switch { case s.CertType != "" && s.CertType != provisioner.SSHUserCert && s.CertType != provisioner.SSHHostCert: return errors.Errorf("unknown certType %s", s.CertType) @@ -47,15 +47,15 @@ func (s *SignSSHRequest) Validate() error { } } -// SignSSHResponse is the response object that returns the SSH certificate. -type SignSSHResponse struct { +// SSHSignResponse is the response object that returns the SSH certificate. +type SSHSignResponse struct { Certificate SSHCertificate `json:"crt"` AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` } -// SSHKeysResponse represents the response object that returns the SSH user and +// SSHRootsResponse represents the response object that returns the SSH user and // host keys. -type SSHKeysResponse struct { +type SSHRootsResponse struct { UserKeys []SSHPublicKey `json:"userKey,omitempty"` HostKeys []SSHPublicKey `json:"hostKey,omitempty"` } @@ -170,11 +170,11 @@ type SSHConfigResponse struct { HostTemplates []Template `json:"hostTemplates,omitempty"` } -// SignSSH is an HTTP handler that reads an SignSSHRequest with a one-time-token +// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. -func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { - var body SignSSHRequest +func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { + var body SSHSignRequest if err := ReadJSON(r.Body, &body); err != nil { WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) return @@ -232,16 +232,16 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusCreated) - JSON(w, &SignSSHResponse{ + JSON(w, &SSHSignResponse{ Certificate: SSHCertificate{cert}, AddUserCertificate: addUserCertificate, }) } -// SSHKeys is an HTTP handler that returns the SSH public keys for user and host +// SSHRoots is an HTTP handler that returns the SSH public keys for user and host // certificates. -func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { - keys, err := h.Authority.GetSSHKeys() +func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { + keys, err := h.Authority.GetSSHRoots() if err != nil { WriteError(w, InternalServerError(err)) return @@ -252,7 +252,7 @@ func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { return } - resp := new(SSHKeysResponse) + resp := new(SSHRootsResponse) for _, k := range keys.HostKeys { resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k}) } @@ -263,10 +263,10 @@ func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { JSON(w, resp) } -// SSHFederatedKeys is an HTTP handler that returns the federated SSH public -// keys for user and host certificates. -func (h *caHandler) SSHFederatedKeys(w http.ResponseWriter, r *http.Request) { - keys, err := h.Authority.GetSSHFederatedKeys() +// SSHFederation is an HTTP handler that returns the federated SSH public keys +// for user and host certificates. +func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { + keys, err := h.Authority.GetSSHFederation() if err != nil { WriteError(w, NotFound(err)) return @@ -277,7 +277,7 @@ func (h *caHandler) SSHFederatedKeys(w http.ResponseWriter, r *http.Request) { return } - resp := new(SSHKeysResponse) + resp := new(SSHRootsResponse) for _, k := range keys.HostKeys { resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k}) } diff --git a/api/ssh_test.go b/api/ssh_test.go index 55a0db90..3f5fcdbb 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -219,7 +219,7 @@ func TestSignSSHRequest_Validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &SignSSHRequest{ + s := &SSHSignRequest{ PublicKey: tt.fields.PublicKey, OTT: tt.fields.OTT, CertType: tt.fields.CertType, @@ -235,7 +235,7 @@ func TestSignSSHRequest_Validate(t *testing.T) { } } -func Test_caHandler_SignSSH(t *testing.T) { +func Test_caHandler_SSHSign(t *testing.T) { user, err := getSignedUserCertificate() assert.FatalError(t, err) host, err := getSignedHostCertificate() @@ -244,17 +244,17 @@ func Test_caHandler_SignSSH(t *testing.T) { userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) hostB64 := base64.StdEncoding.EncodeToString(host.Marshal()) - userReq, err := json.Marshal(SignSSHRequest{ + userReq, err := json.Marshal(SSHSignRequest{ PublicKey: user.Key.Marshal(), OTT: "ott", }) assert.FatalError(t, err) - hostReq, err := json.Marshal(SignSSHRequest{ + hostReq, err := json.Marshal(SSHSignRequest{ PublicKey: host.Key.Marshal(), OTT: "ott", }) assert.FatalError(t, err) - userAddReq, err := json.Marshal(SignSSHRequest{ + userAddReq, err := json.Marshal(SSHSignRequest{ PublicKey: user.Key.Marshal(), OTT: "ott", AddUserPublicKey: user.Key.Marshal(), @@ -299,7 +299,7 @@ func Test_caHandler_SignSSH(t *testing.T) { req := httptest.NewRequest("POST", "http://example.com/ssh/sign", bytes.NewReader(tt.req)) w := httptest.NewRecorder() - h.SignSSH(logging.NewResponseLogger(w), req) + h.SSHSign(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { @@ -320,7 +320,7 @@ func Test_caHandler_SignSSH(t *testing.T) { } } -func Test_caHandler_SSHKeys(t *testing.T) { +func Test_caHandler_SSHRoots(t *testing.T) { user, err := ssh.NewPublicKey(sshUserKey.Public()) assert.FatalError(t, err) userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) @@ -336,22 +336,23 @@ func Test_caHandler_SSHKeys(t *testing.T) { body []byte statusCode int }{ - {"ok", &authority.SSHKeys{HostKey: host, UserKey: user}, nil, []byte(fmt.Sprintf(`{"userKey":"%s","hostKey":"%s"}`, userB64, hostB64)), http.StatusOK}, - {"user", &authority.SSHKeys{UserKey: user}, nil, []byte(fmt.Sprintf(`{"userKey":"%s"}`, userB64)), http.StatusOK}, - {"host", &authority.SSHKeys{HostKey: host}, nil, []byte(fmt.Sprintf(`{"hostKey":"%s"}`, hostB64)), http.StatusOK}, - {"error", nil, fmt.Errorf("an error"), nil, http.StatusNotFound}, + {"ok", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s"],"hostKey":["%s"]}`, userB64, hostB64)), http.StatusOK}, + {"user", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s"]}`, userB64)), http.StatusOK}, + {"host", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{"hostKey":["%s"]}`, hostB64)), http.StatusOK}, + {"empty", &authority.SSHKeys{}, nil, nil, http.StatusNotFound}, + {"error", nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := New(&mockAuthority{ - getSSHKeys: func() (*authority.SSHKeys, error) { + getSSHRoots: func() (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, }).(*caHandler) req := httptest.NewRequest("GET", "http://example.com/ssh/keys", http.NoBody) w := httptest.NewRecorder() - h.SSHKeys(logging.NewResponseLogger(w), req) + h.SSHRoots(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { diff --git a/authority/ssh.go b/authority/ssh.go index 33f00cec..741d57cf 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -87,16 +87,16 @@ type SSHKeys struct { HostKeys []ssh.PublicKey } -// GetSSHKeys returns the SSH User and Host public keys. -func (a *Authority) GetSSHKeys() (*SSHKeys, error) { +// GetSSHRoots returns the SSH User and Host public keys. +func (a *Authority) GetSSHRoots() (*SSHKeys, error) { return &SSHKeys{ HostKeys: a.sshCAHostCerts, UserKeys: a.sshCAUserCerts, }, nil } -// GetSSHFederatedKeys returns the public keys for federated SSH signers. -func (a *Authority) GetSSHFederatedKeys() (*SSHKeys, error) { +// GetSSHFederation returns the public keys for federated SSH signers. +func (a *Authority) GetSSHFederation() (*SSHKeys, error) { return &SSHKeys{ HostKeys: a.sshCAHostFederatedCerts, UserKeys: a.sshCAUserFederatedCerts, diff --git a/ca/client.go b/ca/client.go index 45e1e7ce..7f34a92c 100644 --- a/ca/client.go +++ b/ca/client.go @@ -373,28 +373,6 @@ func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) { return &sign, nil } -// SignSSH performs the SSH certificate sign request to the CA and returns the -// api.SignSSHResponse struct. -func (c *Client) SignSSH(req *api.SignSSHRequest) (*api.SignSSHResponse, error) { - body, err := json.Marshal(req) - if err != nil { - return nil, errors.Wrap(err, "error marshaling request") - } - u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/sign"}) - resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) - if err != nil { - return nil, errors.Wrapf(err, "client POST %s failed", u) - } - if resp.StatusCode >= 400 { - return nil, readError(resp.Body) - } - var sign api.SignSSHResponse - if err := readJSON(resp.Body, &sign); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) - } - return &sign, nil -} - // Renew performs the renew request to the CA and returns the api.SignResponse // struct. func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) { @@ -527,10 +505,32 @@ func (c *Client) Federation() (*api.FederationResponse, error) { return &federation, nil } -// SSHKeys performs the get /ssh/keys request to the CA and returns the -// api.SSHKeysResponse struct. -func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { - u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"}) +// SSHSign performs the POST /ssh/sign request to the CA and returns the +// api.SSHSignResponse struct. +func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/sign"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var sign api.SSHSignResponse + if err := readJSON(resp.Body, &sign); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &sign, nil +} + +// SSHRoots performs the GET /ssh/roots request to the CA and returns the +// api.SSHRootsResponse struct. +func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) { + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/roots"}) resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) @@ -538,7 +538,7 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { if resp.StatusCode >= 400 { return nil, readError(resp.Body) } - var keys api.SSHKeysResponse + var keys api.SSHRootsResponse if err := readJSON(resp.Body, &keys); err != nil { return nil, errors.Wrapf(err, "error reading %s", u) } @@ -546,8 +546,8 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { } // SSHFederation performs the get /ssh/federation request to the CA and returns -// the api.SSHKeysResponse struct. -func (c *Client) SSHFederation() (*api.SSHKeysResponse, error) { +// the api.SSHRootsResponse struct. +func (c *Client) SSHFederation() (*api.SSHRootsResponse, error) { u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"}) resp, err := c.client.Get(u.String()) if err != nil { @@ -556,15 +556,15 @@ func (c *Client) SSHFederation() (*api.SSHKeysResponse, error) { if resp.StatusCode >= 400 { return nil, readError(resp.Body) } - var keys api.SSHKeysResponse + var keys api.SSHRootsResponse if err := readJSON(resp.Body, &keys); err != nil { return nil, errors.Wrapf(err, "error reading %s", u) } return &keys, nil } -// SSHConfig performs the POST request to the CA to get the ssh configuration -// templates. +// SSHConfig performs the POST /ssh/config request to the CA to get the ssh +// configuration templates. func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) { body, err := json.Marshal(req) if err != nil { diff --git a/ca/client_test.go b/ca/client_test.go index 58ee36d1..fc3a5049 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -722,15 +722,15 @@ func TestClient_Federation(t *testing.T) { } } -func TestClient_SSHKeys(t *testing.T) { +func TestClient_SSHRoots(t *testing.T) { key, err := ssh.NewPublicKey(mustKey().Public()) if err != nil { t.Fatal(err) } - ok := &api.SSHKeysResponse{ - HostKey: &api.SSHPublicKey{PublicKey: key}, - UserKey: &api.SSHPublicKey{PublicKey: key}, + ok := &api.SSHRootsResponse{ + HostKeys: []api.SSHPublicKey{{PublicKey: key}}, + UserKeys: []api.SSHPublicKey{{PublicKey: key}}, } notFound := api.NotFound(fmt.Errorf("Not Found")) @@ -759,7 +759,7 @@ func TestClient_SSHKeys(t *testing.T) { api.JSONStatus(w, tt.response, tt.responseCode) }) - got, err := c.SSHKeys() + got, err := c.SSHRoots() if (err != nil) != tt.wantErr { fmt.Printf("%+v", err) t.Errorf("Client.SSHKeys() error = %v, wantErr %v", err, tt.wantErr) From 3ee0dcec937dea4a73f96974d01e488bfe52f3a3 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Oct 2019 13:08:57 -0700 Subject: [PATCH 010/163] Add initial support for check-host endpoint. --- api/api.go | 1 + api/ssh.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ authority/ssh.go | 41 +++++++++++++++++++++++++++++++++++++--- ca/client.go | 27 ++++++++++++++++++++++++++ db/db.go | 45 +++++++++++++++++++++++++++++++++++++++++++- db/simple.go | 11 +++++++++++ 6 files changed, 170 insertions(+), 4 deletions(-) diff --git a/api/api.go b/api/api.go index 6c4f760d..0284167f 100644 --- a/api/api.go +++ b/api/api.go @@ -256,6 +256,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("GET", "/ssh/federation", h.SSHFederation) r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) + r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) diff --git a/api/ssh.go b/api/ssh.go index e34174db..ad649e43 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -20,6 +20,7 @@ type SSHAuthority interface { GetSSHRoots() (*authority.SSHKeys, error) GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) + CheckSSHHost(principal string) (bool, error) } // SSHSignRequest is the request body of an SSH certificate request. @@ -170,6 +171,32 @@ type SSHConfigResponse struct { HostTemplates []Template `json:"hostTemplates,omitempty"` } +// SSHCheckPrincipalRequest is the request body used to check if a principal +// certificate has been created. Right now it only supported for hosts +// certificates. +type SSHCheckPrincipalRequest struct { + Type string `json:"type"` + Principal string `json:"principal"` +} + +// Validate checks the check principal request. +func (r *SSHCheckPrincipalRequest) Validate() error { + switch { + case r.Type != provisioner.SSHHostCert: + return errors.Errorf("unsupported type %s", r.Type) + case r.Principal == "": + return errors.New("missing or empty principal") + default: + return nil + } +} + +// SSHCheckPrincipalResponse is the response body used to check if a principal +// exists. +type SSHCheckPrincipalResponse struct { + Exists bool `json:"exists"` +} + // SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. @@ -320,3 +347,25 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { JSON(w, config) } + +// SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not. +func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { + var body SSHCheckPrincipalRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + if err := body.Validate(); err != nil { + WriteError(w, BadRequest(err)) + return + } + + exists, err := h.Authority.CheckSSHHost(body.Principal) + if err != nil { + WriteError(w, InternalServerError(err)) + return + } + JSON(w, &SSHCheckPrincipalResponse{ + Exists: exists, + }) +} diff --git a/authority/ssh.go b/authority/ssh.go index 741d57cf..1c3f39bb 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/jose" @@ -263,6 +264,13 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign } } + if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + return nil, &apiError{ + err: errors.Wrap(err, "signSSH: error storing certificate in db"), + code: http.StatusInternalServerError, + } + } + return cert, nil } @@ -276,13 +284,13 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) } if subject.CertType != ssh.UserCert { return nil, &apiError{ - err: errors.New("signSSHProxy: certificate is not a user certificate"), + err: errors.New("signSSHAddUser: certificate is not a user certificate"), code: http.StatusForbidden, } } if len(subject.ValidPrincipals) != 1 { return nil, &apiError{ - err: errors.New("signSSHProxy: certificate does not have only one principal"), + err: errors.New("signSSHAddUser: certificate does not have only one principal"), code: http.StatusForbidden, } } @@ -295,7 +303,7 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) var serial uint64 if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { return nil, &apiError{ - err: errors.Wrap(err, "signSSHProxy: error reading random number"), + err: errors.Wrap(err, "signSSHAddUser: error reading random number"), code: http.StatusInternalServerError, } } @@ -331,9 +339,36 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) return nil, err } cert.Signature = sig + + if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + return nil, &apiError{ + err: errors.Wrap(err, "signSSHAddUser: error storing certificate in db"), + code: http.StatusInternalServerError, + } + } + return cert, nil } +// CheckSSHHost checks the given principal has been registered before. +func (a *Authority) CheckSSHHost(principal string) (bool, error) { + exists, err := a.db.IsSSHHost(principal) + if err != nil { + if err == db.ErrNotImplemented { + return false, &apiError{ + err: errors.Wrap(err, "checkSSHHost: isSSHHost is not implemented"), + code: http.StatusNotImplemented, + } + } + return false, &apiError{ + err: errors.Wrap(err, "checkSSHHost: error checking if hosts exists"), + code: http.StatusInternalServerError, + } + } + + return exists, nil +} + func (a *Authority) getAddUserPrincipal() (cmd string) { if a.config.SSH.AddUserPrincipal == "" { return SSHAddUserPrincipal diff --git a/ca/client.go b/ca/client.go index 7f34a92c..160bfe52 100644 --- a/ca/client.go +++ b/ca/client.go @@ -21,6 +21,8 @@ import ( "strconv" "strings" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/pkg/errors" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" @@ -585,6 +587,31 @@ func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, e return &config, nil } +// SSHCheckHost performs the POST /ssh/check-host request to the CA with the +// given principal. +func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, error) { + body, err := json.Marshal(&api.SSHCheckPrincipalRequest{ + Type: provisioner.SSHHostCert, + Principal: principal, + }) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var check api.SSHCheckPrincipalResponse + if err := readJSON(resp.Body, &check); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &check, nil +} + // RootFingerprint is a helper method that returns the current root fingerprint. // It does an health connection and gets the fingerprint from the TLS verified // chains. diff --git a/db/db.go b/db/db.go index 9cf7031e..17e5c209 100644 --- a/db/db.go +++ b/db/db.go @@ -3,17 +3,23 @@ package db import ( "crypto/x509" "encoding/json" + "strconv" + "strings" "time" "github.com/pkg/errors" "github.com/smallstep/nosql" "github.com/smallstep/nosql/database" + "golang.org/x/crypto/ssh" ) var ( certsTable = []byte("x509_certs") revokedCertsTable = []byte("revoked_x509_certs") usedOTTTable = []byte("used_ott") + sshCertsTable = []byte("ssh_certs") + sshHostsTable = []byte("ssh_hosts") + sshUsersTable = []byte("ssh_users") ) // ErrAlreadyExists can be returned if the DB attempts to set a key that has @@ -34,6 +40,8 @@ type AuthDB interface { Revoke(rci *RevokedCertificateInfo) error StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) + IsSSHHost(name string) (bool, error) + StoreSSHCertificate(crt *ssh.Certificate) error Shutdown() error } @@ -55,7 +63,10 @@ func New(c *Config) (AuthDB, error) { return nil, errors.Wrapf(err, "Error opening database of Type %s with source %s", c.Type, c.DataSource) } - tables := [][]byte{revokedCertsTable, certsTable, usedOTTTable} + tables := [][]byte{ + revokedCertsTable, certsTable, usedOTTTable, + sshCertsTable, sshHostsTable, sshUsersTable, + } for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", @@ -138,6 +149,38 @@ func (db *DB) UseToken(id, tok string) (bool, error) { return swapped, nil } +// IsSSHHost returns if a principal is present in the ssh hosts table. +func (db *DB) IsSSHHost(principal string) (bool, error) { + if _, err := db.Get(sshHostsTable, []byte(strings.ToLower(principal))); err != nil { + if database.IsErrNotFound(err) { + return false, nil + } + return false, errors.Wrap(err, "database Get error") + } + return true, nil +} + +// StoreSSHCertificate stores an SSH certificate. +func (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error { + var table []byte + serial := strconv.FormatUint(crt.Serial, 10) + tx := new(database.Tx) + tx.Set(sshCertsTable, []byte(serial), crt.Marshal()) + if crt.CertType == ssh.HostCert { + table = sshHostsTable + } else { + table = sshUsersTable + } + for _, p := range crt.ValidPrincipals { + tx.Set(table, []byte(strings.ToLower(p)), []byte(serial)) + } + if err := db.Update(tx); err != nil { + return errors.Wrap(err, "database Update error") + } + return nil + +} + // Shutdown sends a shutdown message to the database. func (db *DB) Shutdown() error { if db.isUp { diff --git a/db/simple.go b/db/simple.go index 30c2b124..7989de44 100644 --- a/db/simple.go +++ b/db/simple.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/nosql/database" + "golang.org/x/crypto/ssh" ) // ErrNotImplemented is an error returned when an operation is Not Implemented. @@ -58,6 +59,16 @@ func (s *SimpleDB) UseToken(id, tok string) (bool, error) { return true, nil } +// IsSSHHost returns a "NotImplemented" error. +func (s *SimpleDB) IsSSHHost(principal string) (bool, error) { + return false, ErrNotImplemented +} + +// StoreSSHCertificate returns a "NotImplemented" error. +func (s *SimpleDB) StoreSSHCertificate(crt *ssh.Certificate) error { + return ErrNotImplemented +} + // Shutdown returns nil func (s *SimpleDB) Shutdown() error { return nil From 505da52279ea66df8be3d5e86201280188dd1193 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 11:25:48 -0700 Subject: [PATCH 011/163] Create templates path, and remove unnecessary arguments. --- commands/onboard.go | 2 +- pki/pki.go | 40 +++++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/commands/onboard.go b/commands/onboard.go index cc4a1eef..9f35c993 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -162,7 +162,7 @@ func onboardAction(ctx *cli.Context) error { } func onboardPKI(config onboardingConfiguration) (*authority.Config, string, error) { - p, err := pki.New(pki.GetPublicPath(), pki.GetSecretsPath(), pki.GetConfigPath()) + p, err := pki.New() if err != nil { return nil, "", err } diff --git a/pki/pki.go b/pki/pki.go index 1896b7b4..7c1aeff8 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -15,8 +15,6 @@ import ( "strconv" "strings" - "golang.org/x/crypto/ssh" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" @@ -31,6 +29,7 @@ import ( "github.com/smallstep/cli/jose" "github.com/smallstep/cli/ui" "github.com/smallstep/cli/utils" + "golang.org/x/crypto/ssh" ) const ( @@ -46,6 +45,8 @@ const ( // DBPath is the directory name under the step path where the private keys // will be stored. dbPath = "db" + // templatesPath is the directory to store templates + templatesPath = "templates" ) // GetDBPath returns the path where the file-system persistence is stored @@ -84,6 +85,11 @@ func GetOTTKeyPath() string { return filepath.Join(config.StepPath(), privatePath, "ott_key") } +// GetTemplatesPath returns the path where the templates are stored. +func GetTemplatesPath() string { + return filepath.Join(config.StepPath(), templatesPath) +} + // GetProvisioners returns the map of provisioners on the given CA. func GetProvisioners(caURL, rootFile string) (provisioner.List, error) { if len(rootFile) == 0 { @@ -142,21 +148,17 @@ type PKI struct { } // New creates a new PKI configuration. -func New(public, private, config string) (*PKI, error) { - if _, err := os.Stat(public); os.IsNotExist(err) { - if err = os.MkdirAll(public, 0700); err != nil { - return nil, errs.FileError(err, public) - } - } - if _, err := os.Stat(private); os.IsNotExist(err) { - if err = os.MkdirAll(private, 0700); err != nil { - return nil, errs.FileError(err, private) - } - } - if len(config) > 0 { - if _, err := os.Stat(config); os.IsNotExist(err) { - if err = os.MkdirAll(config, 0700); err != nil { - return nil, errs.FileError(err, config) +func New() (*PKI, error) { + public := GetPublicPath() + private := GetSecretsPath() + config := GetConfigPath() + + // Create directories + dirs := []string{public, private, config, GetTemplatesPath()} + for _, name := range dirs { + if _, err := os.Stat(name); os.IsNotExist(err) { + if err = os.MkdirAll(name, 0700); err != nil { + return nil, errs.FileError(err, name) } } } @@ -468,7 +470,7 @@ func (p *PKI) Save(opt ...Option) error { if err != nil { return errors.Wrapf(err, "error marshaling %s", p.config) } - if err = utils.WriteFile(p.config, b, 0666); err != nil { + if err = utils.WriteFile(p.config, b, 0644); err != nil { return errs.FileError(err, p.config) } @@ -497,7 +499,7 @@ func (p *PKI) Save(opt ...Option) error { if err != nil { return errors.Wrapf(err, "error marshaling %s", p.defaults) } - if err = utils.WriteFile(p.defaults, b, 0666); err != nil { + if err = utils.WriteFile(p.defaults, b, 0644); err != nil { return errs.FileError(err, p.defaults) } From 0b5abcf3b0adba4720d2c397b4a0a66910a1fd9b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 12:49:09 -0700 Subject: [PATCH 012/163] Add first version of ssh templates. --- pki/pki.go | 18 +++++++-- pki/templates.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 pki/templates.go diff --git a/pki/pki.go b/pki/pki.go index 7c1aeff8..c7c5ec1c 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -434,6 +434,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { Renegotiation: x509util.DefaultTLSRenegotiation, CipherSuites: x509util.DefaultTLSCipherSuites, }, + Templates: p.getTemplates(), } if p.enableSSH { enableSSHCA := true @@ -461,6 +462,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { func (p *PKI) Save(opt ...Option) error { p.tellPKI() + // Generate and write ca.json config, err := p.GenerateConfig(opt...) if err != nil { return err @@ -489,6 +491,7 @@ func (p *PKI) Save(opt ...Option) error { } } + // Generate and write defaults.json defaults := &caDefaults{ Root: p.root, CAConfig: p.config, @@ -503,11 +506,20 @@ func (p *PKI) Save(opt ...Option) error { return errs.FileError(err, p.defaults) } - ui.PrintSelected("Default configuration", p.defaults) - ui.PrintSelected("Certificate Authority configuration", p.config) + // Generate and write templates + if err := generateTemplates(config.Templates); err != nil { + return err + } + if config.DB != nil { - ui.PrintSelected("Database", config.DB.DataSource) + ui.PrintSelected("Database folder", config.DB.DataSource) } + if config.Templates != nil { + ui.PrintSelected("Templates folder", GetTemplatesPath()) + } + + ui.PrintSelected("Default configuration", p.defaults) + ui.PrintSelected("Certificate Authority configuration", p.config) ui.Println() ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") diff --git a/pki/templates.go b/pki/templates.go new file mode 100644 index 00000000..1d873ed4 --- /dev/null +++ b/pki/templates.go @@ -0,0 +1,101 @@ +package pki + +import ( + "os" + "path/filepath" + + "github.com/smallstep/cli/utils" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/templates" + "github.com/smallstep/cli/errs" +) + +// sshTemplates contains the configuration of default templates used on ssh. +var sshTemplates = &templates.SSHTemplates{ + User: []templates.Template{ + {Name: "include.tpl", Type: templates.Snippet, TemplatePath: "ssh/include.tpl", Path: "~/.ssh/config", Comment: "#"}, + {Name: "config.tpl", Type: templates.File, TemplatePath: "ssh/config.tpl", Path: "ssh/config", Comment: "#"}, + {Name: "known_hosts.tpl", Type: templates.File, TemplatePath: "ssh/known_hosts.tpl", Path: "ssh/known_hosts", Comment: "#"}, + }, + Host: []templates.Template{ + {Name: "sshd_config.tpl", Type: templates.Snippet, TemplatePath: "ssh/sshd_config.tpl", Path: "/etc/ssh/sshd_config", Comment: "#"}, + {Name: "ca.tpl", Type: templates.Snippet, TemplatePath: "ssh/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, + }, +} + +// sshTemplateData contains the data of the default templates used on ssh. +var sshTemplateData = map[string]string{ + // include.tpl adds the step ssh config file + "include.tpl": `Host * + Include {{.User.StepPath}}/ssh/config`, + + // config.tpl is the step ssh config file, it includes the Match rule + // and references the step known_hosts file + "config.tpl": `Match exec "step ssh check-host %h" + ForwardAgent yes + UserKnownHostsFile {{.User.StepPath}}/config/ssh/known_hosts`, + + // known_hosts.tpl authorizes the ssh hosst key + "known_hosts.tpl": "@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}", + + // sshd_config.tpl adds the configuration to support certificates + "sshd_config.tpl": `TrustedUserCAKeys /etc/ssh/ca.pub +HostCertificate /etc/ssh/{{.User.Certificate}} +HostKey /etc/ssh/{{.User.Key}}`, + + // ca.tpl contains the public key used to authorized clients + "ca.tpl": "{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}", +} + +// getTemplates returns all the templates enabled +func (p *PKI) getTemplates() *templates.Templates { + if !p.enableSSH { + return nil + } + + return &templates.Templates{ + SSH: sshTemplates, + Data: map[string]interface{}{}, + } +} + +// generateTemplates generates given templates. +func generateTemplates(t *templates.Templates) error { + if t == nil { + return nil + } + + base := GetTemplatesPath() + // Generate SSH templates + if t.SSH != nil { + // all ssh templates are under ssh: + sshDir := filepath.Join(base, "ssh") + if _, err := os.Stat(sshDir); os.IsNotExist(err) { + if err = os.MkdirAll(sshDir, 0700); err != nil { + return errs.FileError(err, sshDir) + } + } + // Create all templates + for _, t := range t.SSH.User { + data, ok := sshTemplateData[t.Name] + if !ok { + return errors.Errorf("template %s does not exists", t.Name) + } + if err := utils.WriteFile(filepath.Join(base, t.TemplatePath), []byte(data), 0644); err != nil { + return err + } + } + for _, t := range t.SSH.Host { + data, ok := sshTemplateData[t.Name] + if !ok { + return errors.Errorf("template %s does not exists", t.Name) + } + if err := utils.WriteFile(filepath.Join(base, t.TemplatePath), []byte(data), 0644); err != nil { + return err + } + } + } + + return nil +} From 13fb0061ecfa35fd5d7f5bf3671ec401aa84732a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 18:59:50 -0700 Subject: [PATCH 013/163] Add Write method to templates.Output. --- pki/templates.go | 21 ++++++++-------- templates/templates.go | 57 +++++++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/pki/templates.go b/pki/templates.go index 1d873ed4..c5789573 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -4,23 +4,24 @@ import ( "os" "path/filepath" - "github.com/smallstep/cli/utils" - "github.com/pkg/errors" "github.com/smallstep/certificates/templates" + "github.com/smallstep/cli/config" "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/utils" ) // sshTemplates contains the configuration of default templates used on ssh. +// Relative paths are relative to the StepPath. var sshTemplates = &templates.SSHTemplates{ User: []templates.Template{ - {Name: "include.tpl", Type: templates.Snippet, TemplatePath: "ssh/include.tpl", Path: "~/.ssh/config", Comment: "#"}, - {Name: "config.tpl", Type: templates.File, TemplatePath: "ssh/config.tpl", Path: "ssh/config", Comment: "#"}, - {Name: "known_hosts.tpl", Type: templates.File, TemplatePath: "ssh/known_hosts.tpl", Path: "ssh/known_hosts", Comment: "#"}, + {Name: "include.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/include.tpl", Path: "~/.ssh/config", Comment: "#"}, + {Name: "config.tpl", Type: templates.File, TemplatePath: "templates/ssh/config.tpl", Path: "ssh/config", Comment: "#"}, + {Name: "known_hosts.tpl", Type: templates.File, TemplatePath: "templates/ssh/known_hosts.tpl", Path: "ssh/known_hosts", Comment: "#"}, }, Host: []templates.Template{ - {Name: "sshd_config.tpl", Type: templates.Snippet, TemplatePath: "ssh/sshd_config.tpl", Path: "/etc/ssh/sshd_config", Comment: "#"}, - {Name: "ca.tpl", Type: templates.Snippet, TemplatePath: "ssh/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, + {Name: "sshd_config.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/sshd_config.tpl", Path: "/etc/ssh/sshd_config", Comment: "#"}, + {Name: "ca.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, }, } @@ -36,7 +37,7 @@ var sshTemplateData = map[string]string{ ForwardAgent yes UserKnownHostsFile {{.User.StepPath}}/config/ssh/known_hosts`, - // known_hosts.tpl authorizes the ssh hosst key + // known_hosts.tpl authorizes the ssh hosts key "known_hosts.tpl": "@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}", // sshd_config.tpl adds the configuration to support certificates @@ -82,7 +83,7 @@ func generateTemplates(t *templates.Templates) error { if !ok { return errors.Errorf("template %s does not exists", t.Name) } - if err := utils.WriteFile(filepath.Join(base, t.TemplatePath), []byte(data), 0644); err != nil { + if err := utils.WriteFile(config.StepAbs(t.TemplatePath), []byte(data), 0644); err != nil { return err } } @@ -91,7 +92,7 @@ func generateTemplates(t *templates.Templates) error { if !ok { return errors.Errorf("template %s does not exists", t.Name) } - if err := utils.WriteFile(filepath.Join(base, t.TemplatePath), []byte(data), 0644); err != nil { + if err := utils.WriteFile(config.StepAbs(t.TemplatePath), []byte(data), 0644); err != nil { return err } } diff --git a/templates/templates.go b/templates/templates.go index 1ff5cdf2..ee4a5791 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -3,10 +3,14 @@ package templates import ( "bytes" "io/ioutil" + "os" + "path/filepath" "text/template" "github.com/Masterminds/sprig" "github.com/pkg/errors" + "github.com/smallstep/cli/config" + "github.com/smallstep/cli/utils" ) // TemplateType defines how a template will be written in disk. @@ -17,17 +21,10 @@ const ( Snippet TemplateType = "snippet" // File will mark a templates as a full file. File TemplateType = "file" + // Directory will mark a template as a directory. + Directory TemplateType = "directory" ) -// Output represents the text representation of a rendered template. -type Output struct { - Name string `json:"name"` - Type TemplateType `json:"type"` - Comment string `json:"comment"` - Path string `json:"path"` - Content []byte `json:"content"` -} - // Templates is a collection of templates and variables. type Templates struct { SSH *SSHTemplates `json:"ssh,omitempty"` @@ -137,13 +134,14 @@ func (t *Template) Validate() error { // template fails. func (t *Template) Load() error { if t.Template == nil { - b, err := ioutil.ReadFile(t.TemplatePath) + filename := config.StepAbs(t.TemplatePath) + b, err := ioutil.ReadFile(filename) if err != nil { - return errors.Wrapf(err, "error reading %s", t.TemplatePath) + return errors.Wrapf(err, "error reading %s", filename) } tmpl, err := template.New(t.Name).Funcs(sprig.TxtFuncMap()).Parse(string(b)) if err != nil { - return errors.Wrapf(err, "error parsing %s", t.TemplatePath) + return errors.Wrapf(err, "error parsing %s", filename) } t.Template = tmpl } @@ -179,3 +177,38 @@ func (t *Template) Output(data interface{}) (Output, error) { Content: b, }, nil } + +// Output represents the text representation of a rendered template. +type Output struct { + Name string `json:"name"` + Type TemplateType `json:"type"` + Comment string `json:"comment"` + Path string `json:"path"` + Content []byte `json:"content"` +} + +// Write writes the Output to the filesystem as a directory, file or snippet. +func (o *Output) Write() error { + path := config.StepAbs(o.Path) + if o.Type == Directory { + return mkdir(path, 0700) + } + + dir := filepath.Dir(path) + if err := mkdir(dir, 0700); err != nil { + return err + } + + if o.Type == File { + return utils.WriteFile(path, o.Content, 0600) + } + + return utils.WriteSnippet(path, o.Content, 0600) +} + +func mkdir(path string, perm os.FileMode) error { + if err := os.MkdirAll(path, perm); err != nil { + return errors.Wrapf(err, "error creating %s", path) + } + return nil +} From f0b346a528ff932800f6a94f5adcc4688fa8068e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 19:26:09 -0700 Subject: [PATCH 014/163] Fix return of host configurations. --- api/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/ssh.go b/api/ssh.go index ad649e43..d5ad735a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -339,7 +339,7 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { case provisioner.SSHUserCert: config.UserTemplates = ts case provisioner.SSHHostCert: - config.UserTemplates = ts + config.HostTemplates = ts default: WriteError(w, InternalServerError(errors.New("it should hot get here"))) return From 38d735be6e9bc894a4ac8f34ba896e6846f160f2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 19:26:59 -0700 Subject: [PATCH 015/163] Add support for federated keys. --- authority/authority.go | 6 ++++++ pki/templates.go | 10 ++++++++-- templates/values.go | 6 ++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 88c829c6..34eee14b 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -195,9 +195,15 @@ func (a *Authority) init() error { if a.config.SSH != nil { if a.sshCAHostCertSignKey != nil { vars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey() + for _, k := range a.sshCAHostFederatedCerts[1:] { + vars.SSH.HostFederatedKeys = append(vars.SSH.HostFederatedKeys, k) + } } if a.sshCAUserCertSignKey != nil { vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey() + for _, k := range a.sshCAUserFederatedCerts[1:] { + vars.SSH.UserFederatedKeys = append(vars.SSH.UserFederatedKeys, k) + } } } t.Data["Step"] = vars diff --git a/pki/templates.go b/pki/templates.go index c5789573..99a2ac7d 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -38,7 +38,10 @@ var sshTemplateData = map[string]string{ UserKnownHostsFile {{.User.StepPath}}/config/ssh/known_hosts`, // known_hosts.tpl authorizes the ssh hosts key - "known_hosts.tpl": "@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}", + "known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} +{{- range .Step.SSH.HostFederatedKeys}} +@cert-authority * {{.Type}} {{.Marshal | toString | b64enc}} +{{- end}}`, // sshd_config.tpl adds the configuration to support certificates "sshd_config.tpl": `TrustedUserCAKeys /etc/ssh/ca.pub @@ -46,7 +49,10 @@ HostCertificate /etc/ssh/{{.User.Certificate}} HostKey /etc/ssh/{{.User.Key}}`, // ca.tpl contains the public key used to authorized clients - "ca.tpl": "{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}", + "ca.tpl": `{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}} +{{- range .Step.SSH.UserFederatedKeys}} +{{.Type}} {{.Marshal | toString | b64enc}} +{{- end}}`, } // getTemplates returns all the templates enabled diff --git a/templates/values.go b/templates/values.go index 995c2998..505b3b87 100644 --- a/templates/values.go +++ b/templates/values.go @@ -10,6 +10,8 @@ type Step struct { } type StepSSH struct { - HostKey ssh.PublicKey - UserKey ssh.PublicKey + HostKey ssh.PublicKey + UserKey ssh.PublicKey + HostFederatedKeys []ssh.PublicKey + UserFederatedKeys []ssh.PublicKey } From f5023244fef43b3829f91c23e0a86d8506d95ca2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Oct 2019 13:07:28 -0700 Subject: [PATCH 016/163] Add tests for ssh api methods. --- api/api_test.go | 8 ++ api/ssh.go | 2 +- api/ssh_test.go | 235 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 240 insertions(+), 5 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 9f43c851..6232dde5 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -516,6 +516,7 @@ type mockAuthority struct { getSSHRoots func() (*authority.SSHKeys, error) getSSHFederation func() (*authority.SSHKeys, error) getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) + checkSSHHost func(principal string) (bool, error) } // TODO: remove once Authorize is deprecated. @@ -642,6 +643,13 @@ func (m *mockAuthority) GetSSHConfig(typ string, data map[string]string) ([]temp return m.ret1.([]templates.Output), m.err } +func (m *mockAuthority) CheckSSHHost(principal string) (bool, error) { + if m.checkSSHHost != nil { + return m.checkSSHHost(principal) + } + return m.ret1.(bool), m.err +} + func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority diff --git a/api/ssh.go b/api/ssh.go index d5ad735a..e3101b8b 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -295,7 +295,7 @@ func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHFederation() if err != nil { - WriteError(w, NotFound(err)) + WriteError(w, InternalServerError(err)) return } diff --git a/api/ssh_test.go b/api/ssh_test.go index 3f5fcdbb..ed107b6c 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -12,6 +12,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" "time" @@ -19,6 +20,7 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/templates" "golang.org/x/crypto/ssh" ) @@ -337,6 +339,7 @@ func Test_caHandler_SSHRoots(t *testing.T) { statusCode int }{ {"ok", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s"],"hostKey":["%s"]}`, userB64, hostB64)), http.StatusOK}, + {"many", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host, host}, UserKeys: []ssh.PublicKey{user, user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s","%s"],"hostKey":["%s","%s"]}`, userB64, userB64, hostB64, hostB64)), http.StatusOK}, {"user", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s"]}`, userB64)), http.StatusOK}, {"host", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{"hostKey":["%s"]}`, hostB64)), http.StatusOK}, {"empty", &authority.SSHKeys{}, nil, nil, http.StatusNotFound}, @@ -350,25 +353,249 @@ func Test_caHandler_SSHRoots(t *testing.T) { }, }).(*caHandler) - req := httptest.NewRequest("GET", "http://example.com/ssh/keys", http.NoBody) + req := httptest.NewRequest("GET", "http://example.com/ssh/roots", http.NoBody) w := httptest.NewRecorder() h.SSHRoots(logging.NewResponseLogger(w), req) res := w.Result() if res.StatusCode != tt.statusCode { - t.Errorf("caHandler.SSHKeys StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + t.Errorf("caHandler.SSHRoots StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) } body, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - t.Errorf("caHandler.SSHKeys unexpected error = %v", err) + t.Errorf("caHandler.SSHRoots unexpected error = %v", err) } if tt.statusCode < http.StatusBadRequest { if !bytes.Equal(bytes.TrimSpace(body), tt.body) { - t.Errorf("caHandler.SSHKeys Body = %s, wants %s", body, tt.body) + t.Errorf("caHandler.SSHRoots Body = %s, wants %s", body, tt.body) } } }) } } + +func Test_caHandler_SSHFederation(t *testing.T) { + user, err := ssh.NewPublicKey(sshUserKey.Public()) + assert.FatalError(t, err) + userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) + + host, err := ssh.NewPublicKey(sshHostKey.Public()) + assert.FatalError(t, err) + hostB64 := base64.StdEncoding.EncodeToString(host.Marshal()) + + tests := []struct { + name string + keys *authority.SSHKeys + keysErr error + body []byte + statusCode int + }{ + {"ok", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s"],"hostKey":["%s"]}`, userB64, hostB64)), http.StatusOK}, + {"many", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host, host}, UserKeys: []ssh.PublicKey{user, user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s","%s"],"hostKey":["%s","%s"]}`, userB64, userB64, hostB64, hostB64)), http.StatusOK}, + {"user", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":["%s"]}`, userB64)), http.StatusOK}, + {"host", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{"hostKey":["%s"]}`, hostB64)), http.StatusOK}, + {"empty", &authority.SSHKeys{}, nil, nil, http.StatusNotFound}, + {"error", nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ + getSSHFederation: func() (*authority.SSHKeys, error) { + return tt.keys, tt.keysErr + }, + }).(*caHandler) + + req := httptest.NewRequest("GET", "http://example.com/ssh/federation", http.NoBody) + w := httptest.NewRecorder() + h.SSHFederation(logging.NewResponseLogger(w), req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.SSHFederation StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.SSHFederation unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), tt.body) { + t.Errorf("caHandler.SSHFederation Body = %s, wants %s", body, tt.body) + } + } + }) + } +} + +func Test_caHandler_SSHConfig(t *testing.T) { + userOutput := []templates.Output{ + {"config.tpl", templates.File, "#", "ssh/config", []byte("UserKnownHostsFile /home/user/.step/config/ssh/known_hosts")}, + {"known_host.tpl", templates.File, "#", "ssh/known_host", []byte("@cert-authority * ecdsa-sha2-nistp256 AAAA...=")}, + } + hostOutput := []templates.Output{ + {"sshd_config.tpl", templates.Snippet, "#", "/etc/ssh/sshd_config", []byte("TrustedUserCAKeys /etc/ssh/ca.pub")}, + {"ca.tpl", templates.File, "#", "/etc/ssh/ca.pub", []byte("ecdsa-sha2-nistp256 AAAA...=")}, + } + userJSON, err := json.Marshal(userOutput) + assert.FatalError(t, err) + hostJSON, err := json.Marshal(hostOutput) + assert.FatalError(t, err) + + tests := []struct { + name string + req string + output []templates.Output + err error + body []byte + statusCode int + }{ + {"user", `{"type":"user"}`, userOutput, nil, []byte(fmt.Sprintf(`{"userTemplates":%s}`, userJSON)), http.StatusOK}, + {"host", `{"type":"host"}`, hostOutput, nil, []byte(fmt.Sprintf(`{"hostTemplates":%s}`, hostJSON)), http.StatusOK}, + {"noType", `{}`, userOutput, nil, []byte(fmt.Sprintf(`{"userTemplates":%s}`, userJSON)), http.StatusOK}, + {"badType", `{"type":"bad"}`, userOutput, nil, nil, http.StatusBadRequest}, + {"badData", `{"type":"user","data":{"bad"}}`, userOutput, nil, nil, http.StatusBadRequest}, + {"error", `{"type": "user"}`, nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ + getSSHConfig: func(typ string, data map[string]string) ([]templates.Output, error) { + return tt.output, tt.err + }, + }).(*caHandler) + + req := httptest.NewRequest("GET", "http://example.com/ssh/config", strings.NewReader(tt.req)) + w := httptest.NewRecorder() + h.SSHConfig(logging.NewResponseLogger(w), req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.SSHConfig StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.SSHConfig unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), tt.body) { + t.Errorf("caHandler.SSHConfig Body = %s, wants %s", body, tt.body) + } + } + }) + } +} + +func Test_caHandler_SSHCheckHost(t *testing.T) { + tests := []struct { + name string + req string + exists bool + err error + body []byte + statusCode int + }{ + {"true", `{"type":"host","principal":"foo.example.com"}`, true, nil, []byte(`{"exists":true}`), http.StatusOK}, + {"false", `{"type":"host","principal":"bar.example.com"}`, false, nil, []byte(`{"exists":false}`), http.StatusOK}, + {"badType", `{"type":"user","principal":"bar.example.com"}`, false, nil, nil, http.StatusBadRequest}, + {"badPrincipal", `{"type":"host","principal":""}`, false, nil, nil, http.StatusBadRequest}, + {"badRequest", `{"foo"}`, false, nil, nil, http.StatusBadRequest}, + {"error", `{"type":"host","principal":"foo.example.com"}`, false, fmt.Errorf("an error"), nil, http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ + checkSSHHost: func(_ string) (bool, error) { + return tt.exists, tt.err + }, + }).(*caHandler) + + req := httptest.NewRequest("GET", "http://example.com/ssh/check-host", strings.NewReader(tt.req)) + w := httptest.NewRecorder() + h.SSHCheckHost(logging.NewResponseLogger(w), req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.SSHCheckHost StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.SSHCheckHost unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), tt.body) { + t.Errorf("caHandler.SSHCheckHost Body = %s, wants %s", body, tt.body) + } + } + }) + } +} + +func TestSSHPublicKey_MarshalJSON(t *testing.T) { + key, err := ssh.NewPublicKey(sshUserKey.Public()) + assert.FatalError(t, err) + keyB64 := base64.StdEncoding.EncodeToString(key.Marshal()) + + tests := []struct { + name string + publicKey *SSHPublicKey + want []byte + wantErr bool + }{ + {"ok", &SSHPublicKey{PublicKey: key}, []byte(`"` + keyB64 + `"`), false}, + {"null", nil, []byte("null"), false}, + {"null", &SSHPublicKey{PublicKey: nil}, []byte("null"), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.publicKey.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("SSHPublicKey.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SSHPublicKey.MarshalJSON() = %s, want %s", got, tt.want) + } + }) + } +} + +func TestSSHPublicKey_UnmarshalJSON(t *testing.T) { + key, err := ssh.NewPublicKey(sshUserKey.Public()) + assert.FatalError(t, err) + keyB64 := base64.StdEncoding.EncodeToString(key.Marshal()) + + type args struct { + data []byte + } + tests := []struct { + name string + args args + want *SSHPublicKey + wantErr bool + }{ + {"ok", args{[]byte(`"` + keyB64 + `"`)}, &SSHPublicKey{PublicKey: key}, false}, + {"empty", args{[]byte(`""`)}, &SSHPublicKey{}, false}, + {"null", args{[]byte(`null`)}, &SSHPublicKey{}, false}, + {"noString", args{[]byte("123")}, &SSHPublicKey{}, true}, + {"badB64", args{[]byte(`"bad"`)}, &SSHPublicKey{}, true}, + {"badKey", args{[]byte(`"Zm9vYmFyCg=="`)}, &SSHPublicKey{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &SSHPublicKey{} + if err := p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr { + t.Errorf("SSHPublicKey.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(p, tt.want) { + t.Errorf("SSHPublicKey.UnmarshalJSON() = %v, want %v", p, tt.want) + } + }) + } +} From 78f26978fa21ea13583b5f78bb61b530577c3246 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Oct 2019 13:57:06 -0700 Subject: [PATCH 017/163] Fix lint, add keys to fields. --- api/ssh_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/ssh_test.go b/api/ssh_test.go index ed107b6c..075428c0 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -432,12 +432,12 @@ func Test_caHandler_SSHFederation(t *testing.T) { func Test_caHandler_SSHConfig(t *testing.T) { userOutput := []templates.Output{ - {"config.tpl", templates.File, "#", "ssh/config", []byte("UserKnownHostsFile /home/user/.step/config/ssh/known_hosts")}, - {"known_host.tpl", templates.File, "#", "ssh/known_host", []byte("@cert-authority * ecdsa-sha2-nistp256 AAAA...=")}, + {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("UserKnownHostsFile /home/user/.step/config/ssh/known_hosts")}, + {Name: "known_host.tpl", Type: templates.File, Comment: "#", Path: "ssh/known_host", Content: []byte("@cert-authority * ecdsa-sha2-nistp256 AAAA...=")}, } hostOutput := []templates.Output{ - {"sshd_config.tpl", templates.Snippet, "#", "/etc/ssh/sshd_config", []byte("TrustedUserCAKeys /etc/ssh/ca.pub")}, - {"ca.tpl", templates.File, "#", "/etc/ssh/ca.pub", []byte("ecdsa-sha2-nistp256 AAAA...=")}, + {Name: "sshd_config.tpl", Type: templates.Snippet, Comment: "#", Path: "/etc/ssh/sshd_config", Content: []byte("TrustedUserCAKeys /etc/ssh/ca.pub")}, + {Name: "ca.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/ca.pub", Content: []byte("ecdsa-sha2-nistp256 AAAA...=")}, } userJSON, err := json.Marshal(userOutput) assert.FatalError(t, err) From 40052a1824e1fb4964e9b9c10c78f2fcdf3d3b53 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Oct 2019 17:10:47 -0700 Subject: [PATCH 018/163] Add some ssh related tests. --- authority/db_test.go | 33 +++- authority/ssh_test.go | 167 +++++++++++++++++++ authority/testdata/templates/ca.tpl | 4 + authority/testdata/templates/known_hosts.tpl | 4 + 4 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 authority/testdata/templates/ca.tpl create mode 100644 authority/testdata/templates/known_hosts.tpl diff --git a/authority/db_test.go b/authority/db_test.go index e3834b99..bd6b27ca 100644 --- a/authority/db_test.go +++ b/authority/db_test.go @@ -4,17 +4,20 @@ import ( "crypto/x509" "github.com/smallstep/certificates/db" + "golang.org/x/crypto/ssh" ) type MockAuthDB struct { - err error - ret1 interface{} - init func(*db.Config) (db.AuthDB, error) - isRevoked func(string) (bool, error) - revoke func(rci *db.RevokedCertificateInfo) error - storeCertificate func(crt *x509.Certificate) error - useToken func(id, tok string) (bool, error) - shutdown func() error + err error + ret1 interface{} + init func(*db.Config) (db.AuthDB, error) + isRevoked func(string) (bool, error) + revoke func(rci *db.RevokedCertificateInfo) error + storeCertificate func(crt *x509.Certificate) error + useToken func(id, tok string) (bool, error) + isSSHHost func(principal string) (bool, error) + storeSSHCertificate func(crt *ssh.Certificate) error + shutdown func() error } func (m *MockAuthDB) Init(c *db.Config) (db.AuthDB, error) { @@ -58,6 +61,20 @@ func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error { return m.err } +func (m *MockAuthDB) IsSSHHost(principal string) (bool, error) { + if m.isSSHHost != nil { + return m.isSSHHost(principal) + } + return m.ret1.(bool), m.err +} + +func (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error { + if m.storeSSHCertificate != nil { + return m.storeSSHCertificate(crt) + } + return m.err +} + func (m *MockAuthDB) Shutdown() error { if m.shutdown != nil { return m.shutdown() diff --git a/authority/ssh_test.go b/authority/ssh_test.go index ff0bb23c..872278d6 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -4,12 +4,15 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "encoding/base64" "fmt" + "reflect" "testing" "time" "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/templates" "golang.org/x/crypto/ssh" ) @@ -253,3 +256,167 @@ func TestAuthority_SignSSHAddUser(t *testing.T) { }) } } + +func TestAuthority_GetSSHRoots(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + user, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + host, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + + type fields struct { + sshCAUserCerts []ssh.PublicKey + sshCAHostCerts []ssh.PublicKey + } + tests := []struct { + name string + fields fields + want *SSHKeys + wantErr bool + }{ + {"ok", fields{[]ssh.PublicKey{user}, []ssh.PublicKey{host}}, &SSHKeys{UserKeys: []ssh.PublicKey{user}, HostKeys: []ssh.PublicKey{host}}, false}, + {"nil", fields{}, &SSHKeys{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := testAuthority(t) + a.sshCAUserCerts = tt.fields.sshCAUserCerts + a.sshCAHostCerts = tt.fields.sshCAHostCerts + + got, err := a.GetSSHRoots() + if (err != nil) != tt.wantErr { + t.Errorf("Authority.GetSSHRoots() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.GetSSHRoots() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthority_GetSSHFederation(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + user, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + host, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + + type fields struct { + sshCAUserFederatedCerts []ssh.PublicKey + sshCAHostFederatedCerts []ssh.PublicKey + } + tests := []struct { + name string + fields fields + want *SSHKeys + wantErr bool + }{ + {"ok", fields{[]ssh.PublicKey{user}, []ssh.PublicKey{host}}, &SSHKeys{UserKeys: []ssh.PublicKey{user}, HostKeys: []ssh.PublicKey{host}}, false}, + {"nil", fields{}, &SSHKeys{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := testAuthority(t) + a.sshCAUserFederatedCerts = tt.fields.sshCAUserFederatedCerts + a.sshCAHostFederatedCerts = tt.fields.sshCAHostFederatedCerts + + got, err := a.GetSSHFederation() + if (err != nil) != tt.wantErr { + t.Errorf("Authority.GetSSHFederation() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.GetSSHFederation() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthority_GetSSHConfig(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + user, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + userSigner, err := ssh.NewSignerFromSigner(key) + assert.FatalError(t, err) + userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + host, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + hostSigner, err := ssh.NewSignerFromSigner(key) + assert.FatalError(t, err) + hostB64 := base64.StdEncoding.EncodeToString(host.Marshal()) + + tmplConfig := &templates.Templates{ + SSH: &templates.SSHTemplates{ + User: []templates.Template{ + {Name: "known_host.tpl", Type: templates.File, TemplatePath: "./testdata/templates/known_hosts.tpl", Path: "ssh/known_host", Comment: "#"}, + }, + Host: []templates.Template{ + {Name: "ca.tpl", Type: templates.File, TemplatePath: "./testdata/templates/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, + }, + }, + Data: map[string]interface{}{ + "Step": &templates.Step{ + SSH: templates.StepSSH{ + UserKey: user, + HostKey: host, + }, + }, + }, + } + userOutput := []templates.Output{ + {Name: "known_host.tpl", Type: templates.File, Comment: "#", Path: "ssh/known_host", Content: []byte(fmt.Sprintf("@cert-authority * %s %s", host.Type(), hostB64))}, + } + hostOutput := []templates.Output{ + {Name: "ca.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/ca.pub", Content: []byte(user.Type() + " " + userB64)}, + } + + type fields struct { + templates *templates.Templates + userSigner ssh.Signer + hostSigner ssh.Signer + } + type args struct { + typ string + data map[string]string + } + tests := []struct { + name string + fields fields + args args + want []templates.Output + wantErr bool + }{ + {"user", fields{tmplConfig, userSigner, hostSigner}, args{"user", nil}, userOutput, false}, + {"host", fields{tmplConfig, userSigner, hostSigner}, args{"host", nil}, hostOutput, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := testAuthority(t) + a.config.Templates = tt.fields.templates + a.sshCAUserCertSignKey = tt.fields.userSigner + a.sshCAHostCertSignKey = tt.fields.hostSigner + + got, err := a.GetSSHConfig(tt.args.typ, tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("Authority.GetSSHConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.GetSSHConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/testdata/templates/ca.tpl b/authority/testdata/templates/ca.tpl new file mode 100644 index 00000000..21235dd5 --- /dev/null +++ b/authority/testdata/templates/ca.tpl @@ -0,0 +1,4 @@ +{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}} +{{- range .Step.SSH.UserFederatedKeys}} +{{.Type}} {{.Marshal | toString | b64enc}} +{{- end}} \ No newline at end of file diff --git a/authority/testdata/templates/known_hosts.tpl b/authority/testdata/templates/known_hosts.tpl new file mode 100644 index 00000000..acc0fafe --- /dev/null +++ b/authority/testdata/templates/known_hosts.tpl @@ -0,0 +1,4 @@ +@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} +{{- range .Step.SSH.HostFederatedKeys}} +@cert-authority * {{.Type}} {{.Marshal | toString | b64enc}} +{{- end}} \ No newline at end of file From d3b8d2e55ae83c770232156bede868ae4b74571f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Oct 2019 11:41:35 -0700 Subject: [PATCH 019/163] Fix known_host path. --- pki/templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pki/templates.go b/pki/templates.go index 99a2ac7d..0fc125d1 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -35,7 +35,7 @@ var sshTemplateData = map[string]string{ // and references the step known_hosts file "config.tpl": `Match exec "step ssh check-host %h" ForwardAgent yes - UserKnownHostsFile {{.User.StepPath}}/config/ssh/known_hosts`, + UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts`, // known_hosts.tpl authorizes the ssh hosts key "known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} From c2e20c78777f0f16b0aefa2fa86cfd88913afe61 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Oct 2019 12:18:29 -0700 Subject: [PATCH 020/163] Add tests for ssh authority methods. --- authority/ssh_test.go | 178 +++++++++++++++++++ authority/testdata/templates/config.tpl | 3 + authority/testdata/templates/error.tpl | 1 + authority/testdata/templates/include.tpl | 2 + authority/testdata/templates/sshd_config.tpl | 3 + 5 files changed, 187 insertions(+) create mode 100644 authority/testdata/templates/config.tpl create mode 100644 authority/testdata/templates/error.tpl create mode 100644 authority/testdata/templates/include.tpl create mode 100644 authority/testdata/templates/sshd_config.tpl diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 872278d6..629bc3b4 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -12,7 +12,9 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/templates" + "github.com/smallstep/cli/jose" "golang.org/x/crypto/ssh" ) @@ -383,6 +385,44 @@ func TestAuthority_GetSSHConfig(t *testing.T) { {Name: "ca.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/ca.pub", Content: []byte(user.Type() + " " + userB64)}, } + tmplConfigWithUserData := &templates.Templates{ + SSH: &templates.SSHTemplates{ + User: []templates.Template{ + {Name: "include.tpl", Type: templates.File, TemplatePath: "./testdata/templates/include.tpl", Path: "ssh/include", Comment: "#"}, + {Name: "config.tpl", Type: templates.File, TemplatePath: "./testdata/templates/config.tpl", Path: "ssh/config", Comment: "#"}, + }, + Host: []templates.Template{ + {Name: "sshd_config.tpl", Type: templates.File, TemplatePath: "./testdata/templates/sshd_config.tpl", Path: "/etc/ssh/sshd_config", Comment: "#"}, + }, + }, + Data: map[string]interface{}{ + "Step": &templates.Step{ + SSH: templates.StepSSH{ + UserKey: user, + HostKey: host, + }, + }, + }, + } + userOutputWithUserData := []templates.Output{ + {Name: "include.tpl", Type: templates.File, Comment: "#", Path: "ssh/include", Content: []byte("Host *\n\tInclude /home/user/.step/ssh/config")}, + {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("Match exec \"step ssh check-host %h\"\n\tForwardAgent yes\n\tUserKnownHostsFile /home/user/.step/ssh/known_hosts")}, + } + hostOutputWithUserData := []templates.Output{ + {Name: "sshd_config.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/sshd_config", Content: []byte("TrustedUserCAKeys /etc/ssh/ca.pub\nHostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub\nHostKey /etc/ssh/ssh_host_ecdsa_key")}, + } + + tmplConfigErr := &templates.Templates{ + SSH: &templates.SSHTemplates{ + User: []templates.Template{ + {Name: "error.tpl", Type: templates.File, TemplatePath: "./testdata/templates/error.tpl", Path: "ssh/error", Comment: "#"}, + }, + Host: []templates.Template{ + {Name: "error.tpl", Type: templates.File, TemplatePath: "./testdata/templates/error.tpl", Path: "ssh/error", Comment: "#"}, + }, + }, + } + type fields struct { templates *templates.Templates userSigner ssh.Signer @@ -400,7 +440,15 @@ func TestAuthority_GetSSHConfig(t *testing.T) { wantErr bool }{ {"user", fields{tmplConfig, userSigner, hostSigner}, args{"user", nil}, userOutput, false}, + {"user", fields{tmplConfig, userSigner, nil}, args{"user", nil}, userOutput, false}, {"host", fields{tmplConfig, userSigner, hostSigner}, args{"host", nil}, hostOutput, false}, + {"host", fields{tmplConfig, nil, hostSigner}, args{"host", nil}, hostOutput, false}, + {"userWithData", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{"user", map[string]string{"StepPath": "/home/user/.step"}}, userOutputWithUserData, false}, + {"hostWithData", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{"host", map[string]string{"Certificate": "ssh_host_ecdsa_key-cert.pub", "Key": "ssh_host_ecdsa_key"}}, hostOutputWithUserData, false}, + {"disabled", fields{tmplConfig, nil, nil}, args{"host", nil}, nil, true}, + {"badType", fields{tmplConfig, userSigner, hostSigner}, args{"bad", nil}, nil, true}, + {"userError", fields{tmplConfigErr, userSigner, hostSigner}, args{"user", nil}, nil, true}, + {"hostError", fields{tmplConfigErr, userSigner, hostSigner}, args{"host", map[string]string{"Function": "foo"}}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -420,3 +468,133 @@ func TestAuthority_GetSSHConfig(t *testing.T) { }) } } + +func TestAuthority_CheckSSHHost(t *testing.T) { + type fields struct { + exists bool + err error + } + type args struct { + principal string + } + tests := []struct { + name string + fields fields + args args + want bool + wantErr bool + }{ + {"true", fields{true, nil}, args{"foo.internal.com"}, true, false}, + {"false", fields{false, nil}, args{"foo.internal.com"}, false, false}, + {"notImplemented", fields{false, db.ErrNotImplemented}, args{"foo.internal.com"}, false, true}, + {"notImplemented", fields{true, db.ErrNotImplemented}, args{"foo.internal.com"}, false, true}, + {"internal", fields{false, fmt.Errorf("an error")}, args{"foo.internal.com"}, false, true}, + {"internal", fields{true, fmt.Errorf("an error")}, args{"foo.internal.com"}, false, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := testAuthority(t) + a.db = &MockAuthDB{ + isSSHHost: func(_ string) (bool, error) { + return tt.fields.exists, tt.fields.err + }, + } + got, err := a.CheckSSHHost(tt.args.principal) + if (err != nil) != tt.wantErr { + t.Errorf("Authority.CheckSSHHost() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Authority.CheckSSHHost() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSSHConfig_Validate(t *testing.T) { + key, err := jose.GenerateJWK("EC", "P-256", "", "sig", "", 0) + assert.FatalError(t, err) + + tests := []struct { + name string + sshConfig *SSHConfig + wantErr bool + }{ + {"nil", nil, false}, + {"ok", &SSHConfig{Keys: []*SSHPublicKey{{Type: "user", Key: key.Public()}}}, false}, + {"ok", &SSHConfig{Keys: []*SSHPublicKey{{Type: "host", Key: key.Public()}}}, false}, + {"badType", &SSHConfig{Keys: []*SSHPublicKey{{Type: "bad", Key: key.Public()}}}, true}, + {"badKey", &SSHConfig{Keys: []*SSHPublicKey{{Type: "user", Key: *key}}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + if err := tt.sshConfig.Validate(); (err != nil) != tt.wantErr { + t.Errorf("SSHConfig.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSSHPublicKey_Validate(t *testing.T) { + key, err := jose.GenerateJWK("EC", "P-256", "", "sig", "", 0) + assert.FatalError(t, err) + + type fields struct { + Type string + Federated bool + Key jose.JSONWebKey + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"user", fields{"user", true, key.Public()}, false}, + {"host", fields{"host", false, key.Public()}, false}, + {"empty", fields{"", true, key.Public()}, true}, + {"badType", fields{"bad", false, key.Public()}, true}, + {"badKey", fields{"user", false, *key}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SSHPublicKey{ + Type: tt.fields.Type, + Federated: tt.fields.Federated, + Key: tt.fields.Key, + } + if err := k.Validate(); (err != nil) != tt.wantErr { + t.Errorf("SSHPublicKey.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSSHPublicKey_PublicKey(t *testing.T) { + key, err := jose.GenerateJWK("EC", "P-256", "", "sig", "", 0) + assert.FatalError(t, err) + pub, err := ssh.NewPublicKey(key.Public().Key) + assert.FatalError(t, err) + + type fields struct { + publicKey ssh.PublicKey + } + tests := []struct { + name string + fields fields + want ssh.PublicKey + }{ + {"ok", fields{pub}, pub}, + {"nil", fields{nil}, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SSHPublicKey{ + publicKey: tt.fields.publicKey, + } + if got := k.PublicKey(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SSHPublicKey.PublicKey() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/testdata/templates/config.tpl b/authority/testdata/templates/config.tpl new file mode 100644 index 00000000..96233680 --- /dev/null +++ b/authority/testdata/templates/config.tpl @@ -0,0 +1,3 @@ +Match exec "step ssh check-host %h" + ForwardAgent yes + UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts \ No newline at end of file diff --git a/authority/testdata/templates/error.tpl b/authority/testdata/templates/error.tpl new file mode 100644 index 00000000..1661b955 --- /dev/null +++ b/authority/testdata/templates/error.tpl @@ -0,0 +1 @@ +Missing function {{Function}} \ No newline at end of file diff --git a/authority/testdata/templates/include.tpl b/authority/testdata/templates/include.tpl new file mode 100644 index 00000000..e7113727 --- /dev/null +++ b/authority/testdata/templates/include.tpl @@ -0,0 +1,2 @@ +Host * + Include {{.User.StepPath}}/ssh/config \ No newline at end of file diff --git a/authority/testdata/templates/sshd_config.tpl b/authority/testdata/templates/sshd_config.tpl new file mode 100644 index 00000000..5ce01fc4 --- /dev/null +++ b/authority/testdata/templates/sshd_config.tpl @@ -0,0 +1,3 @@ +TrustedUserCAKeys /etc/ssh/ca.pub +HostCertificate /etc/ssh/{{.User.Certificate}} +HostKey /etc/ssh/{{.User.Key}} \ No newline at end of file From 152b7c56a06fcdc3b83b6ecbf4cc8a0e6231d66d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Oct 2019 18:00:46 -0700 Subject: [PATCH 021/163] Add tests for templates and some fixes. --- templates/templates.go | 56 +++-- templates/templates_test.go | 420 ++++++++++++++++++++++++++++++++++++ 2 files changed, 457 insertions(+), 19 deletions(-) create mode 100644 templates/templates_test.go diff --git a/templates/templates.go b/templates/templates.go index ee4a5791..41a7e0f3 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -57,19 +57,21 @@ func (t *Templates) Validate() (err error) { // LoadAll preloads all templates in memory. It returns an error if an error is // found parsing at least one template. func LoadAll(t *Templates) (err error) { - if t.SSH != nil { - for _, tt := range t.SSH.User { - if err = tt.Load(); err != nil { - return err + if t != nil { + if t.SSH != nil { + for _, tt := range t.SSH.User { + if err = tt.Load(); err != nil { + return + } } - } - for _, tt := range t.SSH.Host { - if err = tt.Load(); err != nil { - return err + for _, tt := range t.SSH.Host { + if err = tt.Load(); err != nil { + return + } } } } - return nil + return } // SSHTemplates contains the templates defining ssh configuration files. @@ -113,18 +115,30 @@ func (t *Template) Validate() error { return nil case t.Name == "": return errors.New("template name cannot be empty") - case t.TemplatePath == "": + case t.Type != Snippet && t.Type != File && t.Type != Directory: + return errors.Errorf("invalid template type %s, it must be %s, %s, or %s", t.Type, Snippet, File, Directory) + case t.TemplatePath == "" && t.Type != Directory: return errors.New("template template cannot be empty") + case t.TemplatePath != "" && t.Type == Directory: + return errors.New("template template must be empty with directory type") case t.Path == "": return errors.New("template path cannot be empty") } - // Defaults - if t.Type == "" { - t.Type = Snippet - } - if t.Comment == "" { - t.Comment = "#" + if t.TemplatePath != "" { + // Check for file + st, err := os.Stat(config.StepAbs(t.TemplatePath)) + if err != nil { + return errors.Wrapf(err, "error reading %s", t.TemplatePath) + } + if st.IsDir() { + return errors.Errorf("error reading %s: is not a file", t.TemplatePath) + } + + // Defaults + if t.Comment == "" { + t.Comment = "#" + } } return nil @@ -133,7 +147,7 @@ func (t *Template) Validate() error { // Load loads the template in memory, returns an error if the parsing of the // template fails. func (t *Template) Load() error { - if t.Template == nil { + if t.Template == nil && t.Type != Directory { filename := config.StepAbs(t.TemplatePath) b, err := ioutil.ReadFile(filename) if err != nil { @@ -151,6 +165,10 @@ func (t *Template) Load() error { // Render executes the template with the given data and returns the rendered // version. func (t *Template) Render(data interface{}) ([]byte, error) { + if t.Type == Directory { + return nil, nil + } + if err := t.Load(); err != nil { return nil, err } @@ -172,8 +190,8 @@ func (t *Template) Output(data interface{}) (Output, error) { return Output{ Name: t.Name, Type: t.Type, - Comment: t.Comment, Path: t.Path, + Comment: t.Comment, Content: b, }, nil } @@ -182,8 +200,8 @@ func (t *Template) Output(data interface{}) (Output, error) { type Output struct { Name string `json:"name"` Type TemplateType `json:"type"` - Comment string `json:"comment"` Path string `json:"path"` + Comment string `json:"comment"` Content []byte `json:"content"` } diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 00000000..537fab4c --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,420 @@ +package templates + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/smallstep/assert" + "golang.org/x/crypto/ssh" +) + +func TestTemplates_Validate(t *testing.T) { + sshTemplates := &SSHTemplates{ + User: []Template{ + {Name: "known_host.tpl", Type: File, TemplatePath: "../authority/testdata/templates/known_hosts.tpl", Path: "ssh/known_host", Comment: "#"}, + }, + Host: []Template{ + {Name: "ca.tpl", Type: File, TemplatePath: "../authority/testdata/templates/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, + }, + } + type fields struct { + SSH *SSHTemplates + Data map[string]interface{} + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{sshTemplates, nil}, false}, + {"okWithData", fields{sshTemplates, map[string]interface{}{"Foo": "Bar"}}, false}, + {"badSSH", fields{&SSHTemplates{User: []Template{{}}}, nil}, true}, + {"badDataUser", fields{sshTemplates, map[string]interface{}{"User": "Bar"}}, true}, + {"badDataStep", fields{sshTemplates, map[string]interface{}{"Step": "Bar"}}, true}, + } + var nilValue *Templates + assert.NoError(t, nilValue.Validate()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpl := &Templates{ + SSH: tt.fields.SSH, + Data: tt.fields.Data, + } + if err := tmpl.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Templates.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSSHTemplates_Validate(t *testing.T) { + user := []Template{ + {Name: "include.tpl", Type: Snippet, TemplatePath: "../authority/testdata/templates/include.tpl", Path: "~/.ssh/config", Comment: "#"}, + } + host := []Template{ + {Name: "ca.tpl", Type: File, TemplatePath: "../authority/testdata/templates/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, + } + + type fields struct { + User []Template + Host []Template + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{user, host}, false}, + {"user", fields{user, nil}, false}, + {"host", fields{nil, host}, false}, + {"badUser", fields{[]Template{{}}, nil}, true}, + {"badHost", fields{nil, []Template{{}}}, true}, + } + var nilValue *SSHTemplates + assert.NoError(t, nilValue.Validate()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpl := &SSHTemplates{ + User: tt.fields.User, + Host: tt.fields.Host, + } + if err := tmpl.Validate(); (err != nil) != tt.wantErr { + t.Errorf("SSHTemplates.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestTemplate_Validate(t *testing.T) { + okPath := "~/.ssh/config" + okTmplPath := "../authority/testdata/templates/include.tpl" + + type fields struct { + Name string + Type TemplateType + TemplatePath string + Path string + Comment string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"okSnippet", fields{"include.tpl", Snippet, okTmplPath, okPath, "#"}, false}, + {"okFile", fields{"file.tpl", File, okTmplPath, okPath, "#"}, false}, + {"okDirectory", fields{"dir.tpl", Directory, "", "/tmp/dir", "#"}, false}, + {"badName", fields{"", Snippet, okTmplPath, okPath, "#"}, true}, + {"badType", fields{"include.tpl", "", okTmplPath, okPath, "#"}, true}, + {"badType", fields{"include.tpl", "foo", okTmplPath, okPath, "#"}, true}, + {"badTemplatePath", fields{"include.tpl", Snippet, "", okPath, "#"}, true}, + {"badTemplatePath", fields{"include.tpl", File, "", okPath, "#"}, true}, + {"badTemplatePath", fields{"include.tpl", Directory, okTmplPath, okPath, "#"}, true}, + {"badPath", fields{"include.tpl", Snippet, okTmplPath, "", "#"}, true}, + {"missingTemplate", fields{"include.tpl", Snippet, "./testdata/include.tpl", okTmplPath, "#"}, true}, + {"directoryTemplate", fields{"include.tpl", File, "../authority/testdata", okTmplPath, "#"}, true}, + } + var nilValue *Template + assert.NoError(t, nilValue.Validate()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpl := &Template{ + Name: tt.fields.Name, + Type: tt.fields.Type, + TemplatePath: tt.fields.TemplatePath, + Path: tt.fields.Path, + Comment: tt.fields.Comment, + } + if err := tmpl.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Template.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestLoadAll(t *testing.T) { + tmpl := &Templates{ + SSH: &SSHTemplates{ + User: []Template{ + {Name: "include.tpl", Type: Snippet, TemplatePath: "../authority/testdata/templates/include.tpl", Path: "~/.ssh/config", Comment: "#"}, + }, + Host: []Template{ + {Name: "ca.tpl", Type: File, TemplatePath: "../authority/testdata/templates/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"}, + }, + }, + } + + type args struct { + t *Templates + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"ok", args{tmpl}, false}, + {"empty", args{&Templates{}}, false}, + {"nil", args{nil}, false}, + {"badUser", args{&Templates{SSH: &SSHTemplates{User: []Template{{}}}}}, true}, + {"badHost", args{&Templates{SSH: &SSHTemplates{Host: []Template{{}}}}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := LoadAll(tt.args.t); (err != nil) != tt.wantErr { + t.Errorf("LoadAll() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestTemplate_Load(t *testing.T) { + type fields struct { + Name string + Type TemplateType + TemplatePath string + Path string + Comment string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{"include.tpl", Snippet, "../authority/testdata/templates/include.tpl", "~/.ssh/config", "#"}, false}, + {"error", fields{"error.tpl", Snippet, "../authority/testdata/templates/error.tpl", "/tmp/error", "#"}, true}, + {"missing", fields{"include.tpl", Snippet, "./testdata/include.tpl", "~/.ssh/config", "#"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpl := &Template{ + Name: tt.fields.Name, + Type: tt.fields.Type, + TemplatePath: tt.fields.TemplatePath, + Path: tt.fields.Path, + Comment: tt.fields.Comment, + } + if err := tmpl.Load(); (err != nil) != tt.wantErr { + t.Errorf("Template.Load() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestTemplate_Render(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + user, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + host, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + hostB64 := base64.StdEncoding.EncodeToString(host.Marshal()) + + data := map[string]interface{}{ + "Step": &Step{ + SSH: StepSSH{ + UserKey: user, + HostKey: host, + }, + }, + "User": map[string]string{ + "StepPath": "/tmp/.step", + }, + } + + type fields struct { + Name string + Type TemplateType + TemplatePath string + Path string + Comment string + } + type args struct { + data interface{} + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + {"snippet", fields{"include.tpl", Snippet, "../authority/testdata/templates/include.tpl", "~/.ssh/config", "#"}, args{data}, []byte("Host *\n\tInclude /tmp/.step/ssh/config"), false}, + {"file", fields{"known_hosts.tpl", File, "../authority/testdata/templates/known_hosts.tpl", "ssh/known_hosts", "#"}, args{data}, []byte(fmt.Sprintf("@cert-authority * %s %s", host.Type(), hostB64)), false}, + {"file", fields{"ca.tpl", File, "../authority/testdata/templates/ca.tpl", "/etc/ssh/ca.pub", "#"}, args{data}, []byte(fmt.Sprintf("%s %s", user.Type(), userB64)), false}, + {"directory", fields{"dir.tpl", Directory, "", "/tmp/dir", ""}, args{data}, nil, false}, + {"error", fields{"error.tpl", File, "../authority/testdata/templates/error.tpl", "/tmp/error", "#"}, args{data}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpl := &Template{ + Name: tt.fields.Name, + Type: tt.fields.Type, + TemplatePath: tt.fields.TemplatePath, + Path: tt.fields.Path, + Comment: tt.fields.Comment, + } + got, err := tmpl.Render(tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("Template.Render() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Template.Render() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTemplate_Output(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + user, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + userB64 := base64.StdEncoding.EncodeToString(user.Marshal()) + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + host, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + hostB64 := base64.StdEncoding.EncodeToString(host.Marshal()) + + data := map[string]interface{}{ + "Step": &Step{ + SSH: StepSSH{ + UserKey: user, + HostKey: host, + }, + }, + "User": map[string]string{ + "StepPath": "/tmp/.step", + }, + } + + type fields struct { + Name string + Type TemplateType + TemplatePath string + Path string + Comment string + } + type args struct { + data interface{} + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + {"snippet", fields{"include.tpl", Snippet, "../authority/testdata/templates/include.tpl", "~/.ssh/config", "#"}, args{data}, []byte("Host *\n\tInclude /tmp/.step/ssh/config"), false}, + {"file", fields{"known_hosts.tpl", File, "../authority/testdata/templates/known_hosts.tpl", "ssh/known_hosts", "#"}, args{data}, []byte(fmt.Sprintf("@cert-authority * %s %s", host.Type(), hostB64)), false}, + {"file", fields{"ca.tpl", File, "../authority/testdata/templates/ca.tpl", "/etc/ssh/ca.pub", "#"}, args{data}, []byte(fmt.Sprintf("%s %s", user.Type(), userB64)), false}, + {"directory", fields{"dir.tpl", Directory, "", "/tmp/dir", ""}, args{data}, nil, false}, + {"error", fields{"error.tpl", File, "../authority/testdata/templates/error.tpl", "/tmp/error", "#"}, args{data}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var want Output + if !tt.wantErr { + want = Output{ + Name: tt.fields.Name, + Type: tt.fields.Type, + Path: tt.fields.Path, + Comment: tt.fields.Comment, + Content: tt.want, + } + } + + tmpl := &Template{ + Name: tt.fields.Name, + Type: tt.fields.Type, + TemplatePath: tt.fields.TemplatePath, + Path: tt.fields.Path, + Comment: tt.fields.Comment, + } + got, err := tmpl.Output(tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("Template.Output() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Template.Output() = %v, want %v", got, want) + } + }) + } +} + +func TestOutput_Write(t *testing.T) { + dir, err := ioutil.TempDir("", "test-output-write") + assert.FatalError(t, err) + defer os.RemoveAll(dir) + + join := func(elem ...string) string { + elems := append([]string{dir}, elem...) + return filepath.Join(elems...) + } + assert.FatalError(t, os.Mkdir(join("bad"), 0644)) + + type fields struct { + Name string + Type TemplateType + Path string + Comment string + Content []byte + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"snippet", fields{"snippet", Snippet, join("snippet"), "#", []byte("some content")}, false}, + {"file", fields{"file", File, join("file"), "#", []byte("some content")}, false}, + {"snippetInDir", fields{"file", Snippet, join("dir", "snippets", "snippet"), "#", []byte("some content")}, false}, + {"fileInDir", fields{"file", File, join("dir", "files", "file"), "#", []byte("some content")}, false}, + {"directory", fields{"directory", Directory, join("directory"), "", nil}, false}, + {"snippetErr", fields{"snippet", Snippet, join("bad", "snippet"), "#", []byte("some content")}, true}, + {"fileErr", fields{"file", File, join("bad", "file"), "#", []byte("some content")}, true}, + {"directoryErr", fields{"directory", Directory, join("bad", "directory"), "", nil}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &Output{ + Name: tt.fields.Name, + Type: tt.fields.Type, + Comment: tt.fields.Comment, + Path: tt.fields.Path, + Content: tt.fields.Content, + } + if err := o.Write(); (err != nil) != tt.wantErr { + t.Errorf("Output.Write() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr { + st, err := os.Stat(o.Path) + if err != nil { + t.Errorf("os.Stat(%s) error = %v", o.Path, err) + } else { + if o.Type == Directory { + assert.True(t, st.IsDir()) + assert.Equals(t, os.ModeDir|os.FileMode(0700), st.Mode()) + } else { + assert.False(t, st.IsDir()) + assert.Equals(t, os.FileMode(0600), st.Mode()) + } + } + } + }) + } +} From f47516a15d211296ed9216c226f503cfc60a1862 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 22 Oct 2019 18:41:54 -0700 Subject: [PATCH 022/163] Use github.com/Masterminds/sprig/v3 --- go.mod | 8 +- go.sum | 216 ++++++----------------------------------- templates/templates.go | 2 +- 3 files changed, 39 insertions(+), 187 deletions(-) diff --git a/go.mod b/go.mod index c3ad03f0..4a0ff852 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 // indirect + github.com/Masterminds/sprig/v3 v3.0.0 github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect @@ -12,10 +13,13 @@ require ( github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible github.com/go-sql-driver/mysql v1.4.1 // indirect github.com/golangci/golangci-lint v1.18.0 // indirect + github.com/imdario/mergo v0.3.8 // indirect github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect github.com/manifoldco/promptui v0.3.1 // indirect + github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/newrelic/go-agent v1.11.0 github.com/pkg/errors v0.8.1 @@ -24,11 +28,11 @@ require ( github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/sirupsen/logrus v1.1.1 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.13.4-0.20191014220846-775cfe98ef76 + github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a go.etcd.io/bbolt v1.3.2 // indirect - golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a + golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect google.golang.org/appengine v1.5.0 // indirect diff --git a/go.sum b/go.sum index 0146bf69..a778bc74 100644 --- a/go.sum +++ b/go.sum @@ -5,110 +5,40 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM= github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk= +github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= +github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6Sts= github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= -github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540 h1:djv/qAomOVj8voCHt0M0OYwR/4vfDq1zNKSPKjJCexs= -github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= -github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= -github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= -github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= -github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= -github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= -github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= -github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= -github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= -github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= -github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= -github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= -github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= -github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= -github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= -github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= -github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= -github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/mock v1.0.0 h1:HzcpUG60pfl43n9d2qbdi/3l1uKpAmxlfWEPWtV/QxM= -github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w= -github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c h1:/7detzz5stiXWPzkTlPTzkBEIIE4WGpppBJYjKqBiPI= -github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= -github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= -github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= -github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= -github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= -github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU= -github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.18.0 h1:XmQgfcLofSG/6AsQuQqmLizB+3GggD+o6ObBG9L+VMM= -github.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg= -github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547 h1:fUdgm/BdKvwOHxg5AhNbkNRp2mSy8sxTXyBVs/laQHo= -github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= -github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI= -github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= -github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217 h1:En/tZdwhAn0JNwLuXzP3k2RVtMqMmOEK7Yu/g3tmtJE= -github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= -github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= -github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= -github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg= -github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= -github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +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/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= -github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -117,153 +47,71 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= -github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= -github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= -github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw= -github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= -github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= -github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= -github.com/smallstep/cli v0.13.4-0.20191014220846-775cfe98ef76 h1:iNh6x3czCb/01npI8/o4UdvfmTXJkjwVsBOfcXWvmAs= -github.com/smallstep/cli v0.13.4-0.20191014220846-775cfe98ef76/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= +github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= +github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= -github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= -github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= -github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= -github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= -github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE= -github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= -github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec h1:AmoEvWAO3nDx1MEcMzPh+GzOOIA5Znpv6++c7bePPY0= -github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= -github.com/ultraware/funlen v0.0.1 h1:UeC9tpM4wNWzUJfan8z9sFE4QCzjjzlCZmuJN+aOkH0= -github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= -github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U= -golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -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-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190909030654-5b82db07426d h1:PhtdWYteEBebOX7KXm4qkIAVSUTHQ883/2hRB92r9lk= -golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34 h1:duVSyluuJA+u0BnkcLR01smoLrGgDTfWt5c8ODYG8fU= -mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/templates/templates.go b/templates/templates.go index 41a7e0f3..cfd42e4c 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -7,7 +7,7 @@ import ( "path/filepath" "text/template" - "github.com/Masterminds/sprig" + "github.com/Masterminds/sprig/v3" "github.com/pkg/errors" "github.com/smallstep/cli/config" "github.com/smallstep/cli/utils" From 91ccc3802cf2e6b57fce09706aabc81ac38c16cb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:37:51 -0700 Subject: [PATCH 023/163] Fix lint error. --- authority/authority.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 34eee14b..db038d85 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -195,15 +195,11 @@ func (a *Authority) init() error { if a.config.SSH != nil { if a.sshCAHostCertSignKey != nil { vars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey() - for _, k := range a.sshCAHostFederatedCerts[1:] { - vars.SSH.HostFederatedKeys = append(vars.SSH.HostFederatedKeys, k) - } + vars.SSH.HostFederatedKeys = append(vars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...) } if a.sshCAUserCertSignKey != nil { vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey() - for _, k := range a.sshCAUserFederatedCerts[1:] { - vars.SSH.UserFederatedKeys = append(vars.SSH.UserFederatedKeys, k) - } + vars.SSH.UserFederatedKeys = append(vars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...) } } t.Data["Step"] = vars From e8ad06ef35d673ed18c19da0e49f8e636477fe2b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:40:37 -0700 Subject: [PATCH 024/163] Upgrade go-jose to 2.4.0. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4a0ff852..ad444c36 100644 --- a/go.mod +++ b/go.mod @@ -37,5 +37,5 @@ require ( golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect google.golang.org/appengine v1.5.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/square/go-jose.v2 v2.3.1 + gopkg.in/square/go-jose.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index a778bc74..c4f23be8 100644 --- a/go.sum +++ b/go.sum @@ -111,7 +111,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= +gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From af64bf8d964d220a61e5c490123251f25aff1418 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:42:38 -0700 Subject: [PATCH 025/163] Upgrade cli version. --- go.mod | 14 +------------- go.sum | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index ad444c36..2cf8744c 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,7 @@ module github.com/smallstep/certificates go 1.13 require ( - github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 // indirect github.com/Masterminds/sprig/v3 v3.0.0 - github.com/chzyer/logex v1.1.10 // indirect - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - github.com/dgraph-io/badger v1.5.3 // indirect - github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible github.com/go-sql-driver/mysql v1.4.1 // indirect github.com/golangci/golangci-lint v1.18.0 // indirect @@ -24,18 +18,12 @@ require ( github.com/newrelic/go-agent v1.11.0 github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 - github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 // indirect - github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/sirupsen/logrus v1.1.1 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df + github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a - go.etcd.io/bbolt v1.3.2 // indirect golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect - google.golang.org/appengine v1.5.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/square/go-jose.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index c4f23be8..ed11d4e3 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,16 @@ github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVG github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= +github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= +github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/corpix/uarand v0.0.0-20170903190822-2b8494104d86/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -30,21 +34,28 @@ github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZp github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= 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/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= @@ -65,6 +76,7 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= @@ -75,10 +87,17 @@ github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go.mod h1:/WOAB2LkcjkEbKG5rDol+A22Lp3UsttkLPLkY7tVtuk= +github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= +github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3KzjZKH3ojxLQolUnHSXuomfFjm+/KbIdpY= +github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= +github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= +github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= +github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -88,6 +107,7 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -109,9 +129,13 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= From ba9eb47818db96744b83370fb4f352c5d661295d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:58:48 -0700 Subject: [PATCH 026/163] Use nosql version with go mod. --- go.mod | 2 +- go.sum | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2cf8744c..74201486 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/sirupsen/logrus v1.1.1 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 - github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 + github.com/smallstep/nosql v0.1.1 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 diff --git a/go.sum b/go.sum index ed11d4e3..845b3ade 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3 github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= +github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= +github.com/smallstep/nosql v0.1.1/go.mod h1:qyxCqeyGwkuM6bfJSY3sg+aiXEiD0GbQOPzIF8/ZD8Q= github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= @@ -116,6 +118,7 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRi golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= @@ -124,6 +127,8 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= +golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= From e5da24f269413301fef7b40a8662d8f7a6c080ee Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 18:36:02 -0700 Subject: [PATCH 027/163] Fix list of user ssh public keys. --- authority/authority.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/authority.go b/authority/authority.go index db038d85..d53d8d1b 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -154,7 +154,7 @@ func (a *Authority) init() error { return errors.Wrap(err, "error creating ssh signer") } // Append public key to list of user certs - a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAHostCertSignKey.PublicKey()) + a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAUserCertSignKey.PublicKey()) a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey()) } From cf2b9301c0c31f6c1df14241b057f6dc52867e85 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 25 Oct 2019 12:23:52 -0700 Subject: [PATCH 028/163] Change default user duration to 16h. --- authority/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/config.go b/authority/config.go index 30343b5f..5ec83477 100644 --- a/authority/config.go +++ b/authority/config.go @@ -38,7 +38,7 @@ var ( DisableRenewal: &defaultDisableRenewal, MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &provisioner.Duration{Duration: 4 * time.Hour}, + DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour}, MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, From 605d39e4e8c79afaffea57f21f775ab17be2f432 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 18:07:52 -0800 Subject: [PATCH 029/163] Add proxycommand and new lines to templates. --- ca/client.go | 3 +-- go.sum | 1 + pki/templates.go | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ca/client.go b/ca/client.go index 160bfe52..509ebb7c 100644 --- a/ca/client.go +++ b/ca/client.go @@ -21,11 +21,10 @@ import ( "strconv" "strings" - "github.com/smallstep/certificates/authority/provisioner" - "github.com/pkg/errors" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/config" "github.com/smallstep/cli/crypto/x509util" "gopkg.in/square/go-jose.v2/jwt" diff --git a/go.sum b/go.sum index 845b3ade..50422506 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,7 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go. github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= +github.com/smallstep/cli v0.13.3 h1:S29UydCtDVy0QQBtGdatq064tnks1/0DYxxnEtNiQpc= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3KzjZKH3ojxLQolUnHSXuomfFjm+/KbIdpY= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= diff --git a/pki/templates.go b/pki/templates.go index 0fc125d1..633b762c 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -35,13 +35,15 @@ var sshTemplateData = map[string]string{ // and references the step known_hosts file "config.tpl": `Match exec "step ssh check-host %h" ForwardAgent yes - UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts`, + UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts + ProxyCommand step ssh proxycommand %r %h %p`, // known_hosts.tpl authorizes the ssh hosts key "known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} {{- range .Step.SSH.HostFederatedKeys}} @cert-authority * {{.Type}} {{.Marshal | toString | b64enc}} -{{- end}}`, +{{- end}} +`, // sshd_config.tpl adds the configuration to support certificates "sshd_config.tpl": `TrustedUserCAKeys /etc/ssh/ca.pub @@ -52,7 +54,8 @@ HostKey /etc/ssh/{{.User.Key}}`, "ca.tpl": `{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}} {{- range .Step.SSH.UserFederatedKeys}} {{.Type}} {{.Marshal | toString | b64enc}} -{{- end}}`, +{{- end}} +`, } // getTemplates returns all the templates enabled From 45d94fa4bd989b2f6b79ff23684d025c24a5236a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 18:09:10 -0800 Subject: [PATCH 030/163] Update dependencies. --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 74201486..d95151d4 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.1.1 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 + github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 github.com/smallstep/nosql v0.1.1 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 diff --git a/go.sum b/go.sum index 50422506..71810eae 100644 --- a/go.sum +++ b/go.sum @@ -88,12 +88,15 @@ github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go.mod h1:/WOAB2LkcjkEbKG5rDol+A22Lp3UsttkLPLkY7tVtuk= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go.mod h1:043iBnsMvNhQ+QFwSh0N6JR3H2yamHPPAc78vCf+8Tc= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= github.com/smallstep/cli v0.13.3 h1:S29UydCtDVy0QQBtGdatq064tnks1/0DYxxnEtNiQpc= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3KzjZKH3ojxLQolUnHSXuomfFjm+/KbIdpY= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= +github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 h1:kHHsScwMUDlepa7LkxR55r6NT9ra+U9KsP6qJGZb5jM= +github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03/go.mod h1:dklnISxr+GzUmurBngEF9Jvj0aI9KK5uVgZwOdFniNs= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From 491286e2b8d408a483efd6827b4791561788cb14 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 18:30:03 -0800 Subject: [PATCH 031/163] Make templates public. --- pki/templates.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pki/templates.go b/pki/templates.go index 633b762c..8f8b26b2 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -11,9 +11,9 @@ import ( "github.com/smallstep/cli/utils" ) -// sshTemplates contains the configuration of default templates used on ssh. +// SSHTemplates contains the configuration of default templates used on ssh. // Relative paths are relative to the StepPath. -var sshTemplates = &templates.SSHTemplates{ +var SSHTemplates = &templates.SSHTemplates{ User: []templates.Template{ {Name: "include.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/include.tpl", Path: "~/.ssh/config", Comment: "#"}, {Name: "config.tpl", Type: templates.File, TemplatePath: "templates/ssh/config.tpl", Path: "ssh/config", Comment: "#"}, @@ -25,8 +25,8 @@ var sshTemplates = &templates.SSHTemplates{ }, } -// sshTemplateData contains the data of the default templates used on ssh. -var sshTemplateData = map[string]string{ +// SSHTemplateData contains the data of the default templates used on ssh. +var SSHTemplateData = map[string]string{ // include.tpl adds the step ssh config file "include.tpl": `Host * Include {{.User.StepPath}}/ssh/config`, @@ -65,7 +65,7 @@ func (p *PKI) getTemplates() *templates.Templates { } return &templates.Templates{ - SSH: sshTemplates, + SSH: SSHTemplates, Data: map[string]interface{}{}, } } @@ -88,7 +88,7 @@ func generateTemplates(t *templates.Templates) error { } // Create all templates for _, t := range t.SSH.User { - data, ok := sshTemplateData[t.Name] + data, ok := SSHTemplateData[t.Name] if !ok { return errors.Errorf("template %s does not exists", t.Name) } @@ -97,7 +97,7 @@ func generateTemplates(t *templates.Templates) error { } } for _, t := range t.SSH.Host { - data, ok := sshTemplateData[t.Name] + data, ok := SSHTemplateData[t.Name] if !ok { return errors.Errorf("template %s does not exists", t.Name) } From 5092e8cfc2676c91eeb47fc896966cd905a611e6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 19:51:03 -0800 Subject: [PATCH 032/163] Go mod tidy. --- go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/go.sum b/go.sum index 71810eae..f62a359c 100644 --- a/go.sum +++ b/go.sum @@ -92,7 +92,6 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go. github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= -github.com/smallstep/cli v0.13.3 h1:S29UydCtDVy0QQBtGdatq064tnks1/0DYxxnEtNiQpc= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3KzjZKH3ojxLQolUnHSXuomfFjm+/KbIdpY= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 h1:kHHsScwMUDlepa7LkxR55r6NT9ra+U9KsP6qJGZb5jM= From 64b69374fa00163050335550dba64605c7b97c35 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 25 Oct 2019 13:47:49 -0700 Subject: [PATCH 033/163] Add SSH getHosts api --- api/api.go | 1 + api/ssh.go | 18 +++++++++++++++ authority/ssh.go | 10 +++++++++ ca/client.go | 18 +++++++++++++++ db/db.go | 58 ++++++++++++++++++++++++++++++++++++++---------- db/simple.go | 5 +++++ 6 files changed, 98 insertions(+), 12 deletions(-) diff --git a/api/api.go b/api/api.go index 0284167f..ad8fbb98 100644 --- a/api/api.go +++ b/api/api.go @@ -257,6 +257,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost) + r.MethodFunc("POST", "/ssh/get-hosts", h.SSHGetHosts) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) diff --git a/api/ssh.go b/api/ssh.go index e3101b8b..11d59712 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -21,6 +21,7 @@ type SSHAuthority interface { GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) CheckSSHHost(principal string) (bool, error) + GetSSHHosts() ([]string, error) } // SSHSignRequest is the request body of an SSH certificate request. @@ -66,6 +67,11 @@ type SSHCertificate struct { *ssh.Certificate `json:"omitempty"` } +// SSHGetHostsResponse +type SSHGetHostsResponse struct { + Hosts []string `json:"hosts"` +} + // MarshalJSON implements the json.Marshaler interface. Returns a quoted, // base64 encoded, openssh wire format version of the certificate. func (c SSHCertificate) MarshalJSON() ([]byte, error) { @@ -369,3 +375,15 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { Exists: exists, }) } + +// SSHGetHosts is the HTTP handler that returns a list of valid ssh hosts. +func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { + hosts, err := h.Authority.GetSSHHosts() + if err != nil { + WriteError(w, InternalServerError(err)) + return + } + JSON(w, &SSHGetHostsResponse{ + Hosts: hosts, + }) +} diff --git a/authority/ssh.go b/authority/ssh.go index 1c3f39bb..74833256 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -369,6 +369,16 @@ func (a *Authority) CheckSSHHost(principal string) (bool, error) { return exists, nil } +// GetSSHHosts returns a list of valid host principals. +func (a *Authority) GetSSHHosts() ([]string, error) { + ps, err := a.db.GetSSHHostPrincipals() + if err != nil { + return nil, err + } + + return ps, nil +} + func (a *Authority) getAddUserPrincipal() (cmd string) { if a.config.SSH.AddUserPrincipal == "" { return SSHAddUserPrincipal diff --git a/ca/client.go b/ca/client.go index 509ebb7c..8cefe4c0 100644 --- a/ca/client.go +++ b/ca/client.go @@ -611,6 +611,24 @@ func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, return &check, nil } +// SSHGetHostPrincipals performs the POST /ssh/check-host request to the CA with the +// given principal. +func (c *Client) SSHGetHostPrincipals() (*api.SSHGetHostsResponse, error) { + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/get-hosts"}) + resp, err := c.client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var hosts api.SSHGetHostsResponse + if err := readJSON(resp.Body, &hosts); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &hosts, nil +} + // RootFingerprint is a helper method that returns the current root fingerprint. // It does an health connection and gets the fingerprint from the TLS verified // chains. diff --git a/db/db.go b/db/db.go index 17e5c209..2aa093b4 100644 --- a/db/db.go +++ b/db/db.go @@ -14,12 +14,13 @@ import ( ) var ( - certsTable = []byte("x509_certs") - revokedCertsTable = []byte("revoked_x509_certs") - usedOTTTable = []byte("used_ott") - sshCertsTable = []byte("ssh_certs") - sshHostsTable = []byte("ssh_hosts") - sshUsersTable = []byte("ssh_users") + certsTable = []byte("x509_certs") + revokedCertsTable = []byte("revoked_x509_certs") + usedOTTTable = []byte("used_ott") + sshCertsTable = []byte("ssh_certs") + sshHostsTable = []byte("ssh_hosts") + sshUsersTable = []byte("ssh_users") + sshHostPrincipalsTable = []byte("ssh_host_principals") ) // ErrAlreadyExists can be returned if the DB attempts to set a key that has @@ -42,6 +43,7 @@ type AuthDB interface { UseToken(id, tok string) (bool, error) IsSSHHost(name string) (bool, error) StoreSSHCertificate(crt *ssh.Certificate) error + GetSSHHostPrincipals() ([]string, error) Shutdown() error } @@ -160,19 +162,32 @@ func (db *DB) IsSSHHost(principal string) (bool, error) { return true, nil } +type sshHostPrincipalData struct { + Serial string + Expiry uint64 +} + // StoreSSHCertificate stores an SSH certificate. func (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error { - var table []byte serial := strconv.FormatUint(crt.Serial, 10) tx := new(database.Tx) tx.Set(sshCertsTable, []byte(serial), crt.Marshal()) if crt.CertType == ssh.HostCert { - table = sshHostsTable + for _, p := range crt.ValidPrincipals { + hostPrincipalData, err := json.Marshal(sshHostPrincipalData{ + Serial: serial, + Expiry: crt.ValidBefore, + }) + if err != nil { + return err + } + tx.Set(sshHostsTable, []byte(strings.ToLower(p)), []byte(serial)) + tx.Set(sshHostPrincipalsTable, []byte(strings.ToLower(p)), hostPrincipalData) + } } else { - table = sshUsersTable - } - for _, p := range crt.ValidPrincipals { - tx.Set(table, []byte(strings.ToLower(p)), []byte(serial)) + for _, p := range crt.ValidPrincipals { + tx.Set(sshUsersTable, []byte(strings.ToLower(p)), []byte(serial)) + } } if err := db.Update(tx); err != nil { return errors.Wrap(err, "database Update error") @@ -181,6 +196,25 @@ func (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error { } +// GetSSHHostPrincipals gets a list of all valid host principals. +func (db *DB) GetSSHHostPrincipals() ([]string, error) { + entries, err := db.List(sshHostPrincipalsTable) + if err != nil { + return nil, err + } + var principals []string + for _, e := range entries { + var data sshHostPrincipalData + if err := json.Unmarshal(e.Value, &data); err != nil { + return nil, err + } + if time.Unix(int64(data.Expiry), 0).After(time.Now()) { + principals = append(principals, string(e.Key)) + } + } + return principals, nil +} + // Shutdown sends a shutdown message to the database. func (db *DB) Shutdown() error { if db.isUp { diff --git a/db/simple.go b/db/simple.go index 7989de44..b0733d8d 100644 --- a/db/simple.go +++ b/db/simple.go @@ -69,6 +69,11 @@ func (s *SimpleDB) StoreSSHCertificate(crt *ssh.Certificate) error { return ErrNotImplemented } +// GetSSHHostPrincipals returns a "NotImplemented" error. +func (s *SimpleDB) GetSSHHostPrincipals() ([]string, error) { + return nil, ErrNotImplemented +} + // Shutdown returns nil func (s *SimpleDB) Shutdown() error { return nil From 36fc7fa174013a4449cb4261728f401222770730 Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 26 Oct 2019 17:40:26 -0700 Subject: [PATCH 034/163] get-hosts fixes --- api/api.go | 2 +- ca/client.go | 5 ++--- db/db.go | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/api.go b/api/api.go index ad8fbb98..9ea430c2 100644 --- a/api/api.go +++ b/api/api.go @@ -257,7 +257,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost) - r.MethodFunc("POST", "/ssh/get-hosts", h.SSHGetHosts) + r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) diff --git a/ca/client.go b/ca/client.go index 8cefe4c0..35e07758 100644 --- a/ca/client.go +++ b/ca/client.go @@ -611,9 +611,8 @@ func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, return &check, nil } -// SSHGetHostPrincipals performs the POST /ssh/check-host request to the CA with the -// given principal. -func (c *Client) SSHGetHostPrincipals() (*api.SSHGetHostsResponse, error) { +// SSHGetHosts performs the GET /ssh/get-hosts request to the CA. +func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) { u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/get-hosts"}) resp, err := c.client.Get(u.String()) if err != nil { diff --git a/db/db.go b/db/db.go index 2aa093b4..5195e1e3 100644 --- a/db/db.go +++ b/db/db.go @@ -67,7 +67,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, - sshCertsTable, sshHostsTable, sshUsersTable, + sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { From b5f15531d8ebf7db2d4ec6daf10fff2f0446a029 Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 26 Oct 2019 17:40:43 -0700 Subject: [PATCH 035/163] sshpop first pass --- authority/provisioner/provisioner.go | 6 + authority/provisioner/sshpop.go | 307 +++++++++++++++++++++++++++ go.mod | 9 - go.sum | 123 ++++++++++- 4 files changed, 433 insertions(+), 12 deletions(-) create mode 100644 authority/provisioner/sshpop.go diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 1be21854..0ed56832 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -90,6 +90,8 @@ const ( TypeX5C Type = 7 // TypeK8sSA is used to indicate the X5C provisioners. TypeK8sSA Type = 8 + // TypeSSHPOP is used to indicate the SSHPOP provisioners. + TypeSSHPOP Type = 9 // RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map. RevokeAudienceKey = "revoke" @@ -116,6 +118,8 @@ func (t Type) String() string { return "X5C" case TypeK8sSA: return "K8sSA" + case TypeSSHPOP: + return "SSHPOP" default: return "" } @@ -169,6 +173,8 @@ func (l *List) UnmarshalJSON(data []byte) error { p = &X5C{} case "k8ssa": p = &K8sSA{} + case "sshpop": + p = &SSHPOP{} default: // Skip unsupported provisioners. A client using this method may be // compiled with a version of smallstep/certificates that does not diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go new file mode 100644 index 00000000..a49d32e8 --- /dev/null +++ b/authority/provisioner/sshpop.go @@ -0,0 +1,307 @@ +package provisioner + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/cli/jose" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" +) + +// sshPOPPayload extends jwt.Claims with step attributes. +type sshPOPPayload struct { + jose.Claims + SANs []string `json:"sans,omitempty"` + Step *stepPayload `json:"step,omitempty"` + sshCert *ssh.Certificate +} + +// SSHPOP is the default provisioner, an entity that can sign tokens necessary for +// signature requests. +type SSHPOP struct { + Type string `json:"type"` + Name string `json:"name"` + PubKeys []byte `json:"pubKeys"` + Claims *Claims `json:"claims,omitempty"` + claimer *Claimer + audiences Audiences + sshPubKeys []ssh.PublicKey +} + +// GetID returns the provisioner unique identifier. The name and credential id +// should uniquely identify any SSH-POP provisioner. +func (p *SSHPOP) GetID() string { + return "sshpop/" + p.Name +} + +// GetTokenID returns the identifier of the token. +func (p *SSHPOP) GetTokenID(ott string) (string, error) { + // Validate payload + token, err := jose.ParseSigned(ott) + if err != nil { + return "", errors.Wrap(err, "error parsing token") + } + + // Get claims w/out verification. We need to look up the provisioner + // key in order to verify the claims and we need the issuer from the claims + // before we can look up the provisioner. + var claims jose.Claims + if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { + return "", errors.Wrap(err, "error verifying claims") + } + return claims.ID, nil +} + +// GetName returns the name of the provisioner. +func (p *SSHPOP) GetName() string { + return p.Name +} + +// GetType returns the type of provisioner. +func (p *SSHPOP) GetType() Type { + return TypeSSHPOP +} + +// GetEncryptedKey returns the base provisioner encrypted key if it's defined. +func (p *SSHPOP) GetEncryptedKey() (string, string, bool) { + return "", "", false +} + +// Init initializes and validates the fields of a SSHPOP type. +func (p *SSHPOP) Init(config Config) error { + switch { + case p.Type == "": + return errors.New("provisioner type cannot be empty") + case p.Name == "": + return errors.New("provisioner name cannot be empty") + case len(p.PubKeys) == 0: + return errors.New("provisioner root(s) cannot be empty") + } + + var ( + block *pem.Block + rest = p.PubKeys + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + key, err := pemutil.ParseKey(pem.EncodeToMemory(block)) + if err != nil { + return errors.Wrapf(err, "error parsing public key in provisioner %s", p.GetID()) + } + switch q := key.(type) { + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + sshKey, err := ssh.NewPublicKey(key) + if err != nil { + return errors.Wrap(err, "error converting pub key to SSH pub key") + } + p.sshPubKeys = append(p.sshPubKeys, sshKey) + default: + return errors.Errorf("Unexpected public key type %T in provisioner %s", q, p.GetID()) + } + } + + // Verify that at least one root was found. + if len(p.sshPubKeys) == 0 { + return errors.Errorf("no root public keys found in pub keys attribute for provisioner %s", p.GetName()) + } + + // Update claims with global ones + var err error + if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil { + return err + } + + p.audiences = config.Audiences.WithFragment(p.GetID()) + return nil +} + +// authorizeToken performs common jwt authorization actions and returns the +// claims for case specific downstream parsing. +// e.g. a Sign request will auth/validate different fields than a Revoke request. +func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) { + jwt, err := jose.ParseSigned(token) + if err != nil { + return nil, errors.Wrapf(err, "error parsing token") + } + + encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"] + if !ok { + return nil, errors.New("token missing sshpop header") + } + encodedSSHCertStr, ok := encodedSSHCert.(string) + if !ok { + return nil, errors.New("error unexpected type for sshpop header") + } + sshCertBytes, err := base64.RawURLEncoding.DecodeString(encodedSSHCertStr) + if err != nil { + return nil, errors.Wrap(err, "error decoding sshpop header") + } + sshPub, err := ssh.ParsePublicKey(sshCertBytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing ssh public key") + } + sshCert, ok := sshPub.(*ssh.Certificate) + if !ok { + return nil, errors.New("error converting ssh public key to ssh certificate") + } + + data := bytesForSigning(sshCert) + var found bool + for _, k := range p.sshPubKeys { + if err = (&ssh.Certificate{Key: k}).Verify(data, sshCert.Signature); err == nil { + found = true + } + } + if !found { + return nil, errors.New("error: provisioner could could not verify the sshpop header certificate") + } + + // Using the leaf certificates key to validate the claims accomplishes two + // things: + // 1. Asserts that the private key used to sign the token corresponds + // to the public certificate in the `sshpop` header of the token. + // 2. Asserts that the claims are valid - have not been tampered with. + var claims sshPOPPayload + if err = jwt.Claims(sshCert.Key, &claims); err != nil { + return nil, errors.Wrap(err, "error parsing claims") + } + + // According to "rfc7519 JSON Web Token" acceptable skew should be no + // more than a few minutes. + if err = claims.ValidateWithLeeway(jose.Expected{ + Issuer: p.Name, + Time: time.Now().UTC(), + }, time.Minute); err != nil { + return nil, errors.Wrapf(err, "invalid token") + } + + // validate audiences with the defaults + if !matchesAudience(claims.Audience, audiences) { + return nil, errors.New("invalid token: invalid audience claim (aud)") + } + + if claims.Subject == "" { + return nil, errors.New("token subject cannot be empty") + } + + claims.sshCert = sshCert + return &claims, nil +} + +// AuthorizeRevoke returns an error if the provisioner does not have rights to +// revoke the certificate with serial number in the `sub` property. +func (p *SSHPOP) AuthorizeRevoke(token string) error { + _, err := p.authorizeToken(token, p.audiences.Revoke) + return err +} + +// AuthorizeSign validates the given token. +func (p *SSHPOP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { + claims, err := p.authorizeToken(token, p.audiences.Sign) + if err != nil { + return nil, err + } + + // Check for SSH sign-ing request. + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + return p.authorizeSSHSign(claims) + } + + // NOTE: This is for backwards compatibility with older versions of cli + // and certificates. Older versions added the token subject as the only SAN + // in a CSR by default. + if len(claims.SANs) == 0 { + claims.SANs = []string{claims.Subject} + } + + dnsNames, ips, emails := x509util.SplitSANs(claims.SANs) + + return []SignOption{ + // modifiers / withOptions + newProvisionerExtensionOption(TypeSSHPOP, p.Name, ""), + profileLimitDuration{p.claimer.DefaultTLSCertDuration(), time.Unix(int64(claims.sshCert.ValidBefore), 0)}, + // validators + commonNameValidator(claims.Subject), + defaultPublicKeyValidator{}, + dnsNamesValidator(dnsNames), + emailAddressesValidator(emails), + ipAddressesValidator(ips), + newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + }, nil +} + +// AuthorizeRenewal returns an error if the renewal is disabled. +func (p *SSHPOP) AuthorizeRenewal(cert *x509.Certificate) error { + if p.claimer.IsDisableRenewal() { + return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + } + return nil +} + +// authorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *SSHPOP) authorizeSSHSign(claims *sshPOPPayload) ([]SignOption, error) { + if claims.Step == nil || claims.Step.SSH == nil { + return nil, errors.New("authorization token must be an SSH provisioning token") + } + opts := claims.Step.SSH + signOptions := []SignOption{ + // validates user's SSHOptions with the ones in the token + sshCertificateOptionsValidator(*opts), + // set the key id to the token subject + sshCertificateKeyIDModifier(claims.Subject), + } + + // Add modifiers from custom claims + if opts.CertType != "" { + signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) + } + if len(opts.Principals) > 0 { + signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals)) + } + t := now() + if !opts.ValidAfter.IsZero() { + signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) + } + if !opts.ValidBefore.IsZero() { + signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) + } + + // Default to a user certificate with no principals if not set + signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) + + return append(signOptions, + // Set the default extensions. + &sshDefaultExtensionModifier{}, + // Checks the validity bounds, and set the validity if has not been set. + sshLimitValidityModifier(p.claimer, time.Unix(int64(claims.sshCert.ValidBefore), 0)), + // Validate public key. + &sshDefaultPublicKeyValidator{}, + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate + &sshCertificateDefaultValidator{}, + ), nil +} + +func bytesForSigning(cert *ssh.Certificate) []byte { + c2 := *cert + c2.Signature = nil + out := c2.Marshal() + // Drop trailing signature length. + return out[:len(out)-4] +} diff --git a/go.mod b/go.mod index d95151d4..f5d191f3 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,7 @@ go 1.13 require ( github.com/Masterminds/sprig/v3 v3.0.0 github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible - github.com/go-sql-driver/mysql v1.4.1 // indirect github.com/golangci/golangci-lint v1.18.0 // indirect - github.com/imdario/mergo v0.3.8 // indirect - github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect - github.com/manifoldco/promptui v0.3.1 // indirect - github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-isatty v0.0.4 // indirect github.com/newrelic/go-agent v1.11.0 github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 diff --git a/go.sum b/go.sum index f62a359c..ab8bc2e9 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,15 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkBy 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/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM= -github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk= github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= +github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM= +github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -28,27 +28,76 @@ github.com/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6St github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= +github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg= +github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= +github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +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/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -58,31 +107,54 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= +github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= @@ -103,47 +175,92 @@ github.com/smallstep/nosql v0.1.1/go.mod h1:qyxCqeyGwkuM6bfJSY3sg+aiXEiD0GbQOPzI github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= +github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= From a9ea292bd480be818eb77592f1ffaea8419ab8e8 Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 28 Oct 2019 11:50:43 -0700 Subject: [PATCH 036/163] sshpop provisioner + ssh renew | revoke | rekey first pass --- api/api.go | 5 +- api/revoke.go | 16 +- api/ssh.go | 5 +- api/sshRekey.go | 78 ++++++++ api/sshRenew.go | 68 +++++++ api/sshRevoke.go | 98 ++++++++++ authority/authority.go | 25 +++ authority/authorize.go | 62 +++--- authority/config.go | 32 ++-- authority/provisioner/acme.go | 22 +-- authority/provisioner/aws.go | 34 ++-- authority/provisioner/azure.go | 59 +++--- authority/provisioner/gcp.go | 29 ++- authority/provisioner/jwk.go | 39 ++-- authority/provisioner/k8sSA.go | 3 +- authority/provisioner/method.go | 30 ++- authority/provisioner/noop.go | 22 ++- authority/provisioner/oidc.go | 43 +++-- authority/provisioner/provisioner.go | 123 ++++++++++-- authority/provisioner/sshpop.go | 237 ++++++++++------------- authority/provisioner/x5c.go | 7 +- authority/ssh.go | 275 +++++++++++++++++++++++++++ authority/tls.go | 72 +++++-- ca/client.go | 66 +++++++ db/db.go | 45 +++++ db/simple.go | 10 + 26 files changed, 1176 insertions(+), 329 deletions(-) create mode 100644 api/sshRekey.go create mode 100644 api/sshRenew.go create mode 100644 api/sshRevoke.go diff --git a/api/api.go b/api/api.go index 9ea430c2..68334dcb 100644 --- a/api/api.go +++ b/api/api.go @@ -38,7 +38,7 @@ type Authority interface { LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error) LoadProvisionerByID(string) (provisioner.Interface, error) GetProvisioners(cursor string, limit int) (provisioner.List, string, error) - Revoke(*authority.RevokeOptions) error + Revoke(context.Context, *authority.RevokeOptions) error GetEncryptedKey(kid string) (string, error) GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) @@ -252,6 +252,9 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("GET", "/federation", h.Federation) // SSH CA r.MethodFunc("POST", "/ssh/sign", h.SSHSign) + r.MethodFunc("POST", "/ssh/renew", h.SSHRenew) + r.MethodFunc("POST", "/ssh/revoke", h.SSHRevoke) + r.MethodFunc("POST", "/ssh/rekey", h.SSHRekey) r.MethodFunc("GET", "/ssh/roots", h.SSHRoots) r.MethodFunc("GET", "/ssh/federation", h.SSHFederation) r.MethodFunc("POST", "/ssh/config", h.SSHConfig) diff --git a/api/revoke.go b/api/revoke.go index 15c42e90..aceb8305 100644 --- a/api/revoke.go +++ b/api/revoke.go @@ -1,10 +1,12 @@ package api import ( + "context" "net/http" "github.com/pkg/errors" "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" "golang.org/x/crypto/ocsp" ) @@ -63,10 +65,15 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { PassiveOnly: body.Passive, } + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) // A token indicates that we are using the api via a provisioner token, // otherwise it is assumed that the certificate is revoking itself over mTLS. if len(body.OTT) > 0 { logOtt(w, body.OTT) + if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { + WriteError(w, Unauthorized(err)) + return + } opts.OTT = body.OTT } else { // If no token is present, then the request must be made over mTLS and @@ -77,11 +84,18 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { return } opts.Crt = r.TLS.PeerCertificates[0] + if opts.Crt.SerialNumber.String() != opts.Serial { + WriteError(w, BadRequest(errors.New("revoke: serial number in mtls certificate different than body"))) + return + } + // TODO: should probably be checking if the certificate was revoked here. + // Will need to thread that request down to the authority, so will need + // to add API for that. logCertificate(w, opts.Crt) opts.MTLS = true } - if err := h.Authority.Revoke(opts); err != nil { + if err := h.Authority.Revoke(ctx, opts); err != nil { WriteError(w, Forbidden(err)) return } diff --git a/api/ssh.go b/api/ssh.go index 11d59712..b2305fc6 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -16,6 +16,8 @@ import ( // SSHAuthority is the interface implemented by a SSH CA authority. type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + RenewSSH(cert *ssh.Certificate) (*ssh.Certificate, error) + RekeySSH(cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) GetSSHRoots() (*authority.SSHKeys, error) GetSSHFederation() (*authority.SSHKeys, error) @@ -67,7 +69,8 @@ type SSHCertificate struct { *ssh.Certificate `json:"omitempty"` } -// SSHGetHostsResponse +// SSHGetHostsResponse is the response object that returns the list of valid +// hosts for SSH. type SSHGetHostsResponse struct { Hosts []string `json:"hosts"` } diff --git a/api/sshRekey.go b/api/sshRekey.go new file mode 100644 index 00000000..530b9df3 --- /dev/null +++ b/api/sshRekey.go @@ -0,0 +1,78 @@ +package api + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/provisioner" + "golang.org/x/crypto/ssh" +) + +// SSHRekeyRequest is the request body of an SSH certificate request. +type SSHRekeyRequest struct { + OTT string `json:"ott"` + PublicKey []byte `json:"publicKey"` //base64 encoded +} + +// Validate validates the SSHSignRekey. +func (s *SSHRekeyRequest) Validate() error { + switch { + case len(s.OTT) == 0: + return errors.New("missing or empty ott") + case len(s.PublicKey) == 0: + return errors.New("missing or empty public key") + default: + return nil + } +} + +// SSHRekeyResponse is the response object that returns the SSH certificate. +type SSHRekeyResponse struct { + Certificate SSHCertificate `json:"crt"` +} + +// SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token +// (ott) from the body and creates a new SSH certificate with the information in +// the request. +func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { + var body SSHRekeyRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + + logOtt(w, body.OTT) + if err := body.Validate(); err != nil { + WriteError(w, BadRequest(err)) + return + } + + publicKey, err := ssh.ParsePublicKey(body.PublicKey) + if err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error parsing publicKey"))) + return + } + + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RekeySSHMethod) + signOpts, err := h.Authority.Authorize(ctx, body.OTT) + if err != nil { + WriteError(w, Unauthorized(err)) + return + } + oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT) + if err != nil { + WriteError(w, InternalServerError(err)) + } + + newCert, err := h.Authority.RekeySSH(oldCert, publicKey, signOpts...) + if err != nil { + WriteError(w, Forbidden(err)) + return + } + + w.WriteHeader(http.StatusCreated) + JSON(w, &SSHSignResponse{ + Certificate: SSHCertificate{newCert}, + }) +} diff --git a/api/sshRenew.go b/api/sshRenew.go new file mode 100644 index 00000000..3aea01bb --- /dev/null +++ b/api/sshRenew.go @@ -0,0 +1,68 @@ +package api + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/provisioner" +) + +// SSHRenewRequest is the request body of an SSH certificate request. +type SSHRenewRequest struct { + OTT string `json:"ott"` +} + +// Validate validates the SSHSignRequest. +func (s *SSHRenewRequest) Validate() error { + switch { + case len(s.OTT) == 0: + return errors.New("missing or empty ott") + default: + return nil + } +} + +// SSHRenewResponse is the response object that returns the SSH certificate. +type SSHRenewResponse struct { + Certificate SSHCertificate `json:"crt"` +} + +// SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token +// (ott) from the body and creates a new SSH certificate with the information in +// the request. +func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { + var body SSHRenewRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + + logOtt(w, body.OTT) + if err := body.Validate(); err != nil { + WriteError(w, BadRequest(err)) + return + } + + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RenewSSHMethod) + _, err := h.Authority.Authorize(ctx, body.OTT) + if err != nil { + WriteError(w, Unauthorized(err)) + return + } + oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT) + if err != nil { + WriteError(w, InternalServerError(err)) + } + + newCert, err := h.Authority.RenewSSH(oldCert) + if err != nil { + WriteError(w, Forbidden(err)) + return + } + + w.WriteHeader(http.StatusCreated) + JSON(w, &SSHSignResponse{ + Certificate: SSHCertificate{newCert}, + }) +} diff --git a/api/sshRevoke.go b/api/sshRevoke.go new file mode 100644 index 00000000..9355e5a4 --- /dev/null +++ b/api/sshRevoke.go @@ -0,0 +1,98 @@ +package api + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/logging" + "golang.org/x/crypto/ocsp" +) + +// SSHRevokeResponse is the response object that returns the health of the server. +type SSHRevokeResponse struct { + Status string `json:"status"` +} + +// SSHRevokeRequest is the request body for a revocation request. +type SSHRevokeRequest struct { + Serial string `json:"serial"` + OTT string `json:"ott"` + ReasonCode int `json:"reasonCode"` + Reason string `json:"reason"` + Passive bool `json:"passive"` +} + +// Validate checks the fields of the RevokeRequest and returns nil if they are ok +// or an error if something is wrong. +func (r *SSHRevokeRequest) Validate() (err error) { + if r.Serial == "" { + return BadRequest(errors.New("missing serial")) + } + if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise { + return BadRequest(errors.New("reasonCode out of bounds")) + } + if !r.Passive { + return NotImplemented(errors.New("non-passive revocation not implemented")) + } + if len(r.OTT) == 0 { + return BadRequest(errors.New("missing ott")) + } + return +} + +// Revoke supports handful of different methods that revoke a Certificate. +// +// NOTE: currently only Passive revocation is supported. +func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { + var body SSHRevokeRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + + if err := body.Validate(); err != nil { + WriteError(w, err) + return + } + + opts := &authority.RevokeOptions{ + Serial: body.Serial, + Reason: body.Reason, + ReasonCode: body.ReasonCode, + PassiveOnly: body.Passive, + } + + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeSSHMethod) + // A token indicates that we are using the api via a provisioner token, + // otherwise it is assumed that the certificate is revoking itself over mTLS. + logOtt(w, body.OTT) + if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { + WriteError(w, Unauthorized(err)) + return + } + opts.OTT = body.OTT + + if err := h.Authority.Revoke(ctx, opts); err != nil { + WriteError(w, Forbidden(err)) + return + } + + logSSHRevoke(w, opts) + JSON(w, &SSHRevokeResponse{Status: "ok"}) +} + +func logSSHRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) { + if rl, ok := w.(logging.ResponseLogger); ok { + rl.WithFields(map[string]interface{}{ + "serial": ri.Serial, + "reasonCode": ri.ReasonCode, + "reason": ri.Reason, + "passiveOnly": ri.PassiveOnly, + "mTLS": ri.MTLS, + "ssh": true, + }) + } +} diff --git a/authority/authority.go b/authority/authority.go index d53d8d1b..a62e5034 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -179,8 +179,33 @@ func (a *Authority) init() error { } } + // Merge global and configuration claims + claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, globalProvisionerClaims) + if err != nil { + return err + } + // TODO: should we also be combining the ssh federated roots here? + // If we rotate ssh roots keys, sshpop provisioner will lose ability to + // validate old SSH certificates, unless they are added as federated certs. + sshKeys, err := a.GetSSHRoots() + if err != nil { + return err + } + // Initialize provisioners + config := provisioner.Config{ + Claims: claimer.Claims(), + Audiences: a.config.getAudiences(), + DB: a.db, + SSHKeys: &provisioner.SSHKeys{ + UserKeys: sshKeys.UserKeys, + HostKeys: sshKeys.HostKeys, + }, + } // Store all the provisioners for _, p := range a.config.AuthorityConfig.Provisioners { + if err := p.Init(config); err != nil { + return err + } if err := a.provisioners.Store(p); err != nil { return err } diff --git a/authority/authorize.go b/authority/authorize.go index b8f7cb6f..18eba6b9 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -80,13 +80,32 @@ func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.Si switch m := provisioner.MethodFromContext(ctx); m { case provisioner.SignMethod: return a.authorizeSign(ctx, ott) + case provisioner.RevokeMethod: + return nil, a.authorizeRevoke(ctx, ott) case provisioner.SignSSHMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} } - return a.authorizeSign(ctx, ott) - case provisioner.RevokeMethod: - return nil, &apiError{errors.New("authorize: revoke method is not supported"), http.StatusInternalServerError, errContext} + return a.authorizeSSHSign(ctx, ott) + case provisioner.RenewSSHMethod: + if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { + return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} + } + if _, err := a.authorizeSSHRenew(ctx, ott); err != nil { + return nil, err + } + return nil, nil + case provisioner.RevokeSSHMethod: + return nil, a.authorizeSSHRevoke(ctx, ott) + case provisioner.RekeySSHMethod: + if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { + return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} + } + _, opts, err := a.authorizeSSHRekey(ctx, ott) + if err != nil { + return nil, err + } + return opts, nil default: return nil, &apiError{errors.Errorf("authorize: method %d is not supported", m), http.StatusInternalServerError, errContext} } @@ -121,38 +140,25 @@ func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) // authorizeRevoke authorizes a revocation request by validating and authenticating // the RevokeOptions POSTed with the request. // Returns a tuple of the provisioner ID and error, if one occurred. -func (a *Authority) authorizeRevoke(opts *RevokeOptions) (p provisioner.Interface, err error) { - if opts.MTLS { - if opts.Crt.SerialNumber.String() != opts.Serial { - return nil, errors.New("authorizeRevoke: serial number in certificate different than body") - } - // Load the Certificate provisioner if one exists. - p, err = a.LoadProvisionerByCertificate(opts.Crt) - if err != nil { - return nil, errors.Wrap(err, "authorizeRevoke") - } - } else { - // Gets the token provisioner and validates common token fields. - p, err = a.authorizeToken(opts.OTT) - if err != nil { - return nil, errors.Wrap(err, "authorizeRevoke") - } +func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { + errContext := map[string]interface{}{"ott": token} - // Call the provisioner AuthorizeRevoke to apply provisioner specific auth claims. - err = p.AuthorizeRevoke(opts.OTT) - if err != nil { - return nil, errors.Wrap(err, "authorizeRevoke") - } + p, err := a.authorizeToken(token) + if err != nil { + return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} } - return + if err = p.AuthorizeSSHRevoke(ctx, token); err != nil { + return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} + } + return nil } -// authorizeRenewal tries to locate the step provisioner extension, and checks +// authorizeRenewl tries to locate the step provisioner extension, and checks // if for the configured provisioner, the renewal is enabled or not. If the // extra extension cannot be found, authorize the renewal by default. // // TODO(mariano): should we authorize by default? -func (a *Authority) authorizeRenewal(crt *x509.Certificate) error { +func (a *Authority) authorizeRenew(crt *x509.Certificate) error { errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()} // Check the passive revocation table. @@ -180,7 +186,7 @@ func (a *Authority) authorizeRenewal(crt *x509.Certificate) error { context: errContext, } } - if err := p.AuthorizeRenewal(crt); err != nil { + if err := p.AuthorizeRenew(context.Background(), crt); err != nil { return &apiError{ err: errors.Wrap(err, "renew"), code: http.StatusUnauthorized, diff --git a/authority/config.go b/authority/config.go index 5ec83477..e70eba48 100644 --- a/authority/config.go +++ b/authority/config.go @@ -81,23 +81,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { return errors.New("authority.provisioners cannot be empty") } - // Merge global and configuration claims - claimer, err := provisioner.NewClaimer(c.Claims, globalProvisionerClaims) - if err != nil { - return err - } - - // Initialize provisioners - config := provisioner.Config{ - Claims: claimer.Claims(), - Audiences: audiences, - } - for _, p := range c.Provisioners { - if err := p.Init(config); err != nil { - return err - } - } - if c.Template == nil { c.Template = &x509util.ASN1DN{} } @@ -194,8 +177,11 @@ func (c *Config) Validate() error { // front so we cannot rely on the port. func (c *Config) getAudiences() provisioner.Audiences { audiences := provisioner.Audiences{ - Sign: []string{legacyAuthority}, - Revoke: []string{legacyAuthority}, + Sign: []string{legacyAuthority}, + Revoke: []string{legacyAuthority}, + SSHSign: []string{}, + SSHRevoke: []string{}, + SSHRenew: []string{}, } for _, name := range c.DNSNames { @@ -203,6 +189,14 @@ func (c *Config) getAudiences() provisioner.Audiences { fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name)) audiences.Revoke = append(audiences.Revoke, fmt.Sprintf("https://%s/revoke", name), fmt.Sprintf("https://%s/1.0/revoke", name)) + audiences.SSHSign = append(audiences.SSHSign, + fmt.Sprintf("https://%s/ssh/sign", name), fmt.Sprintf("https://%s/1.0/ssh/sign", name)) + audiences.SSHRevoke = append(audiences.SSHRevoke, + fmt.Sprintf("https://%s/ssh/revoke", name), fmt.Sprintf("https://%s/1.0/ssh/revoke", name)) + audiences.SSHRenew = append(audiences.SSHRenew, + fmt.Sprintf("https://%s/ssh/renew", name), fmt.Sprintf("https://%s/1.0/ssh/renew", name)) + audiences.SSHRekey = append(audiences.SSHRekey, + fmt.Sprintf("https://%s/ssh/rekey", name), fmt.Sprintf("https://%s/1.0/ssh/rekey", name)) } return audiences diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index d1933d47..adba8fd3 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -10,6 +10,7 @@ import ( // ACME is the acme provisioner type, an entity that can authorize the ACME // provisioning flow. type ACME struct { + *base Type string `json:"type"` Name string `json:"name"` Claims *Claims `json:"claims,omitempty"` @@ -58,16 +59,10 @@ func (p *ACME) Init(config Config) (err error) { return err } -// AuthorizeRevoke is not implemented yet for the ACME provisioner. -func (p *ACME) AuthorizeRevoke(token string) error { - return nil -} - -// AuthorizeSign validates the given token. -func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error) { - if m := MethodFromContext(ctx); m != SignMethod { - return nil, errors.Errorf("unexpected method type %d in context", m) - } +// AuthorizeSign does not do any validation, because all validation is handled +// in the ACME protocol. This method returns a list of modifiers / constraints +// on the resulting certificate. +func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { return []SignOption{ // modifiers / withOptions newProvisionerExtensionOption(TypeACME, p.Name, ""), @@ -78,8 +73,11 @@ func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error }, nil } -// AuthorizeRenewal is not implemented for the ACME provisioner. -func (p *ACME) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +// NOTE: This method does not actually validate the certificate or check it's +// revocation status. Just confirms that the provisioner that created the +// certificate was configured to allow renewals. +func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index e1b2ef9d..a58ffb7e 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -123,6 +123,7 @@ type awsInstanceIdentityDocument struct { // Amazon Identity docs are available at // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html type AWS struct { + *base Type string `json:"type"` Name string `json:"name"` Accounts []string `json:"accounts"` @@ -273,14 +274,6 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er return nil, err } - // Check for the sign ssh method, default to sign X.509 - if MethodFromContext(ctx) == SignSSHMethod { - if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) - } - return p.authorizeSSHSign(payload) - } - doc := payload.document // Enforce known CN and default DNS and IP if configured. // By default we'll accept the CN and SANs in the CSR. @@ -306,20 +299,17 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er ), nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *AWS) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +// NOTE: This method does not actually validate the certificate or check it's +// revocation status. Just confirms that the provisioner that created the +// certificate was configured to allow renewals. +func (p *AWS) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } return nil } -// AuthorizeRevoke returns an error because revoke is not supported on AWS -// provisioners. -func (p *AWS) AuthorizeRevoke(token string) error { - return errors.New("revoke is not supported on a AWS provisioner") -} - // assertConfig initializes the config if it has not been initialized func (p *AWS) assertConfig() (err error) { if p.config != nil { @@ -445,8 +435,16 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { return &payload, nil } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) { +// AuthorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + claims, err := p.authorizeToken(token) + if err != nil { + return nil, err + } + doc := claims.document signOptions := []SignOption{ diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index d8252799..5e338e18 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -80,6 +80,7 @@ type azurePayload struct { // https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token // and https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service type Azure struct { + *base Type string `json:"type"` Name string `json:"name"` TenantID string `json:"tenantId"` @@ -208,15 +209,14 @@ func (p *Azure) Init(config Config) (err error) { return nil } -// AuthorizeSign validates the given token and returns the sign options that -// will be used on certificate creation. -func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { +// parseToken returuns the claims, name, group, error. +func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, "", "", errors.Wrapf(err, "error parsing token") } if len(jwt.Headers) == 0 { - return nil, errors.New("error parsing token: header is missing") + return nil, "", "", errors.New("error parsing token: header is missing") } var found bool @@ -229,7 +229,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } } if !found { - return nil, errors.New("cannot validate token") + return nil, "", "", errors.New("cannot validate token") } if err := claims.ValidateWithLeeway(jose.Expected{ @@ -237,19 +237,29 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, Issuer: p.oidcConfig.Issuer, Time: time.Now(), }, 1*time.Minute); err != nil { - return nil, errors.Wrap(err, "failed to validate payload") + return nil, "", "", errors.Wrap(err, "failed to validate payload") } // Validate TenantID if claims.TenantID != p.TenantID { - return nil, errors.New("validation failed: invalid tenant id claim (tid)") + return nil, "", "", errors.New("validation failed: invalid tenant id claim (tid)") } re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID) if len(re) != 4 { - return nil, errors.Errorf("error parsing xms_mirid claim: %s", claims.XMSMirID) + return nil, "", "", errors.Errorf("error parsing xms_mirid claim: %s", claims.XMSMirID) } group, name := re[2], re[3] + return &claims, name, group, nil +} + +// AuthorizeSign validates the given token and returns the sign options that +// will be used on certificate creation. +func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { + _, name, group, err := p.parseToken(token) + if err != nil { + return nil, err + } // Filter by resource group if len(p.ResourceGroups) > 0 { @@ -265,14 +275,6 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } } - // Check for the sign ssh method, default to sign X.509 - if MethodFromContext(ctx) == SignSSHMethod { - if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) - } - return p.authorizeSSHSign(claims, name) - } - // Enforce known common name and default DNS if configured. // By default we'll accept the CN and SANs in the CSR. // There's no way to trust them other than TOFU. @@ -293,22 +295,27 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, ), nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *Azure) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +// NOTE: This method does not actually validate the certificate or check it's +// revocation status. Just confirms that the provisioner that created the +// certificate was configured to allow renewals. +func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } return nil } -// AuthorizeRevoke returns an error because revoke is not supported on Azure -// provisioners. -func (p *Azure) AuthorizeRevoke(token string) error { - return errors.New("revoke is not supported on a Azure provisioner") -} +// AuthorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption, error) { + _, name, _, err := p.parseToken(token) + if err != nil { + return nil, err + } signOptions := []SignOption{ // set the key id to the token subject sshCertificateKeyIDModifier(name), diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index b2ec509a..30a65909 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -74,6 +74,7 @@ func newGCPConfig() *gcpConfig { // Google Identity docs are available at // https://cloud.google.com/compute/docs/instances/verifying-instance-identity type GCP struct { + *base Type string `json:"type"` Name string `json:"name"` ServiceAccounts []string `json:"serviceAccounts"` @@ -212,14 +213,6 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er return nil, err } - // Check for the sign ssh method, default to sign X.509 - if MethodFromContext(ctx) == SignSSHMethod { - if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) - } - return p.authorizeSSHSign(claims) - } - ce := claims.Google.ComputeEngine // Enforce known common name and default DNS if configured. // By default we we'll accept the CN and SANs in the CSR. @@ -247,19 +240,13 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } // AuthorizeRenewal returns an error if the renewal is disabled. -func (p *GCP) AuthorizeRenewal(cert *x509.Certificate) error { +func (p *GCP) AuthorizeRenewal(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } return nil } -// AuthorizeRevoke returns an error because revoke is not supported on GCP -// provisioners. -func (p *GCP) AuthorizeRevoke(token string) error { - return errors.New("revoke is not supported on a GCP provisioner") -} - // assertConfig initializes the config if it has not been initialized. func (p *GCP) assertConfig() { if p.config == nil { @@ -357,8 +344,16 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { return &claims, nil } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) { +// AuthorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + claims, err := p.authorizeToken(token) + if err != nil { + return nil, err + } + ce := claims.Google.ComputeEngine signOptions := []SignOption{ diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index f9178bb7..a3a7d1d9 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -24,6 +24,7 @@ type stepPayload struct { // JWK is the default provisioner, an entity that can sign tokens necessary for // signature requests. type JWK struct { + *base Type string `json:"type"` Name string `json:"name"` Key *jose.JSONWebKey `json:"key"` @@ -129,7 +130,7 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err // AuthorizeRevoke returns an error if the provisioner does not have rights to // revoke the certificate with serial number in the `sub` property. -func (p *JWK) AuthorizeRevoke(token string) error { +func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) return err } @@ -141,14 +142,6 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er return nil, err } - // Check for SSH sign-ing request. - if MethodFromContext(ctx) == SignSSHMethod { - if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) - } - return p.authorizeSSHSign(claims) - } - // NOTE: This is for backwards compatibility with older versions of cli // and certificates. Older versions added the token subject as the only SAN // in a CSR by default. @@ -171,17 +164,27 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er }, nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +// NOTE: This method does not actually validate the certificate or check it's +// revocation status. Just confirms that the provisioner that created the +// certificate was configured to allow renewals. +func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } return nil } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { - t := now() +// AuthorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + // TODO: fix audiences + claims, err := p.authorizeToken(token, p.audiences.Sign) + if err != nil { + return nil, err + } if claims.Step == nil || claims.Step.SSH == nil { return nil, errors.New("authorization token must be an SSH provisioning token") } @@ -193,6 +196,7 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { sshCertificateKeyIDModifier(claims.Subject), } + t := now() // Add modifiers from custom claims if opts.CertType != "" { signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) @@ -223,3 +227,10 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { &sshCertificateDefaultValidator{}, ), nil } + +// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise. +func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error { + // TODO fix audience. + _, err := p.authorizeToken(token, p.audiences.SSHRevoke) + return err +} diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 3e08a7e2..0abed1f3 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -40,6 +40,7 @@ type k8sSAPayload struct { // K8sSA represents a Kubernetes ServiceAccount provisioner; an // entity trusted to make signature requests. type K8sSA struct { + *base Type string `json:"type"` Name string `json:"name"` Claims *Claims `json:"claims,omitempty"` @@ -199,7 +200,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, // AuthorizeRevoke returns an error if the provisioner does not have rights to // revoke the certificate with serial number in the `sub` property. -func (p *K8sSA) AuthorizeRevoke(token string) error { +func (p *K8sSA) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) return err } diff --git a/authority/provisioner/method.go b/authority/provisioner/method.go index c8f96885..4e5f32a7 100644 --- a/authority/provisioner/method.go +++ b/authority/provisioner/method.go @@ -14,12 +14,38 @@ type methodKey struct{} const ( // SignMethod is the method used to sign X.509 certificates. SignMethod Method = iota - // SignSSHMethod is the method used to sign SSH certificate. - SignSSHMethod // RevokeMethod is the method used to revoke X.509 certificates. RevokeMethod + // SignSSHMethod is the method used to sign SSH certificates. + SignSSHMethod + // RenewSSHMethod is the method used to renew SSH certificates. + RenewSSHMethod + // RevokeSSHMethod is the method used to revoke SSH certificates. + RevokeSSHMethod + // RekeySSHMethod is the method used to rekey SSH certificates. + RekeySSHMethod ) +// String returns a string representation of the context method. +func (m Method) String() string { + switch m { + case SignMethod: + return "sign-method" + case RevokeMethod: + return "revoke-method" + case SignSSHMethod: + return "sign-ssh-method" + case RenewSSHMethod: + return "renew-ssh-method" + case RevokeSSHMethod: + return "revoke-ssh-method" + case RekeySSHMethod: + return "rekey-ssh-method" + default: + return "unknown" + } +} + // NewContextWithMethod creates a new context from ctx and attaches method to // it. func NewContextWithMethod(ctx context.Context, method Method) context.Context { diff --git a/authority/provisioner/noop.go b/authority/provisioner/noop.go index 5bdc0677..ccdeccf4 100644 --- a/authority/provisioner/noop.go +++ b/authority/provisioner/noop.go @@ -3,6 +3,8 @@ package provisioner import ( "context" "crypto/x509" + + "golang.org/x/crypto/ssh" ) // noop provisioners is a provisioner that accepts anything. @@ -35,10 +37,26 @@ func (p *noop) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e return []SignOption{}, nil } -func (p *noop) AuthorizeRenewal(cert *x509.Certificate) error { +func (p *noop) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { return nil } -func (p *noop) AuthorizeRevoke(token string) error { +func (p *noop) AuthorizeRevoke(ctx context.Context, token string) error { return nil } + +func (p *noop) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + return []SignOption{}, nil +} + +func (p *noop) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { + return nil, nil +} + +func (p *noop) AuthorizeSSHRevoke(ctx context.Context, token string) error { + return nil +} + +func (p *noop) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { + return nil, []SignOption{}, nil +} diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index b65d9b6f..90e46701 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -50,6 +50,7 @@ type openIDPayload struct { // // ClientSecret is mandatory, but it can be an empty string. type OIDC struct { + *base Type string `json:"type"` Name string `json:"name"` ClientID string `json:"clientID"` @@ -264,7 +265,7 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) { // AuthorizeRevoke returns an error if the provisioner does not have rights to // revoke the certificate with serial number in the `sub` property. // Only tokens generated by an admin have the right to revoke a certificate. -func (o *OIDC) AuthorizeRevoke(token string) error { +func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error { claims, err := o.authorizeToken(token) if err != nil { return err @@ -284,14 +285,6 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e return nil, err } - // Check for the sign ssh method, default to sign X.509 - if MethodFromContext(ctx) == SignSSHMethod { - if !o.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID()) - } - return o.authorizeSSHSign(claims) - } - so := []SignOption{ // modifiers / withOptions newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), @@ -308,16 +301,26 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e return append(so, emailOnlyIdentity(claims.Email)), nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +// NOTE: This method does not actually validate the certificate or check it's +// revocation status. Just confirms that the provisioner that created the +// certificate was configured to allow renewals. +func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if o.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", o.GetID()) } return nil } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) { +// AuthorizeSSHSign returns the list of SignOption for a SignSSH request. +func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !o.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID()) + } + claims, err := o.authorizeToken(token) + if err != nil { + return nil, err + } signOptions := []SignOption{ // set the key id to the token subject sshCertificateKeyIDModifier(claims.Email), @@ -356,6 +359,20 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) { ), nil } +// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise. +func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error { + claims, err := o.authorizeToken(token) + if err != nil { + return err + } + + // Only admins can revoke certificates. + if o.IsAdmin(claims.Email) { + return nil + } + return errors.New("cannot revoke with non-admin token") +} + func getAndDecode(uri string, v interface{}) error { resp, err := http.Get(uri) if err != nil { diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 0ed56832..4a17626c 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/pkg/errors" + "github.com/smallstep/certificates/db" + "golang.org/x/crypto/ssh" ) // Interface is the interface that all provisioner types must implement. @@ -20,27 +22,45 @@ type Interface interface { GetEncryptedKey() (kid string, key string, ok bool) Init(config Config) error AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) - AuthorizeRenewal(cert *x509.Certificate) error - AuthorizeRevoke(token string) error + AuthorizeRevoke(ctx context.Context, token string) error + AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error + AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) + AuthorizeSSHRevoke(ctx context.Context, token string) error + AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) + AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) } // Audiences stores all supported audiences by request type. type Audiences struct { - Sign []string - Revoke []string + Sign []string + Revoke []string + SSHSign []string + SSHRevoke []string + SSHRenew []string + SSHRekey []string } // All returns all supported audiences across all request types in one list. -func (a Audiences) All() []string { - return append(a.Sign, a.Revoke...) +func (a Audiences) All() (auds []string) { + auds = a.Sign + auds = append(auds, a.Revoke...) + auds = append(auds, a.SSHSign...) + auds = append(auds, a.SSHRevoke...) + auds = append(auds, a.SSHRenew...) + auds = append(auds, a.SSHRekey...) + return } // WithFragment returns a copy of audiences where the url audiences contains the // given fragment. func (a Audiences) WithFragment(fragment string) Audiences { ret := Audiences{ - Sign: make([]string, len(a.Sign)), - Revoke: make([]string, len(a.Revoke)), + Sign: make([]string, len(a.Sign)), + Revoke: make([]string, len(a.Revoke)), + SSHSign: make([]string, len(a.SSHSign)), + SSHRevoke: make([]string, len(a.SSHRevoke)), + SSHRenew: make([]string, len(a.SSHRenew)), + SSHRekey: make([]string, len(a.SSHRekey)), } for i, s := range a.Sign { if u, err := url.Parse(s); err == nil { @@ -56,6 +76,34 @@ func (a Audiences) WithFragment(fragment string) Audiences { ret.Revoke[i] = s } } + for i, s := range a.SSHSign { + if u, err := url.Parse(s); err == nil { + ret.SSHSign[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String() + } else { + ret.SSHSign[i] = s + } + } + for i, s := range a.SSHRevoke { + if u, err := url.Parse(s); err == nil { + ret.SSHRevoke[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String() + } else { + ret.SSHRevoke[i] = s + } + } + for i, s := range a.SSHRenew { + if u, err := url.Parse(s); err == nil { + ret.SSHRenew[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String() + } else { + ret.SSHRenew[i] = s + } + } + for i, s := range a.SSHRekey { + if u, err := url.Parse(s); err == nil { + ret.SSHRekey[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String() + } else { + ret.SSHRekey[i] = s + } + } return ret } @@ -92,11 +140,6 @@ const ( TypeK8sSA Type = 8 // TypeSSHPOP is used to indicate the SSHPOP provisioners. TypeSSHPOP Type = 9 - - // RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map. - RevokeAudienceKey = "revoke" - // SignAudienceKey is the key for the 'sign' audiences in the audiences map. - SignAudienceKey = "sign" ) // String returns the string representation of the type. @@ -125,6 +168,12 @@ func (t Type) String() string { } } +// SSHKeys represents the SSH User and Host public keys. +type SSHKeys struct { + UserKeys []ssh.PublicKey + HostKeys []ssh.PublicKey +} + // Config defines the default parameters used in the initialization of // provisioners. type Config struct { @@ -132,6 +181,10 @@ type Config struct { Claims Claims // Audiences are the audiences used in the default provisioner, (JWK). Audiences Audiences + // DB is the interface to the authority DB client. + DB db.AuthDB + // SSHKeys are the root SSH public keys + SSHKeys *SSHKeys } type provisioner struct { @@ -222,6 +275,50 @@ func SanitizeSSHUserPrincipal(email string) string { }, strings.ToLower(email)) } +type base struct{} + +// AuthorizeSign returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for signing x509 Certificates. +func (b *base) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { + return nil, errors.New("not implemented; provisioner does not implement AuthorizeSign") +} + +// AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for revoking x509 Certificates. +func (b *base) AuthorizeRevoke(ctx context.Context, token string) error { + return errors.New("not implemented; provisioner does not implement AuthorizeRevoke") +} + +// AuthorizeRenew returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for renewing x509 Certificates. +func (b *base) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { + return errors.New("not implemented; provisioner does not implement AuthorizeRenew") +} + +// AuthorizeSSHSign returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for signing SSH Certificates. +func (b *base) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHSign") +} + +// AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for revoking SSH Certificates. +func (b *base) AuthorizeSSHRevoke(ctx context.Context, token string) error { + return errors.New("not implemented; provisioner does not implement AuthorizeSSHRevoke") +} + +// AuthorizeSSHRenew returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for renewing SSH Certificates. +func (b *base) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { + return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRenew") +} + +// AuthorizeSSHRekey returns an unimplmented error. Provisioners should overwrite +// this method if they will support authorizing tokens for renewing SSH Certificates. +func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { + return nil, nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRekey") +} + // MockProvisioner for testing type MockProvisioner struct { Mret1, Mret2, Mret3 interface{} diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index a49d32e8..e0c4a2f7 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -2,18 +2,14 @@ package provisioner import ( "context" - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" "encoding/base64" - "encoding/pem" + "fmt" + "strconv" "time" "github.com/pkg/errors" - "github.com/smallstep/cli/crypto/pemutil" - "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/certificates/db" "github.com/smallstep/cli/jose" - "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ssh" ) @@ -28,13 +24,14 @@ type sshPOPPayload struct { // SSHPOP is the default provisioner, an entity that can sign tokens necessary for // signature requests. type SSHPOP struct { + *base Type string `json:"type"` Name string `json:"name"` - PubKeys []byte `json:"pubKeys"` Claims *Claims `json:"claims,omitempty"` + db db.AuthDB claimer *Claimer audiences Audiences - sshPubKeys []ssh.PublicKey + sshPubKeys *SSHKeys } // GetID returns the provisioner unique identifier. The name and credential id @@ -83,38 +80,8 @@ func (p *SSHPOP) Init(config Config) error { return errors.New("provisioner type cannot be empty") case p.Name == "": return errors.New("provisioner name cannot be empty") - case len(p.PubKeys) == 0: - return errors.New("provisioner root(s) cannot be empty") - } - - var ( - block *pem.Block - rest = p.PubKeys - ) - for rest != nil { - block, rest = pem.Decode(rest) - if block == nil { - break - } - key, err := pemutil.ParseKey(pem.EncodeToMemory(block)) - if err != nil { - return errors.Wrapf(err, "error parsing public key in provisioner %s", p.GetID()) - } - switch q := key.(type) { - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: - sshKey, err := ssh.NewPublicKey(key) - if err != nil { - return errors.Wrap(err, "error converting pub key to SSH pub key") - } - p.sshPubKeys = append(p.sshPubKeys, sshKey) - default: - return errors.Errorf("Unexpected public key type %T in provisioner %s", q, p.GetID()) - } - } - - // Verify that at least one root was found. - if len(p.sshPubKeys) == 0 { - return errors.Errorf("no root public keys found in pub keys attribute for provisioner %s", p.GetName()) + case config.SSHKeys == nil: + return errors.New("provisioner public SSH validation keys cannot be empty") } // Update claims with global ones @@ -124,6 +91,8 @@ func (p *SSHPOP) Init(config Config) error { } p.audiences = config.Audiences.WithFragment(p.GetID()) + p.db = config.DB + p.sshPubKeys = config.SSHKeys return nil } @@ -131,50 +100,63 @@ func (p *SSHPOP) Init(config Config) error { // claims for case specific downstream parsing. // e.g. a Sign request will auth/validate different fields than a Revoke request. func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) { - jwt, err := jose.ParseSigned(token) + sshCert, err := ExtractSSHPOPCert(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errors.Wrap(err, "authorizeToken ssh-pop") } - encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"] - if !ok { - return nil, errors.New("token missing sshpop header") - } - encodedSSHCertStr, ok := encodedSSHCert.(string) - if !ok { - return nil, errors.New("error unexpected type for sshpop header") + // Check for revocation. + if isRevoked, err := p.db.IsSSHRevoked(strconv.FormatUint(sshCert.Serial, 10)); err != nil { + return nil, errors.Wrap(err, "authorizeToken ssh-pop") + } else if isRevoked { + return nil, errors.New("authorizeToken ssh-pop: ssh certificate has been revoked") } - sshCertBytes, err := base64.RawURLEncoding.DecodeString(encodedSSHCertStr) + + jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrap(err, "error decoding sshpop header") + return nil, errors.Wrapf(err, "error parsing token") } - sshPub, err := ssh.ParsePublicKey(sshCertBytes) - if err != nil { - return nil, errors.Wrap(err, "error parsing ssh public key") + // Check validity period of the certificate. + n := time.Now() + if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) { + return nil, errors.New("sshpop certificate validAfter is in the future") } - sshCert, ok := sshPub.(*ssh.Certificate) + if sshCert.ValidBefore != 0 && time.Unix(int64(sshCert.ValidBefore), 0).Before(n) { + return nil, errors.New("sshpop certificate validBefore is in the past") + } + sshCryptoPubKey, ok := sshCert.Key.(ssh.CryptoPublicKey) if !ok { - return nil, errors.New("error converting ssh public key to ssh certificate") + return nil, errors.New("ssh public key could not be cast to ssh CryptoPublicKey") } + pubKey := sshCryptoPubKey.CryptoPublicKey() - data := bytesForSigning(sshCert) - var found bool - for _, k := range p.sshPubKeys { + var ( + found bool + data = bytesForSigning(sshCert) + keys []ssh.PublicKey + ) + if sshCert.CertType == ssh.UserCert { + keys = p.sshPubKeys.UserKeys + } else { + keys = p.sshPubKeys.HostKeys + } + for _, k := range keys { if err = (&ssh.Certificate{Key: k}).Verify(data, sshCert.Signature); err == nil { found = true + break } } if !found { return nil, errors.New("error: provisioner could could not verify the sshpop header certificate") } - // Using the leaf certificates key to validate the claims accomplishes two + // Using the ssh certificates key to validate the claims accomplishes two // things: // 1. Asserts that the private key used to sign the token corresponds // to the public certificate in the `sshpop` header of the token. // 2. Asserts that the claims are valid - have not been tampered with. var claims sshPOPPayload - if err = jwt.Claims(sshCert.Key, &claims); err != nil { + if err = jwt.Claims(pubKey, &claims); err != nil { return nil, errors.Wrap(err, "error parsing claims") } @@ -189,6 +171,8 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { + fmt.Printf("claims.Audience = %+v\n", claims.Audience) + fmt.Printf("audiences = %+v\n", audiences) return nil, errors.New("invalid token: invalid audience claim (aud)") } @@ -200,102 +184,77 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa return &claims, nil } -// AuthorizeRevoke returns an error if the provisioner does not have rights to -// revoke the certificate with serial number in the `sub` property. -func (p *SSHPOP) AuthorizeRevoke(token string) error { - _, err := p.authorizeToken(token, p.audiences.Revoke) +// AuthorizeSSHRevoke validates the authorization token and extracts/validates +// the SSH certificate from the ssh-pop header. +func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error { + claims, err := p.authorizeToken(token, p.audiences.SSHRevoke) + if err != nil { + return err + } + if claims.Subject != strconv.FormatUint(claims.sshCert.Serial, 10) { + return errors.New("token subject must be equivalent to certificate serial number") + } return err } -// AuthorizeSign validates the given token. -func (p *SSHPOP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - claims, err := p.authorizeToken(token, p.audiences.Sign) +// AuthorizeSSHRenew validates the authorization token and extracts/validates +// the SSH certificate from the ssh-pop header. +func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { + claims, err := p.authorizeToken(token, p.audiences.SSHRenew) if err != nil { return nil, err } + return claims.sshCert, nil - // Check for SSH sign-ing request. - if MethodFromContext(ctx) == SignSSHMethod { - if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) - } - return p.authorizeSSHSign(claims) - } +} - // NOTE: This is for backwards compatibility with older versions of cli - // and certificates. Older versions added the token subject as the only SAN - // in a CSR by default. - if len(claims.SANs) == 0 { - claims.SANs = []string{claims.Subject} +// AuthorizeSSHRekey validates the authorization token and extracts/validates +// the SSH certificate from the ssh-pop header. +func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { + claims, err := p.authorizeToken(token, p.audiences.SSHRekey) + if err != nil { + return nil, nil, err } - - dnsNames, ips, emails := x509util.SplitSANs(claims.SANs) - - return []SignOption{ - // modifiers / withOptions - newProvisionerExtensionOption(TypeSSHPOP, p.Name, ""), - profileLimitDuration{p.claimer.DefaultTLSCertDuration(), time.Unix(int64(claims.sshCert.ValidBefore), 0)}, - // validators - commonNameValidator(claims.Subject), - defaultPublicKeyValidator{}, - dnsNamesValidator(dnsNames), - emailAddressesValidator(emails), - ipAddressesValidator(ips), - newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + return claims.sshCert, []SignOption{ + // Validate public key + &sshDefaultPublicKeyValidator{}, + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require and validate all the default fields in the SSH certificate. + &sshCertificateDefaultValidator{}, }, nil -} -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *SSHPOP) AuthorizeRenewal(cert *x509.Certificate) error { - if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) - } - return nil } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (p *SSHPOP) authorizeSSHSign(claims *sshPOPPayload) ([]SignOption, error) { - if claims.Step == nil || claims.Step.SSH == nil { - return nil, errors.New("authorization token must be an SSH provisioning token") - } - opts := claims.Step.SSH - signOptions := []SignOption{ - // validates user's SSHOptions with the ones in the token - sshCertificateOptionsValidator(*opts), - // set the key id to the token subject - sshCertificateKeyIDModifier(claims.Subject), +// ExtractSSHPOPCert parses a JWT and extracts and loads the SSH Certificate +// in the sshpop header. If the header is missing, an error is returned. +func ExtractSSHPOPCert(token string) (*ssh.Certificate, error) { + jwt, err := jose.ParseSigned(token) + if err != nil { + return nil, errors.Wrapf(err, "error parsing token") } - // Add modifiers from custom claims - if opts.CertType != "" { - signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) + encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"] + if !ok { + return nil, errors.New("token missing sshpop header") } - if len(opts.Principals) > 0 { - signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals)) + encodedSSHCertStr, ok := encodedSSHCert.(string) + if !ok { + return nil, errors.New("error unexpected type for sshpop header") } - t := now() - if !opts.ValidAfter.IsZero() { - signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) + sshCertBytes, err := base64.StdEncoding.DecodeString(encodedSSHCertStr) + if err != nil { + return nil, errors.Wrap(err, "error decoding sshpop header") } - if !opts.ValidBefore.IsZero() { - signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) + sshPub, err := ssh.ParsePublicKey(sshCertBytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing ssh public key") } - - // Default to a user certificate with no principals if not set - signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) - - return append(signOptions, - // Set the default extensions. - &sshDefaultExtensionModifier{}, - // Checks the validity bounds, and set the validity if has not been set. - sshLimitValidityModifier(p.claimer, time.Unix(int64(claims.sshCert.ValidBefore), 0)), - // Validate public key. - &sshDefaultPublicKeyValidator{}, - // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, - // Require all the fields in the SSH certificate - &sshCertificateDefaultValidator{}, - ), nil + sshCert, ok := sshPub.(*ssh.Certificate) + if !ok { + return nil, errors.New("error converting ssh public key to ssh certificate") + } + return sshCert, nil } func bytesForSigning(cert *ssh.Certificate) []byte { diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 55725982..84236b2c 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -22,6 +22,7 @@ type x5cPayload struct { // X5C is the default provisioner, an entity that can sign tokens necessary for // signature requests. type X5C struct { + *base Type string `json:"type"` Name string `json:"name"` Roots []byte `json:"roots"` @@ -170,7 +171,7 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err // AuthorizeRevoke returns an error if the provisioner does not have rights to // revoke the certificate with serial number in the `sub` property. -func (p *X5C) AuthorizeRevoke(token string) error { +func (p *X5C) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) return err } @@ -213,8 +214,8 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er }, nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *X5C) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } diff --git a/authority/ssh.go b/authority/ssh.go index 74833256..9181b7bc 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -1,10 +1,12 @@ package authority import ( + "context" "crypto/rand" "encoding/binary" "net/http" "strings" + "time" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" @@ -155,6 +157,22 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template return output, nil } +// authorizeSSHSign loads the provisioner from the token, checks that it has not +// been used again and calls the provisioner AuthorizeSSHSign method. Returns a +// list of methods to apply to the signing flow. +func (a *Authority) authorizeSSHSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) { + var errContext = apiCtx{"ott": ott} + p, err := a.authorizeToken(ott) + if err != nil { + return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext} + } + opts, err := p.AuthorizeSSHSign(ctx, ott) + if err != nil { + return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext} + } + return opts, nil +} + // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var mods []provisioner.SSHCertificateModifier @@ -274,6 +292,263 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign return cert, nil } +// authorizeSSHRenew authorizes an SSH certificate renewal request, by +// validating the contents of an SSHPOP token. +func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { + errContext := map[string]interface{}{"ott": token} + + p, err := a.authorizeToken(token) + if err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "authorizeSSHRenew"), + code: http.StatusUnauthorized, + context: errContext, + } + } + cert, err := p.AuthorizeSSHRenew(ctx, token) + if err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "authorizeSSHRenew"), + code: http.StatusUnauthorized, + context: errContext, + } + } + return cert, nil +} + +// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template. +func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) { + nonce, err := randutil.ASCII(32) + if err != nil { + return nil, &apiError{err: err, code: http.StatusInternalServerError} + } + + var serial uint64 + if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "renewSSH: error reading random number"), + code: http.StatusInternalServerError, + } + } + + if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { + return nil, errors.New("rewnewSSh: cannot renew certificate without validity period") + } + dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second + va := time.Now() + vb := va.Add(dur) + + // Build base certificate with the key and some random values + cert := &ssh.Certificate{ + Nonce: []byte(nonce), + Key: oldCert.Key, + Serial: serial, + CertType: oldCert.CertType, + KeyId: oldCert.KeyId, + ValidPrincipals: oldCert.ValidPrincipals, + Permissions: oldCert.Permissions, + ValidAfter: uint64(va.Unix()), + ValidBefore: uint64(vb.Unix()), + } + + // Get signer from authority keys + var signer ssh.Signer + switch cert.CertType { + case ssh.UserCert: + if a.sshCAUserCertSignKey == nil { + return nil, &apiError{ + err: errors.New("renewSSH: user certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + } + signer = a.sshCAUserCertSignKey + case ssh.HostCert: + if a.sshCAHostCertSignKey == nil { + return nil, &apiError{ + err: errors.New("renewSSH: host certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + } + signer = a.sshCAHostCertSignKey + default: + return nil, &apiError{ + err: errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType), + code: http.StatusInternalServerError, + } + } + cert.SignatureKey = signer.PublicKey() + + // Get bytes for signing trailing the signature length. + data := cert.Marshal() + data = data[:len(data)-4] + + // Sign the certificate + sig, err := signer.Sign(rand.Reader, data) + if err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "renewSSH: error signing certificate"), + code: http.StatusInternalServerError, + } + } + cert.Signature = sig + + if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + return nil, &apiError{ + err: errors.Wrap(err, "renewSSH: error storing certificate in db"), + code: http.StatusInternalServerError, + } + } + + return cert, nil +} + +// authorizeSSHRekey authorizes an SSH certificate rekey request, by +// validating the contents of an SSHPOP token. +func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) { + errContext := map[string]interface{}{"ott": token} + + p, err := a.authorizeToken(token) + if err != nil { + return nil, nil, &apiError{ + err: errors.Wrap(err, "authorizeSSHRenew"), + code: http.StatusUnauthorized, + context: errContext, + } + } + cert, opts, err := p.AuthorizeSSHRekey(ctx, token) + if err != nil { + return nil, nil, &apiError{ + err: errors.Wrap(err, "authorizeSSHRekey"), + code: http.StatusUnauthorized, + context: errContext, + } + } + return cert, opts, nil +} + +// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template. +func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + var validators []provisioner.SSHCertificateValidator + + for _, op := range signOpts { + switch o := op.(type) { + // validate the ssh.Certificate + case provisioner.SSHCertificateValidator: + validators = append(validators, o) + default: + return nil, &apiError{ + err: errors.Errorf("rekeySSH: invalid extra option type %T", o), + code: http.StatusInternalServerError, + } + } + } + + nonce, err := randutil.ASCII(32) + if err != nil { + return nil, &apiError{err: err, code: http.StatusInternalServerError} + } + + var serial uint64 + if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "rekeySSH: error reading random number"), + code: http.StatusInternalServerError, + } + } + + if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { + return nil, errors.New("rekeySSh: cannot rekey certificate without validity period") + } + dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second + va := time.Now() + vb := va.Add(dur) + + // Build base certificate with the key and some random values + cert := &ssh.Certificate{ + Nonce: []byte(nonce), + Key: pub, + Serial: serial, + CertType: oldCert.CertType, + KeyId: oldCert.KeyId, + ValidPrincipals: oldCert.ValidPrincipals, + Permissions: oldCert.Permissions, + ValidAfter: uint64(va.Unix()), + ValidBefore: uint64(vb.Unix()), + } + + // Get signer from authority keys + var signer ssh.Signer + switch cert.CertType { + case ssh.UserCert: + if a.sshCAUserCertSignKey == nil { + return nil, &apiError{ + err: errors.New("rekeySSH: user certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + } + signer = a.sshCAUserCertSignKey + case ssh.HostCert: + if a.sshCAHostCertSignKey == nil { + return nil, &apiError{ + err: errors.New("rekeySSH: host certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + } + signer = a.sshCAHostCertSignKey + default: + return nil, &apiError{ + err: errors.Errorf("rekeySSH: unexpected ssh certificate type: %d", cert.CertType), + code: http.StatusInternalServerError, + } + } + cert.SignatureKey = signer.PublicKey() + + // Get bytes for signing trailing the signature length. + data := cert.Marshal() + data = data[:len(data)-4] + + // Sign the certificate + sig, err := signer.Sign(rand.Reader, data) + if err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "rekeySSH: error signing certificate"), + code: http.StatusInternalServerError, + } + } + cert.Signature = sig + + // User provisioners validators + for _, v := range validators { + if err := v.Valid(cert); err != nil { + return nil, &apiError{err: err, code: http.StatusForbidden} + } + } + + if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + return nil, &apiError{ + err: errors.Wrap(err, "rekeySSH: error storing certificate in db"), + code: http.StatusInternalServerError, + } + } + + return cert, nil +} + +// authorizeSSHRevoke authorizes an SSH certificate revoke request, by +// validating the contents of an SSHPOP token. +func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error { + errContext := map[string]interface{}{"ott": token} + + p, err := a.authorizeToken(token) + if err != nil { + return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext} + } + if err = p.AuthorizeSSHRevoke(ctx, token); err != nil { + return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext} + } + return nil +} + // SignSSHAddUser signs a certificate that provisions a new user in a server. func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) { if a.sshCAUserCertSignKey == nil { diff --git a/authority/tls.go b/authority/tls.go index eb20639e..e7c8eb3d 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -1,6 +1,7 @@ package authority import ( + "context" "crypto/tls" "crypto/x509" "encoding/asn1" @@ -16,6 +17,7 @@ import ( "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/cli/jose" ) // GetTLSOptions returns the tls options configured. @@ -127,7 +129,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti // with a validity window that begins 'now'. func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { // Check step provisioner extensions - if err := a.authorizeRenewal(oldCert); err != nil { + if err := a.authorizeRenew(oldCert); err != nil { return nil, err } @@ -147,15 +149,15 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error ExtKeyUsage: oldCert.ExtKeyUsage, UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage, BasicConstraintsValid: oldCert.BasicConstraintsValid, - IsCA: oldCert.IsCA, - MaxPathLen: oldCert.MaxPathLen, - MaxPathLenZero: oldCert.MaxPathLenZero, - OCSPServer: oldCert.OCSPServer, - IssuingCertificateURL: oldCert.IssuingCertificateURL, - DNSNames: oldCert.DNSNames, - EmailAddresses: oldCert.EmailAddresses, - IPAddresses: oldCert.IPAddresses, - URIs: oldCert.URIs, + IsCA: oldCert.IsCA, + MaxPathLen: oldCert.MaxPathLen, + MaxPathLenZero: oldCert.MaxPathLenZero, + OCSPServer: oldCert.OCSPServer, + IssuingCertificateURL: oldCert.IssuingCertificateURL, + DNSNames: oldCert.DNSNames, + EmailAddresses: oldCert.EmailAddresses, + IPAddresses: oldCert.IPAddresses, + URIs: oldCert.URIs, PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical, PermittedDNSDomains: oldCert.PermittedDNSDomains, ExcludedDNSDomains: oldCert.ExcludedDNSDomains, @@ -220,13 +222,14 @@ type RevokeOptions struct { // being renewed. // // TODO: Add OCSP and CRL support. -func (a *Authority) Revoke(opts *RevokeOptions) error { +func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error { errContext := apiCtx{ "serialNumber": opts.Serial, "reasonCode": opts.ReasonCode, "reason": opts.Reason, "passiveOnly": opts.PassiveOnly, "mTLS": opts.MTLS, + "context": string(provisioner.MethodFromContext(ctx)), } if opts.MTLS { errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw) @@ -242,26 +245,57 @@ func (a *Authority) Revoke(opts *RevokeOptions) error { RevokedAt: time.Now().UTC(), } - // Authorize mTLS or token request and get back a provisioner interface. - p, err := a.authorizeRevoke(opts) - if err != nil { - return &apiError{errors.Wrap(err, "revoke"), - http.StatusUnauthorized, errContext} - } - + var ( + p provisioner.Interface + err error + ) // If not mTLS then get the TokenID of the token. if !opts.MTLS { + // Validate payload + token, err := jose.ParseSigned(opts.OTT) + if err != nil { + return &apiError{errors.Wrapf(err, "revoke: error parsing token"), + http.StatusUnauthorized, errContext} + } + + // Get claims w/out verification. We should have already verified this token + // earlier with a call to authorizeSSHRevoke. + var claims Claims + if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { + return &apiError{errors.Wrap(err, "revoke"), http.StatusUnauthorized, errContext} + } + + // This method will also validate the audiences for JWK provisioners. + var ok bool + p, ok = a.provisioners.LoadByToken(token, &claims.Claims) + if !ok { + return &apiError{ + errors.Errorf("revoke: provisioner not found"), + http.StatusInternalServerError, errContext} + } rci.TokenID, err = p.GetTokenID(opts.OTT) if err != nil { return &apiError{errors.Wrap(err, "revoke: could not get ID for token"), http.StatusInternalServerError, errContext} } errContext["tokenID"] = rci.TokenID + } else { + // Load the Certificate provisioner if one exists. + p, err = a.LoadProvisionerByCertificate(opts.Crt) + if err != nil { + return &apiError{ + errors.Wrap(err, "revoke: unable to load certificate provisioner"), + http.StatusUnauthorized, errContext} + } } rci.ProvisionerID = p.GetID() errContext["provisionerID"] = rci.ProvisionerID - err = a.db.Revoke(rci) + if provisioner.MethodFromContext(ctx) == provisioner.RevokeSSHMethod { + err = a.db.RevokeSSH(rci) + } else { // default to revoke x509 + err = a.db.Revoke(rci) + } switch err { case nil: return nil diff --git a/ca/client.go b/ca/client.go index 35e07758..68c6be22 100644 --- a/ca/client.go +++ b/ca/client.go @@ -528,6 +528,72 @@ func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) return &sign, nil } +// SSHRenew performs the POST /ssh/renew request to the CA and returns the +// api.SSHRenewResponse struct. +func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/renew"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var renew api.SSHRenewResponse + if err := readJSON(resp.Body, &renew); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &renew, nil +} + +// SSHRekey performs the POST /ssh/rekey request to the CA and returns the +// api.SSHRekeyResponse struct. +func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/rekey"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var rekey api.SSHRekeyResponse + if err := readJSON(resp.Body, &rekey); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &rekey, nil +} + +// SSHRevoke performs the POST /ssh/revoke request to the CA and returns the +// api.SSHRevokeResponse struct. +func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/revoke"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var revoke api.SSHRevokeResponse + if err := readJSON(resp.Body, &revoke); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &revoke, nil +} + // SSHRoots performs the GET /ssh/roots request to the CA and returns the // api.SSHRootsResponse struct. func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) { diff --git a/db/db.go b/db/db.go index 5195e1e3..7535185b 100644 --- a/db/db.go +++ b/db/db.go @@ -16,6 +16,7 @@ import ( var ( certsTable = []byte("x509_certs") revokedCertsTable = []byte("revoked_x509_certs") + revokedSSHCertsTable = []byte("revoked_ssh_certs") usedOTTTable = []byte("used_ott") sshCertsTable = []byte("ssh_certs") sshHostsTable = []byte("ssh_hosts") @@ -38,7 +39,9 @@ type Config struct { // AuthDB is an interface over an Authority DB client that implements a nosql.DB interface. type AuthDB interface { IsRevoked(sn string) (bool, error) + IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error + RevokeSSH(rci *RevokedCertificateInfo) error StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) IsSSHHost(name string) (bool, error) @@ -68,6 +71,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, + revokedSSHCertsTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -114,6 +118,29 @@ func (db *DB) IsRevoked(sn string) (bool, error) { return true, nil } +// IsSSHRevoked returns whether or not a certificate with the given identifier +// has been revoked. +// In the case of an X509 Certificate the `id` should be the Serial Number of +// the Certificate. +func (db *DB) IsSSHRevoked(sn string) (bool, error) { + // If the DB is nil then act as pass through. + if db == nil { + return false, nil + } + + // If the error is `Not Found` then the certificate has not been revoked. + // Any other error should be propagated to the caller. + if _, err := db.Get(revokedSSHCertsTable, []byte(sn)); err != nil { + if nosql.IsErrNotFound(err) { + return false, nil + } + return false, errors.Wrap(err, "error checking revocation bucket") + } + + // This certificate has been revoked. + return true, nil +} + // Revoke adds a certificate to the revocation table. func (db *DB) Revoke(rci *RevokedCertificateInfo) error { rcib, err := json.Marshal(rci) @@ -132,6 +159,24 @@ func (db *DB) Revoke(rci *RevokedCertificateInfo) error { } } +// RevokeSSH adds a SSH certificate to the revocation table. +func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error { + rcib, err := json.Marshal(rci) + if err != nil { + return errors.Wrap(err, "error marshaling revoked certificate info") + } + + _, swapped, err := db.CmpAndSwap(revokedSSHCertsTable, []byte(rci.Serial), nil, rcib) + switch { + case err != nil: + return errors.Wrap(err, "error AuthDB CmpAndSwap") + case !swapped: + return ErrAlreadyExists + default: + return nil + } +} + // StoreCertificate stores a certificate PEM. func (db *DB) StoreCertificate(crt *x509.Certificate) error { if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil { diff --git a/db/simple.go b/db/simple.go index b0733d8d..05626497 100644 --- a/db/simple.go +++ b/db/simple.go @@ -31,11 +31,21 @@ func (s *SimpleDB) IsRevoked(sn string) (bool, error) { return false, nil } +// IsSSHRevoked noop +func (s *SimpleDB) IsSSHRevoked(sn string) (bool, error) { + return false, nil +} + // Revoke returns a "NotImplemented" error. func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error { return ErrNotImplemented } +// RevokeSSH returns a "NotImplemented" error. +func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error { + return ErrNotImplemented +} + // StoreCertificate returns a "NotImplemented" error. func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error { return ErrNotImplemented From 499d698c75d165101f13e6bc108fb8e4d3d30644 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 6 Nov 2019 13:33:23 -0800 Subject: [PATCH 037/163] Fix api tests. --- api/api_test.go | 93 ++++++++++++++++++++++++++++++++++++++-------- api/revoke_test.go | 21 +++++++++-- api/ssh_test.go | 2 +- 3 files changed, 95 insertions(+), 21 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 6232dde5..1938e300 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -417,17 +417,22 @@ func TestSignRequest_Validate(t *testing.T) { } type mockProvisioner struct { - ret1, ret2, ret3 interface{} - err error - getID func() string - getTokenID func(string) (string, error) - getName func() string - getType func() provisioner.Type - getEncryptedKey func() (string, string, bool) - init func(provisioner.Config) error - authorizeRevoke func(ott string) error - authorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) - authorizeRenewal func(*x509.Certificate) error + ret1, ret2, ret3 interface{} + err error + getID func() string + getTokenID func(string) (string, error) + getName func() string + getType func() provisioner.Type + getEncryptedKey func() (string, string, bool) + init func(provisioner.Config) error + authorizeRenew func(ctx context.Context, cert *x509.Certificate) error + authorizeRevoke func(ctx context.Context, token string) error + authorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) + authorizeRenewal func(*x509.Certificate) error + authorizeSSHSign func(ctx context.Context, token string) ([]provisioner.SignOption, error) + authorizeSSHRevoke func(ctx context.Context, token string) error + authorizeSSHRenew func(ctx context.Context, token string) (*ssh.Certificate, error) + authorizeSSHRekey func(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) } func (m *mockProvisioner) GetID() string { @@ -475,9 +480,16 @@ func (m *mockProvisioner) Init(c provisioner.Config) error { return m.err } -func (m *mockProvisioner) AuthorizeRevoke(ott string) error { +func (m *mockProvisioner) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { + if m.authorizeRenew != nil { + return m.authorizeRenew(ctx, cert) + } + return m.err +} + +func (m *mockProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { if m.authorizeRevoke != nil { - return m.authorizeRevoke(ott) + return m.authorizeRevoke(ctx, token) } return m.err } @@ -496,6 +508,31 @@ func (m *mockProvisioner) AuthorizeRenewal(c *x509.Certificate) error { return m.err } +func (m *mockProvisioner) AuthorizeSSHSign(ctx context.Context, token string) ([]provisioner.SignOption, error) { + if m.authorizeSSHSign != nil { + return m.authorizeSSHSign(ctx, token) + } + return m.ret1.([]provisioner.SignOption), m.err +} +func (m *mockProvisioner) AuthorizeSSHRevoke(ctx context.Context, token string) error { + if m.authorizeSSHRevoke != nil { + return m.authorizeSSHRevoke(ctx, token) + } + return m.err +} +func (m *mockProvisioner) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { + if m.authorizeSSHRenew != nil { + return m.authorizeSSHRenew(ctx, token) + } + return m.ret1.(*ssh.Certificate), m.err +} +func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) { + if m.authorizeSSHRekey != nil { + return m.authorizeSSHRekey(ctx, token) + } + return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err +} + type mockAuthority struct { ret1, ret2 interface{} err error @@ -509,10 +546,13 @@ type mockAuthority struct { loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) loadProvisionerByID func(provID string) (provisioner.Interface, error) getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) - revoke func(*authority.RevokeOptions) error + revoke func(context.Context, *authority.RevokeOptions) error getEncryptedKey func(kid string) (string, error) getRoots func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error) + renewSSH func(cert *ssh.Certificate) (*ssh.Certificate, error) + rekeySSH func(cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + getSSHHosts func() ([]string, error) getSSHRoots func() (*authority.SSHKeys, error) getSSHFederation func() (*authority.SSHKeys, error) getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) @@ -594,9 +634,9 @@ func (m *mockAuthority) LoadProvisionerByID(provID string) (provisioner.Interfac return m.ret1.(provisioner.Interface), m.err } -func (m *mockAuthority) Revoke(opts *authority.RevokeOptions) error { +func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { if m.revoke != nil { - return m.revoke(opts) + return m.revoke(ctx, opts) } return m.err } @@ -622,6 +662,27 @@ func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { return m.ret1.([]*x509.Certificate), m.err } +func (m *mockAuthority) RenewSSH(cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.renewSSH != nil { + return m.renewSSH(cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) RekeySSH(cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.rekeySSH != nil { + return m.rekeySSH(cert, key, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) GetSSHHosts() ([]string, error) { + if m.getSSHHosts != nil { + return m.getSSHHosts() + } + return m.ret1.([]string), m.err +} + func (m *mockAuthority) GetSSHRoots() (*authority.SSHKeys, error) { if m.getSSHRoots != nil { return m.getSSHRoots() diff --git a/api/revoke_test.go b/api/revoke_test.go index 477d90e8..9aa37d1a 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "context" "crypto/tls" "crypto/x509" "encoding/json" @@ -105,7 +106,10 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusOK, auth: &mockAuthority{ - revoke: func(opts *authority.RevokeOptions) error { + authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + return nil, nil + }, + revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { assert.True(t, opts.PassiveOnly) assert.False(t, opts.MTLS) assert.Equals(t, opts.Serial, "sn") @@ -146,7 +150,10 @@ func Test_caHandler_Revoke(t *testing.T) { statusCode: http.StatusOK, tls: cs, auth: &mockAuthority{ - revoke: func(ri *authority.RevokeOptions) error { + authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + return nil, nil + }, + revoke: func(ctx context.Context, ri *authority.RevokeOptions) error { assert.True(t, ri.PassiveOnly) assert.True(t, ri.MTLS) assert.Equals(t, ri.Serial, "1404354960355712309") @@ -178,7 +185,10 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusInternalServerError, auth: &mockAuthority{ - revoke: func(opts *authority.RevokeOptions) error { + authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + return nil, nil + }, + revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { return InternalServerError(errors.New("force")) }, }, @@ -197,7 +207,10 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusForbidden, auth: &mockAuthority{ - revoke: func(opts *authority.RevokeOptions) error { + authorizeSign: func(ott string) ([]provisioner.SignOption, error) { + return nil, nil + }, + revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { return errors.New("force") }, }, diff --git a/api/ssh_test.go b/api/ssh_test.go index 075428c0..e4e2fd9b 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -432,7 +432,7 @@ func Test_caHandler_SSHFederation(t *testing.T) { func Test_caHandler_SSHConfig(t *testing.T) { userOutput := []templates.Output{ - {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("UserKnownHostsFile /home/user/.step/config/ssh/known_hosts")}, + {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("UserKnownHostsFile /home/user/.step/ssh/known_hosts")}, {Name: "known_host.tpl", Type: templates.File, Comment: "#", Path: "ssh/known_host", Content: []byte("@cert-authority * ecdsa-sha2-nistp256 AAAA...=")}, } hostOutput := []templates.Output{ From 946094d2b79565cf23568f74831ec57f0d5c9d47 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 6 Nov 2019 15:53:16 -0800 Subject: [PATCH 038/163] Add multiuse capability to k8ssa provisioners --- api/ssh.go | 2 ++ authority/provisioner/jwk.go | 8 ++--- authority/provisioner/k8sSA.go | 36 ++++++++++++++++++----- authority/provisioner/oidc.go | 2 +- authority/provisioner/sign_ssh_options.go | 14 +++++++++ authority/provisioner/x5c.go | 25 ++++++++-------- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index b2305fc6..11fd3a89 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -35,6 +35,7 @@ type SSHSignRequest struct { ValidAfter TimeDuration `json:"validAfter,omitempty"` ValidBefore TimeDuration `json:"validBefore,omitempty"` AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` + KeyID string `json:"keyID"` } // Validate validates the SSHSignRequest. @@ -239,6 +240,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { opts := provisioner.SSHOptions{ CertType: body.CertType, + KeyID: body.KeyID, Principals: body.Principals, ValidBefore: body.ValidBefore, ValidAfter: body.ValidAfter, diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index a3a7d1d9..52f83846 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -180,8 +180,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } - // TODO: fix audiences - claims, err := p.authorizeToken(token, p.audiences.Sign) + claims, err := p.authorizeToken(token, p.audiences.SSHSign) if err != nil { return nil, err } @@ -192,8 +191,6 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token sshCertificateOptionsValidator(*opts), - // set the key id to the token subject - sshCertificateKeyIDModifier(claims.Subject), } t := now() @@ -219,6 +216,8 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. sshDefaultValidityModifier(p.claimer), + // Validate that the keyID is equivalent to the token subject. + sshCertKeyIDValidator(claims.Subject), // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. @@ -230,7 +229,6 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // AuthorizeSSHRevoke returns nil if the token is valid, false otherwise. func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error { - // TODO fix audience. _, err := p.authorizeToken(token, p.audiences.SSHRevoke) return err } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 0abed1f3..63f16205 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -212,11 +212,6 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, return nil, err } - // Check for SSH sign-ing request. - if MethodFromContext(ctx) == SignSSHMethod { - return nil, errors.New("ssh certificates not enabled for k8s ServiceAccount provisioners") - } - return []SignOption{ // modifiers / withOptions newProvisionerExtensionOption(TypeK8sSA, p.Name, ""), @@ -227,14 +222,41 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, }, nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *K8sSA) AuthorizeRenewal(cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) } return nil } +// AuthorizeSSHSign validates an request for an SSH certificate. +func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("authorizeSSHSign: ssh ca is disabled for provisioner %s", p.GetID()) + } + _, err := p.authorizeToken(token, p.audiences.SSHSign) + if err != nil { + return nil, errors.Wrap(err, "authorizeSSHSign") + } + + // Default to a user certificate with no principals if not set + signOptions := []SignOption{sshCertificateDefaultsModifier{CertType: SSHUserCert}} + + return append(signOptions, + // Set the default extensions. + &sshDefaultExtensionModifier{}, + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key + &sshDefaultPublicKeyValidator{}, + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require and validate all the default fields in the SSH certificate. + &sshCertificateDefaultValidator{}, + ), nil +} + /* func checkAccess(authz kauthz.AuthorizationV1Interface) error { r := &kauthzApi.SelfSubjectAccessReview{ diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 90e46701..d97f96f2 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -322,7 +322,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption return nil, err } signOptions := []SignOption{ - // set the key id to the token subject + // set the key id to the token email sshCertificateKeyIDModifier(claims.Email), } diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index a8f63cd5..5b65c159 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -49,6 +49,7 @@ type SSHCertificateOptionsValidator interface { // SSHOptions contains the options that can be passed to the SignSSH method. type SSHOptions struct { CertType string `json:"certType"` + KeyID string `json:"keyID"` Principals []string `json:"principals"` ValidAfter TimeDuration `json:"validAfter,omitempty"` ValidBefore TimeDuration `json:"validBefore,omitempty"` @@ -70,6 +71,8 @@ func (o SSHOptions) Modify(cert *ssh.Certificate) error { default: return errors.Errorf("ssh certificate has an unknown type: %s", o.CertType) } + + cert.KeyId = o.KeyID cert.ValidPrincipals = o.Principals if !o.ValidAfter.IsZero() { cert.ValidAfter = uint64(o.ValidAfter.Time().Unix()) @@ -373,6 +376,17 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate) error { } } +// sshCertKeyIDValidator implements a validator for the KeyId attribute. +type sshCertKeyIDValidator string + +// Valid returns an error if the given certificate does not contain the necessary fields. +func (v sshCertKeyIDValidator) Valid(cert *ssh.Certificate) error { + if string(v) != cert.KeyId { + return errors.Errorf("invalid ssh certificate KeyId; want %s, but got %s", string(v), cert.KeyId) + } + return nil +} + // sshCertTypeUInt32 func sshCertTypeUInt32(ct string) uint32 { switch ct { diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 84236b2c..a282692e 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -183,14 +183,6 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er return nil, err } - // Check for SSH sign-ing request. - if MethodFromContext(ctx) == SignSSHMethod { - if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) - } - return p.authorizeSSHSign(claims) - } - // NOTE: This is for backwards compatibility with older versions of cli // and certificates. Older versions added the token subject as the only SAN // in a CSR by default. @@ -222,8 +214,17 @@ func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error return nil } -// authorizeSSHSign returns the list of SignOption for a SignSSH request. -func (p *X5C) authorizeSSHSign(claims *x5cPayload) ([]SignOption, error) { +// AuthorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + + claims, err := p.authorizeToken(token, p.audiences.SSHSign) + if err != nil { + return nil, err + } + if claims.Step == nil || claims.Step.SSH == nil { return nil, errors.New("authorization token must be an SSH provisioning token") } @@ -231,8 +232,6 @@ func (p *X5C) authorizeSSHSign(claims *x5cPayload) ([]SignOption, error) { signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token sshCertificateOptionsValidator(*opts), - // set the key id to the token subject - sshCertificateKeyIDModifier(claims.Subject), } // Add modifiers from custom claims @@ -258,6 +257,8 @@ func (p *X5C) authorizeSSHSign(claims *x5cPayload) ([]SignOption, error) { &sshDefaultExtensionModifier{}, // Checks the validity bounds, and set the validity if has not been set. sshLimitValidityModifier(p.claimer, claims.chains[0][0].NotAfter), + // set the key id to the token subject + sshCertKeyIDValidator(claims.Subject), // Validate public key. &sshDefaultPublicKeyValidator{}, // Validate the validity period. From e679deddd751157edaa70afa5e74b4ba5b9c3f17 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 7 Nov 2019 21:39:36 -0800 Subject: [PATCH 039/163] sshpop token should not allow renew/rekey of user ssh certs --- authority/provisioner/sshpop.go | 7 +++++++ authority/ssh.go | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index e0c4a2f7..9891f495 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -204,6 +204,10 @@ func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Cert if err != nil { return nil, err } + if claims.sshCert.CertType != ssh.HostCert { + return nil, errors.New("sshpop AuthorizeSSHRenew: sshpop certificate must be a host ssh certificate") + } + return claims.sshCert, nil } @@ -215,6 +219,9 @@ func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Cert if err != nil { return nil, nil, err } + if claims.sshCert.CertType != ssh.HostCert { + return nil, nil, errors.New("sshpop AuthorizeSSHRekey: sshpop certificate must be a host ssh certificate") + } return claims.sshCert, []SignOption{ // Validate public key &sshDefaultPublicKeyValidator{}, diff --git a/authority/ssh.go b/authority/ssh.go index 9181b7bc..338f1da1 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -332,7 +332,7 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errors.New("rewnewSSh: cannot renew certificate without validity period") + return nil, errors.New("rewnewSSH: cannot renew certificate without validity period") } dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second va := time.Now() @@ -457,7 +457,7 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errors.New("rekeySSh: cannot rekey certificate without validity period") + return nil, errors.New("rekeySSH: cannot rekey certificate without validity period") } dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second va := time.Now() From 69a7058ff0f5dcb2595637ab4db5eb8f39e81682 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 8 Nov 2019 17:43:54 -0800 Subject: [PATCH 040/163] Remove global check for number of k8sSA provisioners. This was causing a bug in the reload of the ca. --- authority/config.go | 11 +++++++++++ authority/provisioner/k8sSA.go | 6 ------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/authority/config.go b/authority/config.go index e70eba48..b12d1da5 100644 --- a/authority/config.go +++ b/authority/config.go @@ -81,6 +81,17 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { return errors.New("authority.provisioners cannot be empty") } + // Check that only one K8sSA is enabled + var k8sCount int + for _, p := range c.Provisioners { + if p.GetType() == provisioner.TypeK8sSA { + k8sCount++ + } + } + if k8sCount > 1 { + return errors.New("cannot have more than one kubernetes service account provisioner") + } + if c.Template == nil { c.Template = &x509util.ASN1DN{} } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 63f16205..0c90552c 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -25,9 +25,6 @@ const ( k8sSAIssuer = "kubernetes/serviceaccount" ) -// This number must <= 1. We'll verify this in Init() below. -var numK8sSAProvisioners = 0 - // jwtPayload extends jwt.Claims with step attributes. type k8sSAPayload struct { jose.Claims @@ -85,8 +82,6 @@ func (p *K8sSA) Init(config Config) (err error) { return errors.New("provisioner type cannot be empty") case p.Name == "": return errors.New("provisioner name cannot be empty") - case numK8sSAProvisioners >= 1: - return errors.New("cannot have more than one kubernetes service account provisioner") } if p.PubKeys != nil { @@ -134,7 +129,6 @@ func (p *K8sSA) Init(config Config) (err error) { } p.audiences = config.Audiences - numK8sSAProvisioners++ return err } From 54826ded832781004e56c14e5dd0d65a1214e4d6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Nov 2019 11:37:54 -0800 Subject: [PATCH 041/163] Allow to set the ssh user, and registry username and password. --- pki/templates.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pki/templates.go b/pki/templates.go index 8f8b26b2..54c1f168 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -35,8 +35,11 @@ var SSHTemplateData = map[string]string{ // and references the step known_hosts file "config.tpl": `Match exec "step ssh check-host %h" ForwardAgent yes + {{- if .User.User }} + User {{.User.User}} + {{- end }} UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts - ProxyCommand step ssh proxycommand %r %h %p`, + ProxyCommand step ssh proxycommand {{- if .User.RegistryUsername}} --username {{.User.RegistryUsername}}{{end}}{{- if .User.RegistryPassword}} --password {{.User.RegistryPassword}}{{end}} %r %h %p`, // known_hosts.tpl authorizes the ssh hosts key "known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} From 0c3b9ebf45d98b6cb7375f7639c659e1d6ffd7c6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 13 Nov 2019 11:18:05 -0800 Subject: [PATCH 042/163] Fix indentation. --- authority/tls.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index e7c8eb3d..2baa71f0 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -149,15 +149,15 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error ExtKeyUsage: oldCert.ExtKeyUsage, UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage, BasicConstraintsValid: oldCert.BasicConstraintsValid, - IsCA: oldCert.IsCA, - MaxPathLen: oldCert.MaxPathLen, - MaxPathLenZero: oldCert.MaxPathLenZero, - OCSPServer: oldCert.OCSPServer, - IssuingCertificateURL: oldCert.IssuingCertificateURL, - DNSNames: oldCert.DNSNames, - EmailAddresses: oldCert.EmailAddresses, - IPAddresses: oldCert.IPAddresses, - URIs: oldCert.URIs, + IsCA: oldCert.IsCA, + MaxPathLen: oldCert.MaxPathLen, + MaxPathLenZero: oldCert.MaxPathLenZero, + OCSPServer: oldCert.OCSPServer, + IssuingCertificateURL: oldCert.IssuingCertificateURL, + DNSNames: oldCert.DNSNames, + EmailAddresses: oldCert.EmailAddresses, + IPAddresses: oldCert.IPAddresses, + URIs: oldCert.URIs, PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical, PermittedDNSDomains: oldCert.PermittedDNSDomains, ExcludedDNSDomains: oldCert.ExcludedDNSDomains, From a4fd76f1a8fadb8d1324a3a541aceb3b3cd68d4a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 10:48:06 -0800 Subject: [PATCH 043/163] Make provisioner tests compile, they are still failing. --- authority/provisioner/acme_test.go | 6 +++--- authority/provisioner/aws_test.go | 8 ++++---- authority/provisioner/azure_test.go | 8 ++++---- authority/provisioner/gcp_test.go | 4 ++-- authority/provisioner/jwk_test.go | 8 ++++---- authority/provisioner/k8sSA_test.go | 8 ++++---- authority/provisioner/noop_test.go | 4 ++-- authority/provisioner/oidc_test.go | 8 ++++---- authority/provisioner/x5c_test.go | 13 +++++++------ 9 files changed, 34 insertions(+), 33 deletions(-) diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index 51231ba3..2ffdd195 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -91,10 +91,10 @@ func TestACME_Init(t *testing.T) { func TestACME_AuthorizeRevoke(t *testing.T) { p, err := generateACME() assert.FatalError(t, err) - assert.Nil(t, p.AuthorizeRevoke("")) + assert.Nil(t, p.AuthorizeRevoke(context.TODO(), "")) } -func TestACME_AuthorizeRenewal(t *testing.T) { +func TestACME_AuthorizeRenew(t *testing.T) { p1, err := generateACME() assert.FatalError(t, err) p2, err := generateACME() @@ -120,7 +120,7 @@ func TestACME_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(tt.args.cert); err != nil { + if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); err != nil { if assert.NotNil(t, tt.err) { assert.HasPrefix(t, err.Error(), tt.err.Error()) } diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index dd82ec9b..bb8e74f8 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -447,7 +447,7 @@ func TestAWS_AuthorizeSign_SSH(t *testing.T) { }) } } -func TestAWS_AuthorizeRenewal(t *testing.T) { +func TestAWS_AuthorizeRenew(t *testing.T) { p1, err := generateAWS() assert.FatalError(t, err) p2, err := generateAWS() @@ -473,8 +473,8 @@ func TestAWS_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.aws.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("AWS.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.aws.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("AWS.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } }) } @@ -502,7 +502,7 @@ func TestAWS_AuthorizeRevoke(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.aws.AuthorizeRevoke(tt.args.token); (err != nil) != tt.wantErr { + if err := tt.aws.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr { t.Errorf("AWS.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 82ff095e..68e4adef 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -388,7 +388,7 @@ func TestAzure_AuthorizeSign_SSH(t *testing.T) { } } -func TestAzure_AuthorizeRenewal(t *testing.T) { +func TestAzure_AuthorizeRenew(t *testing.T) { p1, err := generateAzure() assert.FatalError(t, err) p2, err := generateAzure() @@ -414,8 +414,8 @@ func TestAzure_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.azure.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("Azure.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.azure.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } }) } @@ -443,7 +443,7 @@ func TestAzure_AuthorizeRevoke(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.azure.AuthorizeRevoke(tt.args.token); (err != nil) != tt.wantErr { + if err := tt.azure.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr { t.Errorf("Azure.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 13a735ae..23e45b80 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -461,7 +461,7 @@ func TestGCP_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { + if err := tt.prov.AuthorizeRenewal(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("GCP.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -492,7 +492,7 @@ func TestGCP_AuthorizeRevoke(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.gcp.AuthorizeRevoke(tt.args.token); (err != nil) != tt.wantErr { + if err := tt.gcp.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr { t.Errorf("GCP.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index 185f1596..4861b8c5 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -215,7 +215,7 @@ func TestJWK_AuthorizeRevoke(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRevoke(tt.args.token); err != nil { + if err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token); err != nil { if assert.NotNil(t, tt.err) { assert.HasPrefix(t, err.Error(), tt.err.Error()) } @@ -296,7 +296,7 @@ func TestJWK_AuthorizeSign(t *testing.T) { } } -func TestJWK_AuthorizeRenewal(t *testing.T) { +func TestJWK_AuthorizeRenew(t *testing.T) { p1, err := generateJWK() assert.FatalError(t, err) p2, err := generateJWK() @@ -322,8 +322,8 @@ func TestJWK_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("JWK.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("JWK.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 31bf6d0a..692e7bab 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -219,7 +219,7 @@ func TestK8sSA_AuthorizeRevoke(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if err := tc.p.AuthorizeRevoke(tc.token); err != nil { + if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -230,7 +230,7 @@ func TestK8sSA_AuthorizeRevoke(t *testing.T) { } } -func TestK8sSA_AuthorizeRenewal(t *testing.T) { +func TestK8sSA_AuthorizeRenew(t *testing.T) { p1, err := generateK8sSA(nil) assert.FatalError(t, err) p2, err := generateK8sSA(nil) @@ -256,8 +256,8 @@ func TestK8sSA_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("X5C.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("X5C.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/authority/provisioner/noop_test.go b/authority/provisioner/noop_test.go index a389b6b6..c79e7460 100644 --- a/authority/provisioner/noop_test.go +++ b/authority/provisioner/noop_test.go @@ -14,8 +14,8 @@ func Test_noop(t *testing.T) { assert.Equals(t, "noop", p.GetName()) assert.Equals(t, noopType, p.GetType()) assert.Equals(t, nil, p.Init(Config{})) - assert.Equals(t, nil, p.AuthorizeRenewal(&x509.Certificate{})) - assert.Equals(t, nil, p.AuthorizeRevoke("foo")) + assert.Equals(t, nil, p.AuthorizeRenew(context.TODO(), &x509.Certificate{})) + assert.Equals(t, nil, p.AuthorizeRevoke(context.TODO(), "foo")) kid, key, ok := p.GetEncryptedKey() assert.Equals(t, "", kid) diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 516e0f0e..e26ded0a 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -497,7 +497,7 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.prov.AuthorizeRevoke(tt.args.token) + err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token) if (err != nil) != tt.wantErr { fmt.Println(tt) t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr) @@ -507,7 +507,7 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) { } } -func TestOIDC_AuthorizeRenewal(t *testing.T) { +func TestOIDC_AuthorizeRenew(t *testing.T) { p1, err := generateOIDC() assert.FatalError(t, err) p2, err := generateOIDC() @@ -533,8 +533,8 @@ func TestOIDC_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("OIDC.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 477e3267..4fc4dbe0 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -554,11 +554,12 @@ func TestX5C_AuthorizeSign(t *testing.T) { } } -func TestX5C_authorizeSSHSign(t *testing.T) { +func TestX5C_AuthorizeSSHSign(t *testing.T) { _, fn := mockNow() defer fn() type test struct { p *X5C + token string claims *x5cPayload err error } @@ -618,7 +619,7 @@ func TestX5C_authorizeSSHSign(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if opts, err := tc.p.authorizeSSHSign(tc.claims); err != nil { + if opts, err := tc.p.AuthorizeSSHSign(context.TODO(), tc.token); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -706,7 +707,7 @@ func TestX5C_AuthorizeRevoke(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if err := tc.p.AuthorizeRevoke(tc.token); err != nil { + if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -717,7 +718,7 @@ func TestX5C_AuthorizeRevoke(t *testing.T) { } } -func TestX5C_AuthorizeRenewal(t *testing.T) { +func TestX5C_AuthorizeRenew(t *testing.T) { p1, err := generateX5C(nil) assert.FatalError(t, err) p2, err := generateX5C(nil) @@ -743,8 +744,8 @@ func TestX5C_AuthorizeRenewal(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("X5C.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("X5C.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } }) } From efc2180c4a65b7a45c9d6ebe8072461c3e88bc24 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 10:49:13 -0800 Subject: [PATCH 044/163] Complete AuthDB interface. --- authority/db_test.go | 53 +++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/authority/db_test.go b/authority/db_test.go index bd6b27ca..72684c63 100644 --- a/authority/db_test.go +++ b/authority/db_test.go @@ -8,26 +8,18 @@ import ( ) type MockAuthDB struct { - err error - ret1 interface{} - init func(*db.Config) (db.AuthDB, error) - isRevoked func(string) (bool, error) - revoke func(rci *db.RevokedCertificateInfo) error - storeCertificate func(crt *x509.Certificate) error - useToken func(id, tok string) (bool, error) - isSSHHost func(principal string) (bool, error) - storeSSHCertificate func(crt *ssh.Certificate) error - shutdown func() error -} - -func (m *MockAuthDB) Init(c *db.Config) (db.AuthDB, error) { - if m.init != nil { - return m.init(c) - } - if m.ret1 == nil { - return nil, m.err - } - return m.ret1.(*db.DB), m.err + err error + ret1 interface{} + isRevoked func(string) (bool, error) + isSSHRevoked func(string) (bool, error) + revoke func(rci *db.RevokedCertificateInfo) error + revokeSSH func(rci *db.RevokedCertificateInfo) error + storeCertificate func(crt *x509.Certificate) error + useToken func(id, tok string) (bool, error) + isSSHHost func(principal string) (bool, error) + storeSSHCertificate func(crt *ssh.Certificate) error + getSSHHostPrincipals func() ([]string, error) + shutdown func() error } func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { @@ -37,6 +29,13 @@ func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { return m.ret1.(bool), m.err } +func (m *MockAuthDB) IsSSHRevoked(sn string) (bool, error) { + if m.isSSHRevoked != nil { + return m.isSSHRevoked(sn) + } + return m.ret1.(bool), m.err +} + func (m *MockAuthDB) UseToken(id, tok string) (bool, error) { if m.useToken != nil { return m.useToken(id, tok) @@ -54,6 +53,13 @@ func (m *MockAuthDB) Revoke(rci *db.RevokedCertificateInfo) error { return m.err } +func (m *MockAuthDB) RevokeSSH(rci *db.RevokedCertificateInfo) error { + if m.revokeSSH != nil { + return m.revokeSSH(rci) + } + return m.err +} + func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error { if m.storeCertificate != nil { return m.storeCertificate(crt) @@ -75,6 +81,13 @@ func (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error { return m.err } +func (m *MockAuthDB) GetSSHHostPrincipals() ([]string, error) { + if m.getSSHHostPrincipals != nil { + return m.getSSHHostPrincipals() + } + return m.ret1.([]string), m.err +} + func (m *MockAuthDB) Shutdown() error { if m.shutdown != nil { return m.shutdown() From 2cb6bd880be8c65fc5fa79d110761fad6478738b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:18:49 -0800 Subject: [PATCH 045/163] Make audiences compatible with the old version. --- authority/config.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/authority/config.go b/authority/config.go index b12d1da5..0fd1e5c9 100644 --- a/authority/config.go +++ b/authority/config.go @@ -197,17 +197,25 @@ func (c *Config) getAudiences() provisioner.Audiences { for _, name := range c.DNSNames { audiences.Sign = append(audiences.Sign, - fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name)) + fmt.Sprintf("https://%s/1.0/sign", name), + fmt.Sprintf("https://%s/sign", name)) audiences.Revoke = append(audiences.Revoke, - fmt.Sprintf("https://%s/revoke", name), fmt.Sprintf("https://%s/1.0/revoke", name)) + fmt.Sprintf("https://%s/1.0/revoke", name), + fmt.Sprintf("https://%s/revoke", name)) audiences.SSHSign = append(audiences.SSHSign, - fmt.Sprintf("https://%s/ssh/sign", name), fmt.Sprintf("https://%s/1.0/ssh/sign", name)) + fmt.Sprintf("https://%s/1.0/ssh/sign", name), + fmt.Sprintf("https://%s/ssh/sign", name), + fmt.Sprintf("https://%s/1.0/sign", name), + fmt.Sprintf("https://%s/sign", name)) audiences.SSHRevoke = append(audiences.SSHRevoke, - fmt.Sprintf("https://%s/ssh/revoke", name), fmt.Sprintf("https://%s/1.0/ssh/revoke", name)) + fmt.Sprintf("https://%s/1.0/ssh/revoke", name), + fmt.Sprintf("https://%s/ssh/revoke", name)) audiences.SSHRenew = append(audiences.SSHRenew, - fmt.Sprintf("https://%s/ssh/renew", name), fmt.Sprintf("https://%s/1.0/ssh/renew", name)) + fmt.Sprintf("https://%s/1.0/ssh/renew", name), + fmt.Sprintf("https://%s/ssh/renew", name)) audiences.SSHRekey = append(audiences.SSHRekey, - fmt.Sprintf("https://%s/ssh/rekey", name), fmt.Sprintf("https://%s/1.0/ssh/rekey", name)) + fmt.Sprintf("https://%s/1.0/ssh/rekey", name), + fmt.Sprintf("https://%s/ssh/rekey", name)) } return audiences From fcccb06696e2a5b47dbdb39ac7d0f3b1fd2b8f8e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:26:37 -0800 Subject: [PATCH 046/163] Fix some provisioner tests --- authority/provisioner/aws_test.go | 6 +++--- authority/provisioner/azure_test.go | 6 +++--- authority/provisioner/gcp_test.go | 6 +++--- authority/provisioner/jwk.go | 4 ++++ authority/provisioner/jwk_test.go | 14 +++++++------- authority/provisioner/oidc_test.go | 6 +++--- authority/provisioner/sign_ssh_options.go | 7 +++++-- authority/provisioner/utils_test.go | 8 ++++++-- authority/provisioner/x5c.go | 5 +++++ 9 files changed, 39 insertions(+), 23 deletions(-) diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index bb8e74f8..e855bf9f 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -360,7 +360,7 @@ func TestAWS_AuthorizeSign(t *testing.T) { } } -func TestAWS_AuthorizeSign_SSH(t *testing.T) { +func TestAWS_AuthorizeSSHSign(t *testing.T) { tm, fn := mockNow() defer fn() @@ -425,9 +425,9 @@ func TestAWS_AuthorizeSign_SSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.aws.AuthorizeSign(ctx, tt.args.token) + got, err := tt.aws.AuthorizeSSHSign(ctx, tt.args.token) if (err != nil) != tt.wantErr { - t.Errorf("AWS.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("AWS.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 68e4adef..1760ed5c 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -310,7 +310,7 @@ func TestAzure_AuthorizeSign(t *testing.T) { } } -func TestAzure_AuthorizeSign_SSH(t *testing.T) { +func TestAzure_AuthorizeSSHSign(t *testing.T) { tm, fn := mockNow() defer fn() @@ -365,9 +365,9 @@ func TestAzure_AuthorizeSign_SSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.azure.AuthorizeSign(ctx, tt.args.token) + got, err := tt.azure.AuthorizeSSHSign(ctx, tt.args.token) if (err != nil) != tt.wantErr { - t.Errorf("Azure.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Azure.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 23e45b80..4764dfc7 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -345,7 +345,7 @@ func TestGCP_AuthorizeSign(t *testing.T) { } } -func TestGCP_AuthorizeSign_SSH(t *testing.T) { +func TestGCP_AuthorizeSSHSign(t *testing.T) { tm, fn := mockNow() defer fn() @@ -412,9 +412,9 @@ func TestGCP_AuthorizeSign_SSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.gcp.AuthorizeSign(ctx, tt.args.token) + got, err := tt.gcp.AuthorizeSSHSign(ctx, tt.args.token) if (err != nil) != tt.wantErr { - t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GCP.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 52f83846..f6ed9bbf 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -207,6 +207,10 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, if !opts.ValidBefore.IsZero() { signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) } + // Make sure to define the the KeyID + if opts.KeyID == "" { + signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject)) + } // Default to a user certificate with no principals if not set signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index 4861b8c5..47a6e7cc 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -329,7 +329,7 @@ func TestJWK_AuthorizeRenew(t *testing.T) { } } -func TestJWK_AuthorizeSign_SSH(t *testing.T) { +func TestJWK_AuthorizeSSHSign(t *testing.T) { tm, fn := mockNow() defer fn() @@ -338,7 +338,7 @@ func TestJWK_AuthorizeSign_SSH(t *testing.T) { jwk, err := decryptJSONWebKey(p1.EncryptedKey) assert.FatalError(t, err) - iss, aud := p1.Name, testAudiences.Sign[0] + iss, aud := p1.Name, testAudiences.SSHSign[0] t1, err := generateSimpleSSHUserToken(iss, aud, jwk) assert.FatalError(t, err) @@ -400,9 +400,9 @@ func TestJWK_AuthorizeSign_SSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.prov.AuthorizeSign(ctx, tt.args.token) + got, err := tt.prov.AuthorizeSSHSign(ctx, tt.args.token) if (err != nil) != tt.wantErr { - t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { @@ -432,7 +432,7 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) { jwk, err := decryptJSONWebKey(p1.EncryptedKey) assert.FatalError(t, err) - sub, iss, aud, iat := "subject@smallstep.com", p1.Name, testAudiences.Sign[0], time.Now() + sub, iss, aud, iat := "subject@smallstep.com", p1.Name, testAudiences.SSHSign[0], time.Now() key, err := generateJSONWebKey() assert.FatalError(t, err) @@ -514,8 +514,8 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignSSHMethod) token, err := generateSSHToken(tt.args.sub, tt.args.iss, tt.args.aud, tt.args.iat, tt.args.tokSSHOpts, tt.args.jwk) assert.FatalError(t, err) - if got, err := tt.prov.AuthorizeSign(ctx, token); (err != nil) != tt.wantErr { - t.Errorf("JWK.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) + if got, err := tt.prov.AuthorizeSSHSign(ctx, token); (err != nil) != tt.wantErr { + t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) } else if !tt.wantErr && assert.NotNil(t, got) { var opts SSHOptions if tt.args.userSSHOpts != nil { diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index e26ded0a..8e0c823c 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -330,7 +330,7 @@ func TestOIDC_AuthorizeSign(t *testing.T) { } } -func TestOIDC_AuthorizeSign_SSH(t *testing.T) { +func TestOIDC_AuthorizeSSHSign(t *testing.T) { tm, fn := mockNow() defer fn() @@ -427,9 +427,9 @@ func TestOIDC_AuthorizeSign_SSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.prov.AuthorizeSign(ctx, tt.args.token) + got, err := tt.prov.AuthorizeSSHSign(ctx, tt.args.token) if (err != nil) != tt.wantErr { - t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("OIDC.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 5b65c159..06ddf697 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -74,15 +74,18 @@ func (o SSHOptions) Modify(cert *ssh.Certificate) error { cert.KeyId = o.KeyID cert.ValidPrincipals = o.Principals + + t := now() if !o.ValidAfter.IsZero() { - cert.ValidAfter = uint64(o.ValidAfter.Time().Unix()) + cert.ValidAfter = uint64(o.ValidAfter.RelativeTime(t).Unix()) } if !o.ValidBefore.IsZero() { - cert.ValidBefore = uint64(o.ValidBefore.Time().Unix()) + cert.ValidBefore = uint64(o.ValidBefore.RelativeTime(t).Unix()) } if cert.ValidAfter > 0 && cert.ValidBefore > 0 && cert.ValidAfter > cert.ValidBefore { return errors.New("ssh certificate valid after cannot be greater than valid before") } + return nil } diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 554e38ea..f02c53b4 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -38,8 +38,12 @@ var ( EnableSSHCA: &defaultEnableSSHCA, } testAudiences = Audiences{ - Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"}, - Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"}, + Sign: []string{"https://ca.smallstep.com/1.0/sign", "https://ca.smallstep.com/sign"}, + Revoke: []string{"https://ca.smallstep.com/1.0/revoke", "https://ca.smallstep.com/revoke"}, + SSHSign: []string{"https://ca.smallstep.com/1.0/ssh/sign"}, + SSHRevoke: []string{"https://ca.smallstep.com/1.0/ssh/revoke"}, + SSHRenew: []string{"https://ca.smallstep.com/1.0/ssh/renew"}, + SSHRekey: []string{"https://ca.smallstep.com/1.0/ssh/rekey"}, } ) diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index a282692e..4fa15a44 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -235,6 +235,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, } // Add modifiers from custom claims + // FIXME: this is also set in the sign method using SSHOptions.Modify. if opts.CertType != "" { signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) } @@ -248,6 +249,10 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, if !opts.ValidBefore.IsZero() { signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) } + // Make sure to define the the KeyID + if opts.KeyID == "" { + signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject)) + } // Default to a user certificate with no principals if not set signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) From be93c9e1f4f91def2a07c8753a2b865ed547fa5e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:27:12 -0800 Subject: [PATCH 047/163] Add missing comment. --- authority/provisioner/jwk.go | 1 + 1 file changed, 1 insertion(+) diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index f6ed9bbf..05c079d7 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -195,6 +195,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, t := now() // Add modifiers from custom claims + // FIXME: this is also set in the sign method using SSHOptions.Modify. if opts.CertType != "" { signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) } From 43b663e0c3954033a8cc2750742d6579ad1dfde4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:29:04 -0800 Subject: [PATCH 048/163] Move Option type to a new file. --- authority/authority.go | 11 ---- authority/options.go | 16 ++++++ go.mod | 1 - go.sum | 122 ----------------------------------------- 4 files changed, 16 insertions(+), 134 deletions(-) create mode 100644 authority/options.go diff --git a/authority/authority.go b/authority/authority.go index a62e5034..05a2e43a 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -41,17 +41,6 @@ type Authority struct { initOnce bool } -// Option sets options to the Authority. -type Option func(*Authority) - -// 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) { - a.db = db - } -} - // New creates and initiates a new Authority type. func New(config *Config, opts ...Option) (*Authority, error) { err := config.Validate() diff --git a/authority/options.go b/authority/options.go new file mode 100644 index 00000000..ebf6fe08 --- /dev/null +++ b/authority/options.go @@ -0,0 +1,16 @@ +package authority + +import ( + "github.com/smallstep/certificates/db" +) + +// Option sets options to the Authority. +type Option func(*Authority) + +// 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) { + a.db = db + } +} diff --git a/go.mod b/go.mod index f5d191f3..449237b9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.13 require ( github.com/Masterminds/sprig/v3 v3.0.0 github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible - github.com/golangci/golangci-lint v1.18.0 // indirect github.com/newrelic/go-agent v1.11.0 github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 diff --git a/go.sum b/go.sum index ab8bc2e9..ce311f89 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,11 @@ 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/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk= github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= -github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM= -github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -28,76 +23,27 @@ github.com/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6St github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= -github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= -github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= -github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= -github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= -github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= -github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= -github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= -github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= -github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= -github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= -github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= -github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg= -github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= -github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= -github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= -github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= -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/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -107,54 +53,31 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= -github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= -github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= -github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= -github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= @@ -175,92 +98,47 @@ github.com/smallstep/nosql v0.1.1/go.mod h1:qyxCqeyGwkuM6bfJSY3sg+aiXEiD0GbQOPzI github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= -github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= -github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= -github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= -github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -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= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -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-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= From 8585b29711471da4a0af1b02ff48f0674c1a2f81 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 18:07:16 -0800 Subject: [PATCH 049/163] Make test to compile, they still fail. --- authority/authorize_test.go | 21 ++++++++------------- authority/tls_test.go | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/authority/authorize_test.go b/authority/authorize_test.go index 23a2983c..fa14caa0 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -7,10 +7,9 @@ import ( "testing" "time" - "github.com/smallstep/certificates/authority/provisioner" - "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/jose" @@ -272,9 +271,10 @@ func TestAuthority_authorizeRevoke(t *testing.T) { validAudience := []string{"https://test.ca.smallstep.com/revoke"} type authorizeTest struct { - auth *Authority - opts *RevokeOptions - err error + auth *Authority + token string + opts *RevokeOptions + err error } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/token/invalid-ott": func(t *testing.T) *authorizeTest { @@ -349,17 +349,12 @@ func TestAuthority_authorizeRevoke(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - p, err := tc.auth.authorizeRevoke(tc.opts) - if err != nil { + if err := tc.auth.authorizeRevoke(context.TODO(), tc.token); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { - if assert.Nil(t, tc.err) { - if assert.NotNil(t, p) { - assert.Equals(t, p.GetID(), "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") - } - } + assert.Nil(t, tc.err) } }) } @@ -640,7 +635,7 @@ func TestAuthority_authorizeRenewal(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - err := tc.auth.authorizeRenewal(tc.crt) + err := tc.auth.authorizeRenew(tc.crt) if err != nil { if assert.NotNil(t, tc.err) { switch v := err.(type) { diff --git a/authority/tls_test.go b/authority/tls_test.go index cee44535..c5c7f8c1 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -816,7 +816,7 @@ func TestRevoke(t *testing.T) { for name, f := range tests { tc := f() t.Run(name, func(t *testing.T) { - if err := tc.a.Revoke(tc.opts); err != nil { + if err := tc.a.Revoke(context.TODO(), tc.opts); err != nil { if assert.NotNil(t, tc.err) { switch v := err.(type) { case *apiError: From 86a0558587dde15f9553e8fc170eb4730c014ef0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 18:24:58 -0800 Subject: [PATCH 050/163] Add support for /ssh/bastion method. --- api/api.go | 1 + api/api_test.go | 8 ++++++ api/ssh.go | 47 ++++++++++++++++++++++++++++++++++++ api/ssh_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++ authority/authority.go | 2 ++ authority/options.go | 8 ++++++ authority/ssh.go | 32 ++++++++++++++++++++++-- authority/ssh_test.go | 46 +++++++++++++++++++++++++++++++++++ 8 files changed, 197 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 68334dcb..334def24 100644 --- a/api/api.go +++ b/api/api.go @@ -261,6 +261,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost) r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts) + r.MethodFunc("POST", "/ssh/bastion", h.SSHBastion) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) diff --git a/api/api_test.go b/api/api_test.go index 1938e300..e68eb7db 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -557,6 +557,7 @@ type mockAuthority struct { getSSHFederation func() (*authority.SSHKeys, error) getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) checkSSHHost func(principal string) (bool, error) + getSSHBastion func(user string, hostname string) (*authority.Bastion, error) } // TODO: remove once Authorize is deprecated. @@ -711,6 +712,13 @@ func (m *mockAuthority) CheckSSHHost(principal string) (bool, error) { return m.ret1.(bool), m.err } +func (m *mockAuthority) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) { + if m.getSSHBastion != nil { + return m.getSSHBastion(user, hostname) + } + return m.ret1.(*authority.Bastion), m.err +} + func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority diff --git a/api/ssh.go b/api/ssh.go index 11fd3a89..7a2ba282 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -24,6 +24,7 @@ type SSHAuthority interface { GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) CheckSSHHost(principal string) (bool, error) GetSSHHosts() ([]string, error) + GetSSHBastion(user string, hostname string) (*authority.Bastion, error) } // SSHSignRequest is the request body of an SSH certificate request. @@ -207,6 +208,28 @@ type SSHCheckPrincipalResponse struct { Exists bool `json:"exists"` } +// SSHBastionRequest is the request body used to get the bastion for a given +// host. +type SSHBastionRequest struct { + User string `json:"user"` + Hostname string `json:"hostname"` +} + +// Validate checks the values of the SSHBastionRequest. +func (r *SSHBastionRequest) Validate() error { + if r.Hostname == "" { + return errors.New("missing or empty hostname") + } + return nil +} + +// SSHBastionResponse is the response body used to return the bastion for a +// given host. +type SSHBastionResponse struct { + Hostname string `json:"hostname"` + Bastion *authority.Bastion `json:"bastion,omitempty"` +} + // SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token // (ott) from the body and creates a new SSH certificate with the information in // the request. @@ -392,3 +415,27 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { Hosts: hosts, }) } + +// SSHBastion provides returns the bastion configured if any. +func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) { + var body SSHBastionRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + if err := body.Validate(); err != nil { + WriteError(w, BadRequest(err)) + return + } + + bastion, err := h.Authority.GetSSHBastion(body.User, body.Hostname) + if err != nil { + WriteError(w, InternalServerError(err)) + return + } + + JSON(w, &SSHBastionResponse{ + Hostname: body.Hostname, + Bastion: bastion, + }) +} diff --git a/api/ssh_test.go b/api/ssh_test.go index e4e2fd9b..cc615ee7 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -537,6 +537,61 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { } } +func Test_caHandler_SSHBastion(t *testing.T) { + bastion := &authority.Bastion{ + Hostname: "bastion.local", + } + bastionPort := &authority.Bastion{ + Hostname: "bastion.local", + Port: "2222", + } + + tests := []struct { + name string + bastion *authority.Bastion + bastionErr error + req []byte + body []byte + statusCode int + }{ + {"ok", bastion, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local"}}`), http.StatusOK}, + {"ok", bastionPort, nil, []byte(`{"hostname":"host.local","user":"user"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local","port":"2222"}}`), http.StatusOK}, + {"empty", nil, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local"}`), http.StatusOK}, + {"bad json", bastion, nil, []byte(`bad json`), nil, http.StatusBadRequest}, + {"bad request", bastion, nil, []byte(`{"hostname": ""}`), nil, http.StatusBadRequest}, + {"error", nil, fmt.Errorf("an error"), []byte(`{"hostname":"host.local"}`), nil, http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ + getSSHBastion: func(user, hostname string) (*authority.Bastion, error) { + return tt.bastion, tt.bastionErr + }, + }).(*caHandler) + + req := httptest.NewRequest("POST", "http://example.com/ssh/bastion", bytes.NewReader(tt.req)) + w := httptest.NewRecorder() + h.SSHBastion(logging.NewResponseLogger(w), req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.SSHBastion StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.SSHBastion unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), tt.body) { + t.Errorf("caHandler.SSHBastion Body = %s, wants %s", body, tt.body) + } + } + }) + } +} + func TestSSHPublicKey_MarshalJSON(t *testing.T) { key, err := ssh.NewPublicKey(sshUserKey.Public()) assert.FatalError(t, err) diff --git a/authority/authority.go b/authority/authority.go index 05a2e43a..091b84b9 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -39,6 +39,8 @@ type Authority struct { db db.AuthDB // Do not re-initialize initOnce bool + // Custom functions + sshBastionFunc func(user, hostname string) (*Bastion, error) } // New creates and initiates a new Authority type. diff --git a/authority/options.go b/authority/options.go index ebf6fe08..3d602255 100644 --- a/authority/options.go +++ b/authority/options.go @@ -14,3 +14,11 @@ func WithDatabase(db db.AuthDB) Option { a.db = db } } + +// WithSSHBastionFunc defines 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) { + a.sshBastionFunc = fn + } +} diff --git a/authority/ssh.go b/authority/ssh.go index 338f1da1..67c884b8 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -34,8 +34,18 @@ type SSHConfig struct { HostKey string `json:"hostKey"` UserKey string `json:"userKey"` Keys []*SSHPublicKey `json:"keys,omitempty"` - AddUserPrincipal string `json:"addUserPrincipal"` - AddUserCommand string `json:"addUserCommand"` + AddUserPrincipal string `json:"addUserPrincipal,omitempty"` + AddUserCommand string `json:"addUserCommand,omitempty"` + Bastion *Bastion `json:"bastion,omitempty"` +} + +// Bastion contains the custom properties used on bastion. +type Bastion struct { + Hostname string `json:"hostname"` + User string `json:"user,omitempty"` + Port string `json:"port,omitempty"` + Command string `json:"cmd,omitempty"` + Flags string `json:"flags,omitempty"` } // Validate checks the fields in SSHConfig. @@ -157,6 +167,24 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template return output, nil } +// GetSSHBastion returns the bastion configuration, for the given pair user, +// hostname. +func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error) { + if a.sshBastionFunc != nil { + return a.sshBastionFunc(user, hostname) + } + if a.config.SSH != nil { + if a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != "" { + return a.config.SSH.Bastion, nil + } + return nil, nil + } + return nil, &apiError{ + err: errors.New("getSSHBastion: ssh is not configured"), + code: http.StatusNotFound, + } +} + // authorizeSSHSign loads the provisioner from the token, checks that it has not // been used again and calls the provisioner AuthorizeSSHSign method. Returns a // list of methods to apply to the signing flow. diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 629bc3b4..c2f4ceb7 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -598,3 +599,48 @@ func TestSSHPublicKey_PublicKey(t *testing.T) { }) } } + +func TestAuthority_GetSSHBastion(t *testing.T) { + bastion := &Bastion{ + Hostname: "bastion.local", + Port: "2222", + } + type fields struct { + config *Config + sshBastionFunc func(user, hostname string) (*Bastion, error) + } + type args struct { + user string + hostname string + } + tests := []struct { + name string + fields fields + args args + want *Bastion + wantErr bool + }{ + {"config", fields{&Config{SSH: &SSHConfig{Bastion: bastion}}, nil}, args{"user", "host.local"}, bastion, false}, + {"nil", fields{&Config{SSH: &SSHConfig{Bastion: nil}}, nil}, args{"user", "host.local"}, nil, false}, + {"empty", fields{&Config{SSH: &SSHConfig{Bastion: &Bastion{}}}, nil}, args{"user", "host.local"}, nil, false}, + {"func", fields{&Config{}, func(_, _ string) (*Bastion, error) { return bastion, nil }}, args{"user", "host.local"}, bastion, false}, + {"func err", fields{&Config{}, func(_, _ string) (*Bastion, error) { return nil, errors.New("foo") }}, args{"user", "host.local"}, nil, true}, + {"error", fields{&Config{SSH: nil}, nil}, args{"user", "host.local"}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authority{ + config: tt.fields.config, + sshBastionFunc: tt.fields.sshBastionFunc, + } + got, err := a.GetSSHBastion(tt.args.user, tt.args.hostname) + if (err != nil) != tt.wantErr { + t.Errorf("Authority.GetSSHBastion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Authority.GetSSHBastion() = %v, want %v", got, tt.want) + } + }) + } +} From 35b753924398e0a135d933ec478d087ea4ebcaae Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 20:32:38 -0800 Subject: [PATCH 051/163] Add client method for SSHBastion --- ca/client.go | 21 ++++++++++++++++ ca/client_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/ca/client.go b/ca/client.go index 68c6be22..a7cd0a7a 100644 --- a/ca/client.go +++ b/ca/client.go @@ -694,6 +694,27 @@ func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) { return &hosts, nil } +// SSHBastion performs the POST /ssh/bastion request to the CA. +func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/bastion"}) + resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var bastion api.SSHBastionResponse + if err := readJSON(resp.Body, &bastion); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &bastion, nil +} + // RootFingerprint is a helper method that returns the current root fingerprint. // It does an health connection and gets the fingerprint from the TLS verified // chains. diff --git a/ca/client_test.go b/ca/client_test.go index fc3a5049..f9a968c0 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -18,6 +18,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/crypto/x509util" "golang.org/x/crypto/ssh" @@ -882,3 +883,64 @@ func TestClient_RootFingerprintWithServer(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7", fp) } + +func TestClient_SSHBastion(t *testing.T) { + ok := &api.SSHBastionResponse{ + Hostname: "host.local", + Bastion: &authority.Bastion{ + Hostname: "bastion.local", + }, + } + badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + + tests := []struct { + name string + request *api.SSHBastionRequest + response interface{} + responseCode int + wantErr bool + }{ + {"ok", &api.SSHBastionRequest{Hostname: "host.local"}, ok, 200, false}, + {"bad response", &api.SSHBastionRequest{Hostname: "host.local"}, "bad json", 200, true}, + {"empty request", &api.SSHBastionRequest{}, badRequest, 403, true}, + {"nil request", nil, badRequest, 403, true}, + } + + srv := httptest.NewServer(nil) + defer srv.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport)) + if err != nil { + t.Errorf("NewClient() error = %v", err) + return + } + + srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + api.JSONStatus(w, tt.response, tt.responseCode) + }) + + got, err := c.SSHBastion(tt.request) + if (err != nil) != tt.wantErr { + fmt.Printf("%+v", err) + t.Errorf("Client.SSHBastion() error = %v, wantErr %v", err, tt.wantErr) + return + } + + switch { + case err != nil: + if got != nil { + t.Errorf("Client.SSHBastion() = %v, want nil", got) + } + if tt.responseCode != 200 && !reflect.DeepEqual(err, tt.response) { + t.Errorf("Client.SSHBastion() error = %v, want %v", err, tt.response) + } + default: + if !reflect.DeepEqual(got, tt.response) { + t.Errorf("Client.SSHBastion() = %v, want %v", got, tt.response) + } + } + }) + } +} From 6ca1df5081b030db109dcff1f012cf7369584329 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 14 Nov 2019 20:38:07 -0800 Subject: [PATCH 052/163] Add WithGetIdentityFunc option and attr to authority * Add Identity type to provisioner --- authority/authority.go | 3 ++- authority/options.go | 11 ++++++++++- authority/provisioner/provisioner.go | 6 ++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 091b84b9..77c887a2 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -40,7 +40,8 @@ type Authority struct { // Do not re-initialize initOnce bool // Custom functions - sshBastionFunc func(user, hostname string) (*Bastion, error) + sshBastionFunc func(user, hostname string) (*Bastion, error) + getIdentityFunc func(p provisioner.Interface, email string) (*provisioner.Identity, error) } // New creates and initiates a new Authority type. diff --git a/authority/options.go b/authority/options.go index 3d602255..409e8c2d 100644 --- a/authority/options.go +++ b/authority/options.go @@ -1,6 +1,7 @@ package authority import ( + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" ) @@ -15,10 +16,18 @@ func WithDatabase(db db.AuthDB) Option { } } -// WithSSHBastionFunc defines sets a custom function to get the bastion for a +// 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) { a.sshBastionFunc = fn } } + +// 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) { + a.getIdentityFunc = fn + } +} diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 4a17626c..8d0673a3 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -319,6 +319,12 @@ func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certif return nil, nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRekey") } +// Identity is the type representing an externally supplied identity that is used +// by provisioners to populate certificate fields. +type Identity struct { + Usernames []string `json:"usernames"` +} + // MockProvisioner for testing type MockProvisioner struct { Mret1, Mret2, Mret3 interface{} From b299a73f3c7c9b5de1c3e7166229a4865498f9a1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 22:09:20 -0800 Subject: [PATCH 053/163] Remove registry urls from templates. --- pki/templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pki/templates.go b/pki/templates.go index 54c1f168..8cf62674 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -39,7 +39,7 @@ var SSHTemplateData = map[string]string{ User {{.User.User}} {{- end }} UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts - ProxyCommand step ssh proxycommand {{- if .User.RegistryUsername}} --username {{.User.RegistryUsername}}{{end}}{{- if .User.RegistryPassword}} --password {{.User.RegistryPassword}}{{end}} %r %h %p`, + ProxyCommand step ssh proxycommand %r %h %p`, // known_hosts.tpl authorizes the ssh hosts key "known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} From f25a2a43eb3f16645a14dcc6dc81ca399aaefd25 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 15 Nov 2019 11:59:04 -0800 Subject: [PATCH 054/163] remove printfs --- authority/provisioner/sshpop.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index 9891f495..407a7a3a 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -3,7 +3,6 @@ package provisioner import ( "context" "encoding/base64" - "fmt" "strconv" "time" @@ -171,8 +170,6 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - fmt.Printf("claims.Audience = %+v\n", claims.Audience) - fmt.Printf("audiences = %+v\n", audiences) return nil, errors.New("invalid token: invalid audience claim (aud)") } From 8b2105a8f99965f1be44956c0612eb2176f9a924 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 15 Nov 2019 16:57:51 -0800 Subject: [PATCH 055/163] Instrument getIdentity func for OIDC ssh provisioner --- authority/authority.go | 3 +- authority/provisioner/jwk.go | 4 +- authority/provisioner/oidc.go | 27 +++++--- authority/provisioner/oidc_test.go | 46 +++++++++++-- authority/provisioner/provisioner.go | 78 +++++++++++++++++++---- authority/provisioner/provisioner_test.go | 49 ++++++++++++++ authority/provisioner/sign_ssh_options.go | 28 ++++---- authority/provisioner/x5c.go | 4 +- authority/provisioner/x5c_test.go | 4 +- 9 files changed, 198 insertions(+), 45 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 77c887a2..3177efd9 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -41,7 +41,7 @@ type Authority struct { initOnce bool // Custom functions sshBastionFunc func(user, hostname string) (*Bastion, error) - getIdentityFunc func(p provisioner.Interface, email string) (*provisioner.Identity, error) + getIdentityFunc provisioner.GetIdentityFunc } // New creates and initiates a new Authority type. @@ -192,6 +192,7 @@ func (a *Authority) init() error { UserKeys: sshKeys.UserKeys, HostKeys: sshKeys.HostKeys, }, + GetIdentityFunc: a.getIdentityFunc, } // Store all the provisioners for _, p := range a.config.AuthorityConfig.Provisioners { diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 05c079d7..c47960f9 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -197,10 +197,10 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Add modifiers from custom claims // FIXME: this is also set in the sign method using SSHOptions.Modify. if opts.CertType != "" { - signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) + signOptions = append(signOptions, sshCertTypeModifier(opts.CertType)) } if len(opts.Principals) > 0 { - signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals)) + signOptions = append(signOptions, sshCertPrincipalsModifier(opts.Principals)) } if !opts.ValidAfter.IsZero() { signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index d97f96f2..4538ef81 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -64,6 +64,7 @@ type OIDC struct { configuration openIDConfiguration keyStore *keyStore claimer *Claimer + getIdentityFunc GetIdentityFunc } // IsAdmin returns true if the given email is in the Admins whitelist, false @@ -169,6 +170,13 @@ func (o *OIDC) Init(config Config) (err error) { if err != nil { return err } + + // Set the identity getter if it exists, otherwise use the default. + if config.GetIdentityFunc == nil { + o.getIdentityFunc = DefaultIdentityFunc + } else { + o.getIdentityFunc = config.GetIdentityFunc + } return nil } @@ -326,23 +334,26 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption sshCertificateKeyIDModifier(claims.Email), } - name := SanitizeSSHUserPrincipal(claims.Email) - if !sshUserRegex.MatchString(name) { - return nil, errors.Errorf("invalid principal '%s' from email address '%s'", name, claims.Email) + // Get the identity using either the default identityFunc or one injected + // externally. + iden, err := o.getIdentityFunc(o, claims.Email) + if err != nil { + return nil, errors.Wrap(err, "authorizeSSHSign") } - - // Admin users will default to user + name but they can be changed by the - // user options. Non-admins are only able to sign user certificates. defaults := SSHOptions{ CertType: SSHUserCert, - Principals: []string{name}, + Principals: iden.Usernames, } + // Admin users can use any principal, and can sign user and host certificates. + // Non-admin users can only use principals returned by the identityFunc, and + // can only sign user certificates. if !o.IsAdmin(claims.Email) { signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) } - // Default to a user with name as principal if not set + // Default to a user certificate with usernames as principals if those options + // are not set. signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 8e0c823c..cbb7b2a2 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -347,6 +347,10 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { assert.FatalError(t, err) p3, err := generateOIDC() assert.FatalError(t, err) + p4, err := generateOIDC() + assert.FatalError(t, err) + p5, err := generateOIDC() + assert.FatalError(t, err) // Admin + Domains p3.Admins = []string{"name@smallstep.com", "root@example.com"} p3.Domains = []string{"smallstep.com"} @@ -356,12 +360,27 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" p2.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" + p4.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" + p5.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" assert.FatalError(t, p1.Init(config)) assert.FatalError(t, p2.Init(config)) assert.FatalError(t, p3.Init(config)) + assert.FatalError(t, p4.Init(config)) + assert.FatalError(t, p5.Init(config)) + + p4.getIdentityFunc = func(p Interface, email string) (*Identity, error) { + return &Identity{Usernames: []string{"max", "mariano"}}, nil + } + p5.getIdentityFunc = func(p Interface, email string) (*Identity, error) { + return nil, errors.New("force") + } t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0]) assert.FatalError(t, err) + okGetIdentityToken, err := generateSimpleToken("the-issuer", p4.ClientID, &keys.Keys[0]) + assert.FatalError(t, err) + failGetIdentityToken, err := generateSimpleToken("the-issuer", p5.ClientID, &keys.Keys[0]) + assert.FatalError(t, err) // Admin email not in domains okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{}, time.Now(), &keys.Keys[0]) assert.FatalError(t, err) @@ -384,11 +403,11 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { userDuration := p1.claimer.DefaultUserSSHCertDuration() hostDuration := p1.claimer.DefaultHostSSHCertDuration() expectedUserOptions := &SSHOptions{ - CertType: "user", Principals: []string{"name"}, + CertType: "user", Principals: []string{"name", "name@smallstep.com"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)), } expectedAdminOptions := &SSHOptions{ - CertType: "user", Principals: []string{"root"}, + CertType: "user", Principals: []string{"root", "root@example.com"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)), } expectedHostOptions := &SSHOptions{ @@ -412,17 +431,32 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { {"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false}, {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false}, {"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false}, - {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, false, false}, - {"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false}, + {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, + &SSHOptions{CertType: "user", Principals: []string{"name"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + {"ok-principals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{Principals: []string{"mariano"}}, pub}, + &SSHOptions{CertType: "user", Principals: []string{"mariano"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + {"ok-emptyPrincipals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{}, pub}, + &SSHOptions{CertType: "user", Principals: []string{"max", "mariano"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + {"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, + &SSHOptions{CertType: "user", Principals: []string{"name"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, {"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, false, false}, {"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, false, false}, - {"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}, pub}, expectedAdminOptions, false, false}, - {"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false}, + {"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}, pub}, + &SSHOptions{CertType: "user", Principals: []string{"root"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + {"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, + &SSHOptions{CertType: "user", Principals: []string{"name"}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, {"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true}, {"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, false, true}, {"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, false, true}, {"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, true, false}, + {"fail-getIdentity", p5, args{failGetIdentityToken, SSHOptions{}, pub}, nil, true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 8d0673a3..4b4200f5 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -185,6 +185,9 @@ type Config struct { DB db.AuthDB // SSHKeys are the root SSH public keys SSHKeys *SSHKeys + // GetIdentityFunc is a function that returns an identity that will be + // used by the provisioner to populate certificate attributes. + GetIdentityFunc GetIdentityFunc } type provisioner struct { @@ -314,7 +317,7 @@ func (b *base) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certif } // AuthorizeSSHRekey returns an unimplmented error. Provisioners should overwrite -// this method if they will support authorizing tokens for renewing SSH Certificates. +// this method if they will support authorizing tokens for rekeying SSH Certificates. func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { return nil, nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRekey") } @@ -325,6 +328,23 @@ type Identity struct { Usernames []string `json:"usernames"` } +// GetIdentityFunc is a function that returns an identity. +type GetIdentityFunc func(p Interface, email string) (*Identity, error) + +// DefaultIdentityFunc return a default identity depending on the provisioner type. +func DefaultIdentityFunc(p Interface, email string) (*Identity, error) { + switch k := p.(type) { + case *OIDC: + name := SanitizeSSHUserPrincipal(email) + if !sshUserRegex.MatchString(name) { + return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email) + } + return &Identity{Usernames: []string{name, email}}, nil + default: + return nil, errors.Errorf("provisioner type '%T' not supported by identity function", k) + } +} + // MockProvisioner for testing type MockProvisioner struct { Mret1, Mret2, Mret3 interface{} @@ -335,9 +355,13 @@ type MockProvisioner struct { MgetType func() Type MgetEncryptedKey func() (string, string, bool) Minit func(Config) error - MauthorizeRevoke func(ott string) error MauthorizeSign func(ctx context.Context, ott string) ([]SignOption, error) - MauthorizeRenewal func(*x509.Certificate) error + MauthorizeRenew func(ctx context.Context, cert *x509.Certificate) error + MauthorizeRevoke func(ctx context.Context, ott string) error + MauthorizeSSHSign func(ctx context.Context, ott string) ([]SignOption, error) + MauthorizeSSHRenew func(ctx context.Context, ott string) (*ssh.Certificate, error) + MauthorizeSSHRekey func(ctx context.Context, ott string) (*ssh.Certificate, []SignOption, error) + MauthorizeSSHRevoke func(ctx context.Context, ott string) error } // GetID mock @@ -391,26 +415,58 @@ func (m *MockProvisioner) Init(c Config) error { return m.Merr } +// AuthorizeSign mock +func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]SignOption, error) { + if m.MauthorizeSign != nil { + return m.MauthorizeSign(ctx, ott) + } + return m.Mret1.([]SignOption), m.Merr +} + // AuthorizeRevoke mock -func (m *MockProvisioner) AuthorizeRevoke(ott string) error { +func (m *MockProvisioner) AuthorizeRevoke(ctx context.Context, ott string) error { if m.MauthorizeRevoke != nil { - return m.MauthorizeRevoke(ott) + return m.MauthorizeRevoke(ctx, ott) } return m.Merr } -// AuthorizeSign mock -func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]SignOption, error) { +// AuthorizeRenew mock +func (m *MockProvisioner) AuthorizeRenew(ctx context.Context, c *x509.Certificate) error { + if m.MauthorizeRenew != nil { + return m.MauthorizeRenew(ctx, c) + } + return m.Merr +} + +// AuthorizeSSHSign mock +func (m *MockProvisioner) AuthorizeSSHSign(ctx context.Context, ott string) ([]SignOption, error) { if m.MauthorizeSign != nil { return m.MauthorizeSign(ctx, ott) } return m.Mret1.([]SignOption), m.Merr } -// AuthorizeRenewal mock -func (m *MockProvisioner) AuthorizeRenewal(c *x509.Certificate) error { - if m.MauthorizeRenewal != nil { - return m.MauthorizeRenewal(c) +// AuthorizeSSHRenew mock +func (m *MockProvisioner) AuthorizeSSHRenew(ctx context.Context, ott string) (*ssh.Certificate, error) { + if m.MauthorizeRenew != nil { + return m.MauthorizeSSHRenew(ctx, ott) + } + return m.Mret1.(*ssh.Certificate), m.Merr +} + +// AuthorizeSSHRekey mock +func (m *MockProvisioner) AuthorizeSSHRekey(ctx context.Context, ott string) (*ssh.Certificate, []SignOption, error) { + if m.MauthorizeSSHRekey != nil { + return m.MauthorizeSSHRekey(ctx, ott) + } + return m.Mret1.(*ssh.Certificate), m.Mret2.([]SignOption), m.Merr +} + +// AuthorizeSSHRevoke mock +func (m *MockProvisioner) AuthorizeSSHRevoke(ctx context.Context, ott string) error { + if m.MauthorizeSSHRevoke != nil { + return m.MauthorizeSSHRevoke(ctx, ott) } return m.Merr } diff --git a/authority/provisioner/provisioner_test.go b/authority/provisioner/provisioner_test.go index d79c2b69..14e62769 100644 --- a/authority/provisioner/provisioner_test.go +++ b/authority/provisioner/provisioner_test.go @@ -2,6 +2,9 @@ package provisioner import ( "testing" + + "github.com/pkg/errors" + "github.com/smallstep/assert" ) func TestType_String(t *testing.T) { @@ -52,3 +55,49 @@ func TestSanitizeSSHUserPrincipal(t *testing.T) { }) } } + +func TestDefaultIdentityFunc(t *testing.T) { + type test struct { + p Interface + email string + err error + identity *Identity + } + tests := map[string]func(*testing.T) test{ + "fail/unsupported-provisioner": func(t *testing.T) test { + return test{ + p: &X5C{}, + err: errors.New("provisioner type '*provisioner.X5C' not supported by identity function"), + } + }, + "fail/bad-ssh-regex": func(t *testing.T) test { + return test{ + p: &OIDC{}, + email: "$%^#_>@smallstep.com", + err: errors.New("invalid principal '______' from email '$%^#_>@smallstep.com'"), + } + }, + "ok": func(t *testing.T) test { + return test{ + p: &OIDC{}, + email: "max.furman@smallstep.com", + identity: &Identity{Usernames: []string{"maxfurman", "max.furman@smallstep.com"}}, + } + }, + } + for name, get := range tests { + t.Run(name, func(t *testing.T) { + tc := get(t) + identity, err := DefaultIdentityFunc(tc.p, tc.email) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.Equals(t, tc.err.Error(), err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, identity.Usernames, tc.identity.Usernames) + } + } + }) + } +} diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 06ddf697..ceb57105 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -107,6 +107,16 @@ func (o SSHOptions) match(got SSHOptions) error { return nil } +// sshCertPrincipalsModifier is an SSHCertificateModifier that sets the +// principals to the SSH certificate. +type sshCertPrincipalsModifier []string + +// Modify the ValidPrincipals value of the cert. +func (o sshCertPrincipalsModifier) Modify(cert *ssh.Certificate) error { + cert.ValidPrincipals = []string(o) + return nil +} + // sshCertificateKeyIDModifier is an SSHCertificateModifier that sets the given // Key ID in the SSH certificate. type sshCertificateKeyIDModifier string @@ -116,24 +126,16 @@ func (m sshCertificateKeyIDModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertificateCertTypeModifier is an SSHCertificateModifier that sets the -// certificate type to the SSH certificate. -type sshCertificateCertTypeModifier string +// sshCertTypeModifier is an SSHCertificateModifier that sets the +// certificate type. +type sshCertTypeModifier string -func (m sshCertificateCertTypeModifier) Modify(cert *ssh.Certificate) error { +// Modify sets the CertType for the ssh certificate. +func (m sshCertTypeModifier) Modify(cert *ssh.Certificate) error { cert.CertType = sshCertTypeUInt32(string(m)) return nil } -// sshCertificatePrincipalsModifier is an SSHCertificateModifier that sets the -// principals to the SSH certificate. -type sshCertificatePrincipalsModifier []string - -func (m sshCertificatePrincipalsModifier) Modify(cert *ssh.Certificate) error { - cert.ValidPrincipals = []string(m) - return nil -} - // sshCertificateValidAfterModifier is an SSHCertificateModifier that sets the // ValidAfter in the SSH certificate. type sshCertificateValidAfterModifier uint64 diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 4fa15a44..651cd136 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -237,10 +237,10 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Add modifiers from custom claims // FIXME: this is also set in the sign method using SSHOptions.Modify. if opts.CertType != "" { - signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) + signOptions = append(signOptions, sshCertTypeModifier(opts.CertType)) } if len(opts.Principals) > 0 { - signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals)) + signOptions = append(signOptions, sshCertPrincipalsModifier(opts.Principals)) } t := now() if !opts.ValidAfter.IsZero() { diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 4fc4dbe0..94018b55 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -636,9 +636,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH) case sshCertificateKeyIDModifier: assert.Equals(t, string(v), "foo") - case sshCertificateCertTypeModifier: + case sshCertTypeModifier: assert.Equals(t, string(v), tc.claims.Step.SSH.CertType) - case sshCertificatePrincipalsModifier: + case sshCertPrincipalsModifier: assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals) case sshCertificateValidAfterModifier: assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix()) From c407a9319b79ab68546056087a60bd5beb921107 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 20 Nov 2019 11:32:27 -0800 Subject: [PATCH 056/163] Add getSSHHosts injection func --- api/ssh.go | 15 +++++++++++++-- authority/authority.go | 1 + authority/options.go | 16 ++++++++++++---- authority/ssh.go | 13 +++++++------ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 7a2ba282..15c3c4b2 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -23,7 +23,7 @@ type SSHAuthority interface { GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) CheckSSHHost(principal string) (bool, error) - GetSSHHosts() ([]string, error) + GetSSHHosts(user string) ([]string, error) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) } @@ -406,7 +406,18 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { // SSHGetHosts is the HTTP handler that returns a list of valid ssh hosts. func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { - hosts, err := h.Authority.GetSSHHosts() + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + WriteError(w, BadRequest(errors.New("missing peer certificate"))) + return + } + + cert := r.TLS.PeerCertificates[0] + email := cert.EmailAddresses[0] + if len(email) == 0 { + WriteError(w, BadRequest(errors.New("client certificate missing email SAN"))) + return + } + hosts, err := h.Authority.GetSSHHosts(email) if err != nil { WriteError(w, InternalServerError(err)) return diff --git a/authority/authority.go b/authority/authority.go index 3177efd9..e00d978c 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -41,6 +41,7 @@ type Authority struct { initOnce bool // Custom functions sshBastionFunc func(user, hostname string) (*Bastion, error) + sshGetHostsFunc func(user string) ([]string, error) getIdentityFunc provisioner.GetIdentityFunc } diff --git a/authority/options.go b/authority/options.go index 409e8c2d..5a161118 100644 --- a/authority/options.go +++ b/authority/options.go @@ -16,6 +16,14 @@ func WithDatabase(db db.AuthDB) Option { } } +// 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) { + a.getIdentityFunc = fn + } +} + // 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 { @@ -24,10 +32,10 @@ func WithSSHBastionFunc(fn func(user, host string) (*Bastion, error)) Option { } } -// 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 { +// WithSSHGetHosts sets a custom function to get the bastion for a +// given user-host pair. +func WithSSHGetHosts(fn func(user string) ([]string, error)) Option { return func(a *Authority) { - a.getIdentityFunc = fn + a.sshGetHostsFunc = fn } } diff --git a/authority/ssh.go b/authority/ssh.go index 67c884b8..36806ae6 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -673,13 +673,14 @@ func (a *Authority) CheckSSHHost(principal string) (bool, error) { } // GetSSHHosts returns a list of valid host principals. -func (a *Authority) GetSSHHosts() ([]string, error) { - ps, err := a.db.GetSSHHostPrincipals() - if err != nil { - return nil, err +func (a *Authority) GetSSHHosts(email string) ([]string, error) { + if a.sshBastionFunc != nil { + return a.sshGetHostsFunc(email) + } + return nil, &apiError{ + err: errors.New("getSSHHosts is not configured"), + code: http.StatusNotFound, } - - return ps, nil } func (a *Authority) getAddUserPrincipal() (cmd string) { From 18f8d90cd74da1849ef4bda305d855af187c67c3 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 18 Nov 2019 17:07:23 -0800 Subject: [PATCH 057/163] Add support for identity authentication. --- ca/client.go | 62 +++++++++++++++++++++++++++++++++++++++++++-- ca/identity.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 ca/identity.go diff --git a/ca/client.go b/ca/client.go index a7cd0a7a..6c043ca7 100644 --- a/ca/client.go +++ b/ca/client.go @@ -27,6 +27,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/config" "github.com/smallstep/cli/crypto/x509util" + "golang.org/x/net/http2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -38,9 +39,13 @@ type clientOptions struct { rootSHA256 string rootFilename string rootBundle []byte + certificate tls.Certificate } func (o *clientOptions) apply(opts []ClientOption) (err error) { + if err = o.applyDefaultIdentity(); err != nil { + return + } for _, fn := range opts { if err = fn(o); err != nil { return @@ -49,6 +54,32 @@ func (o *clientOptions) apply(opts []ClientOption) (err error) { return } +// applyDefaultIdentity sets the options for the default identity if the +// identity file is present. The identity is enabled by default. +func (o *clientOptions) applyDefaultIdentity() error { + b, err := ioutil.ReadFile(IdentityFile) + if err != nil { + return nil + } + var identity Identity + if err := json.Unmarshal(b, &identity); err != nil { + return errors.Wrapf(err, "error unmarshaling %s", IdentityFile) + } + if err := identity.Validate(); err != nil { + return err + } + opts, err := identity.Options() + if err != nil { + return err + } + for _, fn := range opts { + if err := fn(o); err != nil { + return err + } + } + return nil +} + // checkTransport checks if other ways to set up a transport have been provided. // If they have it returns an error. func (o *clientOptions) checkTransport() error { @@ -85,10 +116,28 @@ func (o *clientOptions) getTransport(endpoint string) (tr http.RoundTripper, err if tr, err = getTransportFromFile(rootFile); err != nil { return nil, err } - return tr, nil } - return nil, errors.New("a transport, a root cert, or a root sha256 must be used") + if tr == nil { + return nil, errors.New("a transport, a root cert, or a root sha256 must be used") + } + } + + // Add client certificate if available + if o.certificate.Certificate != nil { + switch tr := tr.(type) { + case *http.Transport: + if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil { + tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate} + } + case *http2.Transport: + if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil { + tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate} + } + default: + return nil, errors.Errorf("unsupported transport type %T", tr) + } } + return tr, nil } @@ -141,6 +190,15 @@ func WithCABundle(bundle []byte) ClientOption { } } +// WithCertificate will set the given certificate as the TLS client certificate +// in the client. +func WithCertificate(crt tls.Certificate) ClientOption { + return func(o *clientOptions) error { + o.certificate = crt + return nil + } +} + func getTransportFromFile(filename string) (http.RoundTripper, error) { data, err := ioutil.ReadFile(filename) if err != nil { diff --git a/ca/identity.go b/ca/identity.go new file mode 100644 index 00000000..15f8358c --- /dev/null +++ b/ca/identity.go @@ -0,0 +1,69 @@ +package ca + +import ( + "crypto/tls" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/cli/config" +) + +// IdentityType represents the different types of identity files. +type IdentityType string + +// MutualTLS represents the identity using mTLS +const MutualTLS IdentityType = "mTLS" + +// IdentityFile contains the location of the identity file. +var IdentityFile = filepath.Join(config.StepPath(), "config", "identity.json") + +// Identity represents the identity file that can be used to authenticate with +// the CA. +type Identity struct { + Type string `json:"type"` + Certificate string `json:"crt"` + Key string `json:"key"` +} + +// Kind returns the type for the given identity. +func (i *Identity) Kind() IdentityType { + switch strings.ToLower(i.Type) { + case "mtls": + return MutualTLS + default: + return IdentityType(i.Type) + } +} + +// Validate validates the identity object. +func (i *Identity) Validate() error { + switch i.Kind() { + case MutualTLS: + if i.Certificate == "" { + return errors.New("identity.crt cannot be empty") + } + if i.Key == "" { + return errors.New("identity.key cannot be empty") + } + return nil + case "": + return errors.New("identity.type cannot be empty") + default: + return errors.Errorf("unsupported identity type %s", i.Type) + } +} + +// Options returns the ClientOptions used for the given identity. +func (i *Identity) Options() ([]ClientOption, error) { + switch i.Kind() { + case MutualTLS: + crt, err := tls.LoadX509KeyPair(i.Certificate, i.Key) + if err != nil { + return nil, errors.Wrap(err, "error creating identity certificate") + } + return []ClientOption{WithCertificate(crt)}, nil + default: + return nil, errors.Errorf("unsupported identity type %s", i.Type) + } +} From af6f6b4a85b1db336910d874ffece2b57d4cee4c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 11:50:46 -0800 Subject: [PATCH 058/163] Support for retry and identity files. --- ca/client.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++-- ca/identity.go | 93 ++++++++++++++++++++++++++++- 2 files changed, 244 insertions(+), 6 deletions(-) diff --git a/ca/client.go b/ca/client.go index 6c043ca7..51d21199 100644 --- a/ca/client.go +++ b/ca/client.go @@ -31,6 +31,10 @@ import ( "gopkg.in/square/go-jose.v2/jwt" ) +// RetryFunc defines the method used to retry a request. If it returns true, the +// request will be retried once. +type RetryFunc func(code int) bool + // ClientOption is the type of options passed to the Client constructor. type ClientOption func(o *clientOptions) error @@ -40,6 +44,7 @@ type clientOptions struct { rootFilename string rootBundle []byte certificate tls.Certificate + retryFunc RetryFunc } func (o *clientOptions) apply(opts []ClientOption) (err error) { @@ -199,6 +204,14 @@ func WithCertificate(crt tls.Certificate) ClientOption { } } +// WithRetryFunc defines a method used to retry a request. +func WithRetryFunc(fn RetryFunc) ClientOption { + return func(o *clientOptions) error { + o.retryFunc = fn + return nil + } +} + func getTransportFromFile(filename string) (http.RoundTripper, error) { data, err := ioutil.ReadFile(filename) if err != nil { @@ -330,8 +343,10 @@ func WithProvisionerLimit(limit int) ProvisionerOption { // Client implements an HTTP client for the CA server. type Client struct { - client *http.Client - endpoint *url.URL + client *http.Client + endpoint *url.URL + retryFunc RetryFunc + opts []ClientOption } // NewClient creates a new Client with the given endpoint and options. @@ -354,10 +369,31 @@ func NewClient(endpoint string, opts ...ClientOption) (*Client, error) { client: &http.Client{ Transport: tr, }, - endpoint: u, + endpoint: u, + retryFunc: o.retryFunc, + opts: opts, }, nil } +func (c *Client) retryOnError(r *http.Response) bool { + if c.retryFunc != nil { + if c.retryFunc(r.StatusCode) { + o := new(clientOptions) + if err := o.apply(c.opts); err != nil { + return false + } + tr, err := o.getTransport(c.endpoint.String()) + if err != nil { + return false + } + r.Body.Close() + c.client.Transport = tr + return true + } + } + return false +} + // SetTransport updates the transport of the internal HTTP client. func (c *Client) SetTransport(tr http.RoundTripper) { c.client.Transport = tr @@ -366,12 +402,18 @@ func (c *Client) SetTransport(tr http.RoundTripper) { // Health performs the health request to the CA and returns the // api.HealthResponse struct. func (c *Client) Health() (*api.HealthResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/health"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var health api.HealthResponse @@ -386,13 +428,19 @@ func (c *Client) Health() (*api.HealthResponse, error) { // resulting root certificate with the given SHA256, returning an error if they // do not match. func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) { + var retried bool sha256Sum = strings.ToLower(strings.Replace(sha256Sum, "-", "", -1)) u := c.endpoint.ResolveReference(&url.URL{Path: "/root/" + sha256Sum}) +retry: resp, err := getInsecureClient().Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var root api.RootResponse @@ -410,16 +458,22 @@ func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) { // Sign performs the sign request to the CA and returns the api.SignResponse // struct. func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/sign"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var sign api.SignResponse @@ -435,13 +489,19 @@ func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) { // Renew performs the renew request to the CA and returns the api.SignResponse // struct. func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/renew"}) client := &http.Client{Transport: tr} +retry: resp, err := client.Post(u.String(), "application/json", http.NoBody) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var sign api.SignResponse @@ -454,12 +514,13 @@ func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) { // Revoke performs the revoke request to the CA and returns the api.RevokeResponse // struct. func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } - var client *http.Client +retry: if tr != nil { client = &http.Client{Transport: tr} } else { @@ -472,6 +533,10 @@ func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.Revo return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var revoke api.RevokeResponse @@ -487,6 +552,7 @@ func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.Revo // ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to // paginate the provisioners. func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) { + var retried bool o := new(provisionerOptions) if err := o.apply(opts); err != nil { return nil, err @@ -495,11 +561,16 @@ func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersRespo Path: "/provisioners", RawQuery: o.rawQuery(), }) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var provisioners api.ProvisionersResponse @@ -513,12 +584,18 @@ func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersRespo // the given provisioner kid and returns the api.ProvisionerKeyResponse struct // with the encrypted key. func (c *Client) ProvisionerKey(kid string) (*api.ProvisionerKeyResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/provisioners/" + kid + "/encrypted-key"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var key api.ProvisionerKeyResponse @@ -531,12 +608,18 @@ func (c *Client) ProvisionerKey(kid string) (*api.ProvisionerKeyResponse, error) // Roots performs the get roots request to the CA and returns the // api.RootsResponse struct. func (c *Client) Roots() (*api.RootsResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/roots"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var roots api.RootsResponse @@ -549,12 +632,18 @@ func (c *Client) Roots() (*api.RootsResponse, error) { // Federation performs the get federation request to the CA and returns the // api.FederationResponse struct. func (c *Client) Federation() (*api.FederationResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/federation"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var federation api.FederationResponse @@ -567,16 +656,22 @@ func (c *Client) Federation() (*api.FederationResponse, error) { // SSHSign performs the POST /ssh/sign request to the CA and returns the // api.SSHSignResponse struct. func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/sign"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var sign api.SSHSignResponse @@ -589,16 +684,22 @@ func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) // SSHRenew performs the POST /ssh/renew request to the CA and returns the // api.SSHRenewResponse struct. func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/renew"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var renew api.SSHRenewResponse @@ -611,16 +712,22 @@ func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, erro // SSHRekey performs the POST /ssh/rekey request to the CA and returns the // api.SSHRekeyResponse struct. func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/rekey"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var rekey api.SSHRekeyResponse @@ -633,16 +740,22 @@ func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, erro // SSHRevoke performs the POST /ssh/revoke request to the CA and returns the // api.SSHRevokeResponse struct. func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/revoke"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var revoke api.SSHRevokeResponse @@ -655,12 +768,18 @@ func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, e // SSHRoots performs the GET /ssh/roots request to the CA and returns the // api.SSHRootsResponse struct. func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/roots"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var keys api.SSHRootsResponse @@ -673,12 +792,18 @@ func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) { // SSHFederation performs the get /ssh/federation request to the CA and returns // the api.SSHRootsResponse struct. func (c *Client) SSHFederation() (*api.SSHRootsResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var keys api.SSHRootsResponse @@ -691,16 +816,22 @@ func (c *Client) SSHFederation() (*api.SSHRootsResponse, error) { // SSHConfig performs the POST /ssh/config request to the CA to get the ssh // configuration templates. func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/config"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var config api.SSHConfigResponse @@ -713,6 +844,7 @@ func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, e // SSHCheckHost performs the POST /ssh/check-host request to the CA with the // given principal. func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, error) { + var retried bool body, err := json.Marshal(&api.SSHCheckPrincipalRequest{ Type: provisioner.SSHHostCert, Principal: principal, @@ -721,11 +853,16 @@ func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var check api.SSHCheckPrincipalResponse @@ -737,12 +874,18 @@ func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, // SSHGetHosts performs the GET /ssh/get-hosts request to the CA. func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) { + var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/get-hosts"}) +retry: resp, err := c.client.Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var hosts api.SSHGetHostsResponse @@ -754,16 +897,22 @@ func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) { // SSHBastion performs the POST /ssh/bastion request to the CA. func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) { + var retried bool body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/bastion"}) +retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { return nil, errors.Wrapf(err, "client POST %s failed", u) } if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } return nil, readError(resp.Body) } var bastion api.SSHBastionResponse diff --git a/ca/identity.go b/ca/identity.go index 15f8358c..5576dde3 100644 --- a/ca/identity.go +++ b/ca/identity.go @@ -1,17 +1,30 @@ package ca import ( + "bytes" + "crypto" "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "io/ioutil" + "os" "path/filepath" "strings" + "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/api" "github.com/smallstep/cli/config" + "github.com/smallstep/cli/crypto/pemutil" ) // IdentityType represents the different types of identity files. type IdentityType string +// Disabled represents a disabled identity type +const Disabled IdentityType = "" + // MutualTLS represents the identity using mTLS const MutualTLS IdentityType = "mTLS" @@ -26,9 +39,73 @@ type Identity struct { Key string `json:"key"` } +// WriteDefaultIdentity writes the given certificates and key and the +// identity.json pointing to the new files. +func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error { + base := filepath.Join(config.StepPath(), "config") + if err := os.MkdirAll(base, 0600); err != nil { + return errors.Wrap(err, "error creating config directory") + } + + base = filepath.Join(config.StepPath(), "identity") + if err := os.MkdirAll(base, 0600); err != nil { + return errors.Wrap(err, "error creating identity directory") + } + + certFilename := filepath.Join(base, "identity.crt") + keyFilename := filepath.Join(base, "identity_key") + + // Write certificate + buf := new(bytes.Buffer) + for _, crt := range certChain { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + } + if err := pem.Encode(buf, block); err != nil { + return errors.Wrap(err, "error encoding identity certificate") + } + } + if err := ioutil.WriteFile(certFilename, buf.Bytes(), 0600); err != nil { + return errors.Wrap(err, "error writing identity certificate") + } + + // Write key + buf.Reset() + block, err := pemutil.Serialize(key) + if err != nil { + return err + } + if err := pem.Encode(buf, block); err != nil { + return errors.Wrap(err, "error encoding identity key") + } + if err := ioutil.WriteFile(keyFilename, buf.Bytes(), 0600); err != nil { + return errors.Wrap(err, "error writing identity certificate") + } + + // Write identity.json + buf.Reset() + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + if err := enc.Encode(Identity{ + Type: string(MutualTLS), + Certificate: certFilename, + Key: keyFilename, + }); err != nil { + return errors.Wrap(err, "error writing identity json") + } + if err := ioutil.WriteFile(IdentityFile, buf.Bytes(), 0600); err != nil { + return errors.Wrap(err, "error writing identity certificate") + } + + return nil +} + // Kind returns the type for the given identity. func (i *Identity) Kind() IdentityType { switch strings.ToLower(i.Type) { + case "": + return Disabled case "mtls": return MutualTLS default: @@ -39,6 +116,8 @@ func (i *Identity) Kind() IdentityType { // Validate validates the identity object. func (i *Identity) Validate() error { switch i.Kind() { + case Disabled: + return nil case MutualTLS: if i.Certificate == "" { return errors.New("identity.crt cannot be empty") @@ -47,8 +126,6 @@ func (i *Identity) Validate() error { return errors.New("identity.key cannot be empty") } return nil - case "": - return errors.New("identity.type cannot be empty") default: return errors.Errorf("unsupported identity type %s", i.Type) } @@ -57,11 +134,23 @@ func (i *Identity) Validate() error { // Options returns the ClientOptions used for the given identity. func (i *Identity) Options() ([]ClientOption, error) { switch i.Kind() { + case Disabled: + return nil, nil case MutualTLS: crt, err := tls.LoadX509KeyPair(i.Certificate, i.Key) if err != nil { return nil, errors.Wrap(err, "error creating identity certificate") } + // Check if certificate is expired. + // Do not return any options if expired. + x509Cert, err := x509.ParseCertificate(crt.Certificate[0]) + if err != nil { + return nil, errors.Wrap(err, "error creating identity certificate") + } + now := time.Now() + if now.Before(x509Cert.NotBefore) || now.After(x509Cert.NotAfter) { + return nil, nil + } return []ClientOption{WithCertificate(crt)}, nil default: return nil, errors.Errorf("unsupported identity type %s", i.Type) From 3fda081e42c5d640fe3ce04454bb4dfd764d7321 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 11:51:25 -0800 Subject: [PATCH 059/163] Add identity certificate in ssh response. --- api/ssh.go | 60 +++++++++++++++++++++++++++++++----------- authority/authorize.go | 40 +++++++++++++++++++--------- authority/config.go | 4 ++- authority/ssh.go | 8 +++--- 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 15c3c4b2..43b24d52 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -29,14 +29,15 @@ type SSHAuthority interface { // SSHSignRequest is the request body of an SSH certificate request. type SSHSignRequest struct { - PublicKey []byte `json:"publicKey"` //base64 encoded - OTT string `json:"ott"` - CertType string `json:"certType,omitempty"` - Principals []string `json:"principals,omitempty"` - ValidAfter TimeDuration `json:"validAfter,omitempty"` - ValidBefore TimeDuration `json:"validBefore,omitempty"` - AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` - KeyID string `json:"keyID"` + PublicKey []byte `json:"publicKey"` // base64 encoded + OTT string `json:"ott"` + CertType string `json:"certType,omitempty"` + Principals []string `json:"principals,omitempty"` + ValidAfter TimeDuration `json:"validAfter,omitempty"` + ValidBefore TimeDuration `json:"validBefore,omitempty"` + AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` + KeyID string `json:"keyID"` + IdentityCSR CertificateRequest `json:"identityCSR,omitempty"` } // Validate validates the SSHSignRequest. @@ -49,14 +50,21 @@ func (s *SSHSignRequest) Validate() error { case len(s.OTT) == 0: return errors.New("missing or empty ott") default: + // Validate identity signature if provided + if s.IdentityCSR.CertificateRequest != nil { + if err := s.IdentityCSR.CertificateRequest.CheckSignature(); err != nil { + return errors.Wrap(err, "invalid csr") + } + } return nil } } // SSHSignResponse is the response object that returns the SSH certificate. type SSHSignResponse struct { - Certificate SSHCertificate `json:"crt"` - AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` + Certificate SSHCertificate `json:"crt"` + AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` + IdentityCertificate []Certificate `json:"identityCrt,omitempty"` } // SSHRootsResponse represents the response object that returns the SSH user and @@ -292,11 +300,33 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { addUserCertificate = &SSHCertificate{addUserCert} } - w.WriteHeader(http.StatusCreated) - JSON(w, &SSHSignResponse{ - Certificate: SSHCertificate{cert}, - AddUserCertificate: addUserCertificate, - }) + // Sign identity certificate if available. + var identityCertificate []Certificate + if cr := body.IdentityCSR.CertificateRequest; cr != nil { + opts := provisioner.Options{ + NotBefore: body.ValidAfter, + NotAfter: body.ValidBefore, + } + ctx := authority.NewContextWithSkipTokenReuse(context.Background()) + ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) + signOpts, err := h.Authority.Authorize(ctx, body.OTT) + if err != nil { + WriteError(w, Unauthorized(err)) + return + } + certChain, err := h.Authority.Sign(cr, opts, signOpts...) + if err != nil { + WriteError(w, Forbidden(err)) + return + } + identityCertificate = certChainToPEM(certChain) + } + + JSONStatus(w, &SSHSignResponse{ + Certificate: SSHCertificate{cert}, + AddUserCertificate: addUserCertificate, + IdentityCertificate: identityCertificate, + }, http.StatusCreated) } // SSHRoots is an HTTP handler that returns the SSH public keys for user and host diff --git a/authority/authorize.go b/authority/authorize.go index 18eba6b9..db2b2414 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -19,10 +19,24 @@ type Claims struct { Nonce string `json:"nonce,omitempty"` } +type skipTokenReuseKey struct{} + +// NewContextWithSkipTokenReuse creates a new context from ctx and attaches a +// value to skip the token reuse. +func NewContextWithSkipTokenReuse(ctx context.Context) context.Context { + return context.WithValue(ctx, skipTokenReuseKey{}, true) +} + +// SkipTokenReuseFromContext returns if the token reuse needs to be ignored. +func SkipTokenReuseFromContext(ctx context.Context) bool { + m, _ := ctx.Value(skipTokenReuseKey{}).(bool) + return m +} + // authorizeToken parses the token and returns the provisioner used to generate // the token. This method enforces the One-Time use policy (tokens can only be // used once). -func (a *Authority) authorizeToken(ott string) (provisioner.Interface, error) { +func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner.Interface, error) { var errContext = map[string]interface{}{"ott": ott} // Validate payload @@ -58,15 +72,17 @@ func (a *Authority) authorizeToken(ott string) (provisioner.Interface, error) { http.StatusUnauthorized, errContext} } - // Store the token to protect against reuse. - if reuseKey, err := p.GetTokenID(ott); err == nil { - ok, err := a.db.UseToken(reuseKey, ott) - if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when checking if token already used"), - http.StatusInternalServerError, errContext} - } - if !ok { - return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext} + // Store the token to protect against reuse unless it's skipped. + if !SkipTokenReuseFromContext(ctx) { + if reuseKey, err := p.GetTokenID(ott); err == nil { + ok, err := a.db.UseToken(reuseKey, ott) + if err != nil { + return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when checking if token already used"), + http.StatusInternalServerError, errContext} + } + if !ok { + return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext} + } } } @@ -116,7 +132,7 @@ func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.Si // list of methods to apply to the signing flow. func (a *Authority) authorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) { var errContext = apiCtx{"ott": ott} - p, err := a.authorizeToken(ott) + p, err := a.authorizeToken(ctx, ott) if err != nil { return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext} } @@ -143,7 +159,7 @@ func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} } diff --git a/authority/config.go b/authority/config.go index 0fd1e5c9..462a764b 100644 --- a/authority/config.go +++ b/authority/config.go @@ -198,7 +198,9 @@ func (c *Config) getAudiences() provisioner.Audiences { for _, name := range c.DNSNames { audiences.Sign = append(audiences.Sign, fmt.Sprintf("https://%s/1.0/sign", name), - fmt.Sprintf("https://%s/sign", name)) + fmt.Sprintf("https://%s/sign", name), + fmt.Sprintf("https://%s/1.0/ssh/sign", name), + fmt.Sprintf("https://%s/ssh/sign", name)) audiences.Revoke = append(audiences.Revoke, fmt.Sprintf("https://%s/1.0/revoke", name), fmt.Sprintf("https://%s/revoke", name)) diff --git a/authority/ssh.go b/authority/ssh.go index 36806ae6..4f34d81a 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -190,7 +190,7 @@ func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error // list of methods to apply to the signing flow. func (a *Authority) authorizeSSHSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) { var errContext = apiCtx{"ott": ott} - p, err := a.authorizeToken(ott) + p, err := a.authorizeToken(ctx, ott) if err != nil { return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext} } @@ -325,7 +325,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return nil, &apiError{ err: errors.Wrap(err, "authorizeSSHRenew"), @@ -435,7 +435,7 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return nil, nil, &apiError{ err: errors.Wrap(err, "authorizeSSHRenew"), @@ -567,7 +567,7 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext} } From 35912cc90620fb2dd5f0b40b9fc1a7e73bd84217 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 20 Nov 2019 12:59:48 -0800 Subject: [PATCH 060/163] change func def for getSSHHosts * continue to return all hosts if injection method not specified --- api/ssh.go | 17 ++++++----------- authority/authority.go | 2 +- authority/options.go | 4 +++- authority/ssh.go | 17 +++++++++++------ 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 43b24d52..cec2dcb7 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -2,6 +2,7 @@ package api import ( "context" + "crypto/x509" "encoding/base64" "encoding/json" "net/http" @@ -23,7 +24,7 @@ type SSHAuthority interface { GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) CheckSSHHost(principal string) (bool, error) - GetSSHHosts(user string) ([]string, error) + GetSSHHosts(cert *x509.Certificate) ([]string, error) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) } @@ -436,18 +437,12 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { // SSHGetHosts is the HTTP handler that returns a list of valid ssh hosts. func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { - if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { - WriteError(w, BadRequest(errors.New("missing peer certificate"))) - return + var cert *x509.Certificate + if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { + cert = r.TLS.PeerCertificates[0] } - cert := r.TLS.PeerCertificates[0] - email := cert.EmailAddresses[0] - if len(email) == 0 { - WriteError(w, BadRequest(errors.New("client certificate missing email SAN"))) - return - } - hosts, err := h.Authority.GetSSHHosts(email) + hosts, err := h.Authority.GetSSHHosts(cert) if err != nil { WriteError(w, InternalServerError(err)) return diff --git a/authority/authority.go b/authority/authority.go index e00d978c..44fe3fc7 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -41,7 +41,7 @@ type Authority struct { initOnce bool // Custom functions sshBastionFunc func(user, hostname string) (*Bastion, error) - sshGetHostsFunc func(user string) ([]string, error) + sshGetHostsFunc func(cert *x509.Certificate) ([]string, error) getIdentityFunc provisioner.GetIdentityFunc } diff --git a/authority/options.go b/authority/options.go index 5a161118..f1738e68 100644 --- a/authority/options.go +++ b/authority/options.go @@ -1,6 +1,8 @@ package authority import ( + "crypto/x509" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" ) @@ -34,7 +36,7 @@ func WithSSHBastionFunc(fn func(user, host string) (*Bastion, error)) Option { // WithSSHGetHosts sets a custom function to get the bastion for a // given user-host pair. -func WithSSHGetHosts(fn func(user string) ([]string, error)) Option { +func WithSSHGetHosts(fn func(cert *x509.Certificate) ([]string, error)) Option { return func(a *Authority) { a.sshGetHostsFunc = fn } diff --git a/authority/ssh.go b/authority/ssh.go index 4f34d81a..779a6da9 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -3,6 +3,7 @@ package authority import ( "context" "crypto/rand" + "crypto/x509" "encoding/binary" "net/http" "strings" @@ -673,14 +674,18 @@ func (a *Authority) CheckSSHHost(principal string) (bool, error) { } // GetSSHHosts returns a list of valid host principals. -func (a *Authority) GetSSHHosts(email string) ([]string, error) { - if a.sshBastionFunc != nil { - return a.sshGetHostsFunc(email) +func (a *Authority) GetSSHHosts(cert *x509.Certificate) ([]string, error) { + if a.sshGetHostsFunc != nil { + return a.sshGetHostsFunc(cert) } - return nil, &apiError{ - err: errors.New("getSSHHosts is not configured"), - code: http.StatusNotFound, + hosts, err := a.db.GetSSHHostPrincipals() + if err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "getSSHHosts"), + code: http.StatusInternalServerError, + } } + return hosts, nil } func (a *Authority) getAddUserPrincipal() (cmd string) { From 80ee3645c452678b2bcdb1b86c5363fd7a1e3926 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 16:03:31 -0800 Subject: [PATCH 061/163] Fix directory permissions. --- ca/client.go | 19 +++++++++++++++++ ca/identity.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/ca/client.go b/ca/client.go index 51d21199..eefc4612 100644 --- a/ca/client.go +++ b/ca/client.go @@ -394,6 +394,25 @@ func (c *Client) retryOnError(r *http.Response) bool { return false } +// GetRootCAs returns the RootCAs certificate pool from the configured +// transport. +func (c *Client) GetRootCAs() *x509.CertPool { + switch t := c.client.Transport.(type) { + case *http.Transport: + if t.TLSClientConfig != nil { + return t.TLSClientConfig.RootCAs + } + return nil + case *http2.Transport: + if t.TLSClientConfig != nil { + return t.TLSClientConfig.RootCAs + } + return nil + default: + return nil + } +} + // SetTransport updates the transport of the internal HTTP client. func (c *Client) SetTransport(tr http.RoundTripper) { c.client.Transport = tr diff --git a/ca/identity.go b/ca/identity.go index 5576dde3..f7f37049 100644 --- a/ca/identity.go +++ b/ca/identity.go @@ -8,6 +8,7 @@ import ( "encoding/json" "encoding/pem" "io/ioutil" + "net/http" "os" "path/filepath" "strings" @@ -39,16 +40,29 @@ type Identity struct { Key string `json:"key"` } +// LoadDefaultIdentity loads the default identity. +func LoadDefaultIdentity() (*Identity, error) { + b, err := ioutil.ReadFile(IdentityFile) + if err != nil { + return nil, errors.Wrap(err, "error reading identity json") + } + identity := new(Identity) + if err := json.Unmarshal(b, &identity); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling %s", IdentityFile) + } + return identity, nil +} + // WriteDefaultIdentity writes the given certificates and key and the // identity.json pointing to the new files. func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error { base := filepath.Join(config.StepPath(), "config") - if err := os.MkdirAll(base, 0600); err != nil { + if err := os.MkdirAll(base, 0700); err != nil { return errors.Wrap(err, "error creating config directory") } base = filepath.Join(config.StepPath(), "identity") - if err := os.MkdirAll(base, 0600); err != nil { + if err := os.MkdirAll(base, 0700); err != nil { return errors.Wrap(err, "error creating identity directory") } @@ -156,3 +170,43 @@ func (i *Identity) Options() ([]ClientOption, error) { return nil, errors.Errorf("unsupported identity type %s", i.Type) } } + +// Renew renews the identity certificate using the given client. +func (i *Identity) Renew(client *Client) error { + switch i.Kind() { + case Disabled: + return nil + case MutualTLS: + cert, err := tls.LoadX509KeyPair(i.Certificate, i.Key) + if err != nil { + return errors.Wrap(err, "error creating identity certificate") + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: client.GetRootCAs(), + PreferServerCipherSuites: true, + }, + } + resp, err := client.Renew(tr) + if err != nil { + return err + } + buf := new(bytes.Buffer) + for _, crt := range resp.CertChainPEM { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + } + if err := pem.Encode(buf, block); err != nil { + return errors.Wrap(err, "error encoding identity certificate") + } + } + if err := ioutil.WriteFile(i.Certificate, buf.Bytes(), 0600); err != nil { + return errors.Wrap(err, "error writing identity certificate") + } + return nil + default: + return errors.Errorf("unsupported identity type %s", i.Type) + } +} From 2f18a26d4f98e04c8eeb2e456b2eac48a5732e7c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 17:01:31 -0800 Subject: [PATCH 062/163] Add version endpoint. --- api/api.go | 46 ++++++++++++++++++++++++++++++++++++++++++ authority/authority.go | 3 +-- cmd/step-ca/main.go | 2 ++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 334def24..1fb0230a 100644 --- a/api/api.go +++ b/api/api.go @@ -42,6 +42,7 @@ type Authority interface { GetEncryptedKey(kid string) (string, error) GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) + Version() authority.Version } // TimeDuration is an alias of provisioner.TimeDuration @@ -71,6 +72,13 @@ func NewCertificate(cr *x509.Certificate) Certificate { } } +// reset sets the inner x509.CertificateRequest to nil +func (c *Certificate) reset() { + if c != nil { + c.Certificate = nil + } +} + // MarshalJSON implements the json.Marshaler interface. The certificate is // quoted string using the PEM encoding. func (c Certificate) MarshalJSON() ([]byte, error) { @@ -91,6 +99,13 @@ func (c *Certificate) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &s); err != nil { return errors.Wrap(err, "error decoding certificate") } + + // Make sure the inner x509.Certificate is nil + if s == "null" || s == "" { + c.reset() + return nil + } + block, _ := pem.Decode([]byte(s)) if block == nil { return errors.New("error decoding certificate") @@ -117,6 +132,13 @@ func NewCertificateRequest(cr *x509.CertificateRequest) CertificateRequest { } } +// reset sets the inner x509.CertificateRequest to nil +func (c *CertificateRequest) reset() { + if c != nil { + c.CertificateRequest = nil + } +} + // MarshalJSON implements the json.Marshaler interface. The certificate request // is a quoted string using the PEM encoding. func (c CertificateRequest) MarshalJSON() ([]byte, error) { @@ -137,6 +159,13 @@ func (c *CertificateRequest) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &s); err != nil { return errors.Wrap(err, "error decoding csr") } + + // Make sure the inner x509.CertificateRequest is nil + if s == "null" || s == "" { + c.reset() + return nil + } + block, _ := pem.Decode([]byte(s)) if block == nil { return errors.New("error decoding csr") @@ -162,6 +191,13 @@ type RouterHandler interface { Route(r Router) } +// VersionResponse is the response object that returns the version of the +// server. +type VersionResponse struct { + Version string `json:"version"` + RequireClientAuthentication bool `json:"requireClientAuthentication,omitempty"` +} + // HealthResponse is the response object that returns the health of the server. type HealthResponse struct { Status string `json:"status"` @@ -241,6 +277,7 @@ func New(authority Authority) RouterHandler { } func (h *caHandler) Route(r Router) { + r.MethodFunc("GET", "/version", h.Version) r.MethodFunc("GET", "/health", h.Health) r.MethodFunc("GET", "/root/{sha}", h.Root) r.MethodFunc("POST", "/sign", h.Sign) @@ -268,6 +305,15 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/sign-ssh", h.SSHSign) } +// Version is an HTTP handler that returns the version of the server. +func (h *caHandler) Version(w http.ResponseWriter, r *http.Request) { + v := h.Authority.Version() + JSON(w, VersionResponse{ + Version: v.Version, + RequireClientAuthentication: v.RequireClientAuthentication, + }) +} + // Health is an HTTP handler that returns the status of the server. func (h *caHandler) Health(w http.ResponseWriter, r *http.Request) { JSON(w, HealthResponse{Status: "ok"}) diff --git a/authority/authority.go b/authority/authority.go index 44fe3fc7..9faf3348 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,11 +8,10 @@ import ( "sync" "time" - "github.com/smallstep/certificates/templates" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/x509util" "golang.org/x/crypto/ssh" diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 3e47d52f..d3ed089c 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -13,6 +13,7 @@ import ( "strconv" "time" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/commands" "github.com/smallstep/cli/command" "github.com/smallstep/cli/command/version" @@ -29,6 +30,7 @@ var ( func init() { config.Set("Smallstep CA", Version, BuildTime) + authority.GlobalVersion.Version = Version rand.Seed(time.Now().UnixNano()) } From 7a06a60f8866be06851d592ad567fd690c69f5d4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 17:02:06 -0800 Subject: [PATCH 063/163] Add missing version.go file. --- authority/version.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 authority/version.go diff --git a/authority/version.go b/authority/version.go new file mode 100644 index 00000000..41e1be80 --- /dev/null +++ b/authority/version.go @@ -0,0 +1,17 @@ +package authority + +// GlobalVersion stores the version information of the server. +var GlobalVersion = Version{ + Version: "0.0.0", +} + +// Version defines the +type Version struct { + Version string + RequireClientAuthentication bool +} + +// Version returns the version information of the server. +func (a *Authority) Version() Version { + return GlobalVersion +} From 012f64cdadee0ae08dd8b41de87f5fa952fa3069 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 17:15:48 -0800 Subject: [PATCH 064/163] Add version support to the ca.Client. --- ca/client.go | 24 +++++++++++++++++++++ ca/client_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/ca/client.go b/ca/client.go index eefc4612..6c288ee2 100644 --- a/ca/client.go +++ b/ca/client.go @@ -418,6 +418,30 @@ func (c *Client) SetTransport(tr http.RoundTripper) { c.client.Transport = tr } +// Version performs the version request to the CA and returns the +// api.VersionResponse struct. +func (c *Client) Version() (*api.VersionResponse, error) { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: "/version"}) +retry: + resp, err := c.client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readError(resp.Body) + } + var version api.VersionResponse + if err := readJSON(resp.Body, &version); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &version, nil +} + // Health performs the health request to the CA and returns the // api.HealthResponse struct. func (c *Client) Health() (*api.HealthResponse, error) { diff --git a/ca/client_test.go b/ca/client_test.go index f9a968c0..7a36a64b 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -150,6 +150,61 @@ func equalJSON(t *testing.T, a interface{}, b interface{}) bool { return bytes.Equal(ab, bb) } +func TestClient_Version(t *testing.T) { + ok := &api.VersionResponse{Version: "test"} + internal := api.InternalServerError(fmt.Errorf("Internal Server Error")) + notFound := api.NotFound(fmt.Errorf("Not Found")) + + tests := []struct { + name string + response interface{} + responseCode int + wantErr bool + }{ + {"ok", ok, 200, false}, + {"500", internal, 500, true}, + {"404", notFound, 404, true}, + } + + srv := httptest.NewServer(nil) + defer srv.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport)) + if err != nil { + t.Errorf("NewClient() error = %v", err) + return + } + + srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + api.JSONStatus(w, tt.response, tt.responseCode) + }) + + got, err := c.Version() + if (err != nil) != tt.wantErr { + fmt.Printf("%+v", err) + t.Errorf("Client.Version() error = %v, wantErr %v", err, tt.wantErr) + return + } + + switch { + case err != nil: + if got != nil { + t.Errorf("Client.Version() = %v, want nil", got) + } + if !reflect.DeepEqual(err, tt.response) { + t.Errorf("Client.Version() error = %v, want %v", err, tt.response) + } + default: + if !reflect.DeepEqual(got, tt.response) { + t.Errorf("Client.Version() = %v, want %v", got, tt.response) + } + } + }) + } +} + func TestClient_Health(t *testing.T) { ok := &api.HealthResponse{Status: "ok"} nok := api.InternalServerError(fmt.Errorf("Internal Server Error")) From 927784237d2a043dda0f5c6d78faf95443c2cbaf Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 20 Nov 2019 17:23:51 -0800 Subject: [PATCH 065/163] Use an actual Hosts type when returning ssh hosts --- api/ssh.go | 5 +++-- authority/authority.go | 3 ++- authority/options.go | 3 ++- authority/ssh.go | 10 ++++++++-- sshutil/types.go | 14 ++++++++++++++ 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 sshutil/types.go diff --git a/api/ssh.go b/api/ssh.go index cec2dcb7..0bc2c35a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "golang.org/x/crypto/ssh" ) @@ -24,7 +25,7 @@ type SSHAuthority interface { GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) CheckSSHHost(principal string) (bool, error) - GetSSHHosts(cert *x509.Certificate) ([]string, error) + GetSSHHosts(cert *x509.Certificate) ([]sshutil.Host, error) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) } @@ -83,7 +84,7 @@ type SSHCertificate struct { // SSHGetHostsResponse is the response object that returns the list of valid // hosts for SSH. type SSHGetHostsResponse struct { - Hosts []string `json:"hosts"` + Hosts []sshutil.Host `json:"hosts"` } // MarshalJSON implements the json.Marshaler interface. Returns a quoted, diff --git a/authority/authority.go b/authority/authority.go index 44fe3fc7..3f525001 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "github.com/pkg/errors" @@ -41,7 +42,7 @@ type Authority struct { initOnce bool // Custom functions sshBastionFunc func(user, hostname string) (*Bastion, error) - sshGetHostsFunc func(cert *x509.Certificate) ([]string, error) + sshGetHostsFunc func(cert *x509.Certificate) ([]sshutil.Host, error) getIdentityFunc provisioner.GetIdentityFunc } diff --git a/authority/options.go b/authority/options.go index f1738e68..a2e19edb 100644 --- a/authority/options.go +++ b/authority/options.go @@ -5,6 +5,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/sshutil" ) // Option sets options to the Authority. @@ -36,7 +37,7 @@ func WithSSHBastionFunc(fn func(user, host string) (*Bastion, error)) Option { // WithSSHGetHosts sets a custom function to get the bastion for a // given user-host pair. -func WithSSHGetHosts(fn func(cert *x509.Certificate) ([]string, error)) Option { +func WithSSHGetHosts(fn func(cert *x509.Certificate) ([]sshutil.Host, error)) Option { return func(a *Authority) { a.sshGetHostsFunc = fn } diff --git a/authority/ssh.go b/authority/ssh.go index 779a6da9..232527a8 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/jose" @@ -674,17 +675,22 @@ func (a *Authority) CheckSSHHost(principal string) (bool, error) { } // GetSSHHosts returns a list of valid host principals. -func (a *Authority) GetSSHHosts(cert *x509.Certificate) ([]string, error) { +func (a *Authority) GetSSHHosts(cert *x509.Certificate) ([]sshutil.Host, error) { if a.sshGetHostsFunc != nil { return a.sshGetHostsFunc(cert) } - hosts, err := a.db.GetSSHHostPrincipals() + hostnames, err := a.db.GetSSHHostPrincipals() if err != nil { return nil, &apiError{ err: errors.Wrap(err, "getSSHHosts"), code: http.StatusInternalServerError, } } + + hosts := make([]sshutil.Host, len(hostnames)) + for i, hn := range hostnames { + hosts[i] = sshutil.Host{Hostname: hn} + } return hosts, nil } diff --git a/sshutil/types.go b/sshutil/types.go new file mode 100644 index 00000000..322b9fb5 --- /dev/null +++ b/sshutil/types.go @@ -0,0 +1,14 @@ +package sshutil + +// HostGroup defines expected attributes for a host group that a host might belong to. +type HostGroup struct { + ID string + Name string +} + +// Host defines expected attributes for an ssh host. +type Host struct { + HostID string `json:"hid"` + HostGroups []HostGroup `json:"host_groups"` + Hostname string `json:"hostname"` +} From 87ce2c9b4b6cb62d9c2354c67ed4972daf51dacd Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 19:11:54 -0800 Subject: [PATCH 066/163] Add Identity helpers. --- ca/client.go | 39 +++++++++++++++++++++++++++++++++++++++ ca/identity.go | 15 +++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/ca/client.go b/ca/client.go index 6c288ee2..bf26e4c5 100644 --- a/ca/client.go +++ b/ca/client.go @@ -26,6 +26,7 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/cli/config" + "github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/x509util" "golang.org/x/net/http2" "gopkg.in/square/go-jose.v2/jwt" @@ -1033,6 +1034,44 @@ func CreateSignRequest(ott string) (*api.SignRequest, crypto.PrivateKey, error) }, pk, nil } +// CreateCertificateRequest creates a new CSR with the given common name and +// SANs. If no san is provided the commonName will set also a SAN. +func CreateCertificateRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) { + key, err := keys.GenerateDefaultKey() + if err != nil { + return nil, nil, err + } + return createCertificateRequest(commonName, sans, key) +} + +func createCertificateRequest(commonName string, sans []string, key crypto.PrivateKey) (*api.CertificateRequest, crypto.PrivateKey, error) { + if len(sans) == 0 { + sans = []string{commonName} + } + dnsNames, ips, emails := x509util.SplitSANs(sans) + template := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: commonName, + }, + DNSNames: dnsNames, + IPAddresses: ips, + EmailAddresses: emails, + } + csr, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, nil, err + } + cr, err := x509.ParseCertificateRequest(csr) + if err != nil { + return nil, nil, err + } + if err := cr.CheckSignature(); err != nil { + return nil, nil, err + } + + return &api.CertificateRequest{CertificateRequest: cr}, key, nil +} + func getInsecureClient() *http.Client { return &http.Client{ Transport: &http.Transport{ diff --git a/ca/identity.go b/ca/identity.go index f7f37049..1d3699c6 100644 --- a/ca/identity.go +++ b/ca/identity.go @@ -40,6 +40,21 @@ type Identity struct { Key string `json:"key"` } +// NewIdentityRequest returns a new CSR to create the identity. If an identity +// was already present it reuses the private key. +func NewIdentityRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) { + var identityKey crypto.PrivateKey + if i, err := LoadDefaultIdentity(); err == nil && i.Key != "" { + if k, err := pemutil.Read(i.Key); err == nil { + identityKey = k + } + } + if identityKey == nil { + return CreateCertificateRequest(commonName, sans...) + } + return createCertificateRequest(commonName, sans, identityKey) +} + // LoadDefaultIdentity loads the default identity. func LoadDefaultIdentity() (*Identity, error) { b, err := ioutil.ReadFile(IdentityFile) From 864dd3cf1fab06901a23c98c2b34fe4eed68b662 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 21 Nov 2019 19:06:19 -0800 Subject: [PATCH 067/163] Fix extra write header. --- api/sshRekey.go | 5 ++--- api/sshRenew.go | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/api/sshRekey.go b/api/sshRekey.go index 530b9df3..234a6df5 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -71,8 +71,7 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusCreated) - JSON(w, &SSHSignResponse{ + JSONStatus(w, &SSHSignResponse{ Certificate: SSHCertificate{newCert}, - }) + }, http.StatusCreated) } diff --git a/api/sshRenew.go b/api/sshRenew.go index 3aea01bb..4324ebba 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -61,8 +61,7 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusCreated) - JSON(w, &SSHSignResponse{ + JSONStatus(w, &SSHSignResponse{ Certificate: SSHCertificate{newCert}, - }) + }, http.StatusCreated) } From e88034bea4026c96985dc8520bb345c086d5bf4f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 25 Nov 2019 19:59:53 -0800 Subject: [PATCH 068/163] Update templates. --- pki/templates.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pki/templates.go b/pki/templates.go index 8cf62674..b78d61e4 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -27,25 +27,39 @@ var SSHTemplates = &templates.SSHTemplates{ // SSHTemplateData contains the data of the default templates used on ssh. var SSHTemplateData = map[string]string{ - // include.tpl adds the step ssh config file + // include.tpl adds the step ssh config file. + // + // Note: on windows `Include C:\...` is treated as a relative path. "include.tpl": `Host * - Include {{.User.StepPath}}/ssh/config`, +{{- if eq .User.GOOS "windows" }} + Include {{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config +{{- else }} + Include {{.User.StepPath}}/ssh/config +{{- end }}`, - // config.tpl is the step ssh config file, it includes the Match rule - // and references the step known_hosts file + // config.tpl is the step ssh config file, it includes the Match rule and + // references the step known_hosts file. + // + // Note: on windows ProxyCommand requires the full path "config.tpl": `Match exec "step ssh check-host %h" ForwardAgent yes - {{- if .User.User }} +{{- if .User.User }} User {{.User.User}} - {{- end }} +{{- end }} +{{- if eq .User.GOOS "windows" }} + UserKnownHostsFile {{.User.StepPath}}\ssh\known_hosts + ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p +{{- else }} UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts - ProxyCommand step ssh proxycommand %r %h %p`, + ProxyCommand step ssh proxycommand %r %h %p +{{- end }} +`, // known_hosts.tpl authorizes the ssh hosts key "known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}} {{- range .Step.SSH.HostFederatedKeys}} @cert-authority * {{.Type}} {{.Marshal | toString | b64enc}} -{{- end}} +{{- end }} `, // sshd_config.tpl adds the configuration to support certificates @@ -57,7 +71,7 @@ HostKey /etc/ssh/{{.User.Key}}`, "ca.tpl": `{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}} {{- range .Step.SSH.UserFederatedKeys}} {{.Type}} {{.Marshal | toString | b64enc}} -{{- end}} +{{- end }} `, } From 02dd66e2a91a0cb9437cf1ee6ea37dfd754dd616 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 26 Nov 2019 11:55:18 -0500 Subject: [PATCH 069/163] extraneous new line --- db/db.go | 1 - 1 file changed, 1 deletion(-) diff --git a/db/db.go b/db/db.go index 7535185b..8753cc1a 100644 --- a/db/db.go +++ b/db/db.go @@ -238,7 +238,6 @@ func (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error { return errors.Wrap(err, "database Update error") } return nil - } // GetSSHHostPrincipals gets a list of all valid host principals. From bf3b557bad381d354afd54e1556e95db4d58a003 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 17:45:32 -0800 Subject: [PATCH 070/163] Make identity duration the same as the SSH cert. --- api/ssh.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 0bc2c35a..e3fff0b3 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "net/http" + "time" "github.com/pkg/errors" "github.com/smallstep/certificates/authority" @@ -306,8 +307,8 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { var identityCertificate []Certificate if cr := body.IdentityCSR.CertificateRequest; cr != nil { opts := provisioner.Options{ - NotBefore: body.ValidAfter, - NotAfter: body.ValidBefore, + NotBefore: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidAfter), 0)), + NotAfter: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidBefore), 0)), } ctx := authority.NewContextWithSkipTokenReuse(context.Background()) ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) From 052897169300adea0f4268f349fc5528ecba17b6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:15:34 -0800 Subject: [PATCH 071/163] Fix api tests. --- api/api_test.go | 123 +++++++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 53 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index e68eb7db..98d612ab 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -29,6 +29,7 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/jose" @@ -207,19 +208,21 @@ func TestCertificate_MarshalJSON(t *testing.T) { func TestCertificate_UnmarshalJSON(t *testing.T) { tests := []struct { - name string - data []byte - wantErr bool + name string + data []byte + wantCert bool + wantErr bool }{ - {"no data", nil, true}, - {"empty string", []byte(`""`), true}, - {"incomplete string 1", []byte(`"foobar`), true}, {"incomplete string 2", []byte(`foobar"`), true}, - {"invalid string", []byte(`"foobar"`), true}, - {"invalid bytes 0", []byte{}, true}, {"invalid bytes 1", []byte{1}, true}, - {"empty csr", []byte(`"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----\n"`), true}, - {"invalid type", []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"`), true}, - {"valid root", []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `"`), false}, - {"valid cert", []byte(`"` + strings.Replace(certPEM, "\n", `\n`, -1) + `"`), false}, + {"no data", nil, false, true}, + {"incomplete string 1", []byte(`"foobar`), false, true}, {"incomplete string 2", []byte(`foobar"`), false, true}, + {"invalid string", []byte(`"foobar"`), false, true}, + {"invalid bytes 0", []byte{}, false, true}, {"invalid bytes 1", []byte{1}, false, true}, + {"empty csr", []byte(`"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----\n"`), false, true}, + {"invalid type", []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"`), false, true}, + {"empty string", []byte(`""`), false, false}, + {"json null", []byte(`null`), false, false}, + {"valid root", []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `"`), true, false}, + {"valid cert", []byte(`"` + strings.Replace(certPEM, "\n", `\n`, -1) + `"`), true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -227,7 +230,7 @@ func TestCertificate_UnmarshalJSON(t *testing.T) { if err := c.UnmarshalJSON(tt.data); (err != nil) != tt.wantErr { t.Errorf("Certificate.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } - if !tt.wantErr && c.Certificate == nil { + if tt.wantCert && c.Certificate == nil { t.Error("Certificate.UnmarshalJSON() failed, Certificate is nil") } }) @@ -236,16 +239,18 @@ func TestCertificate_UnmarshalJSON(t *testing.T) { func TestCertificate_UnmarshalJSON_json(t *testing.T) { tests := []struct { - name string - data string - wantErr bool + name string + data string + wantCert bool + wantErr bool }{ - {"invalid type (null)", `{"crt":null}`, true}, - {"invalid type (bool)", `{"crt":true}`, true}, - {"invalid type (number)", `{"crt":123}`, true}, - {"invalid type (object)", `{"crt":{}}`, true}, - {"empty crt", `{"crt":"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----\n"}`, true}, - {"valid crt", `{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `"}`, false}, + {"invalid type (bool)", `{"crt":true}`, false, true}, + {"invalid type (number)", `{"crt":123}`, false, true}, + {"invalid type (object)", `{"crt":{}}`, false, true}, + {"empty crt (null)", `{"crt":null}`, false, false}, + {"empty crt (string)", `{"crt":""}`, false, false}, + {"empty crt", `{"crt":"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----\n"}`, false, true}, + {"valid crt", `{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `"}`, true, false}, } type request struct { @@ -259,12 +264,12 @@ func TestCertificate_UnmarshalJSON_json(t *testing.T) { t.Errorf("json.Unmarshal() error = %v, wantErr %v", err, tt.wantErr) } - switch tt.wantErr { - case false: + switch tt.wantCert { + case true: if body.Cert.Certificate == nil { t.Error("json.Unmarshal() failed, Certificate is nil") } - case true: + case false: if body.Cert.Certificate != nil { t.Error("json.Unmarshal() failed, Certificate is not nil") } @@ -313,18 +318,20 @@ func TestCertificateRequest_MarshalJSON(t *testing.T) { func TestCertificateRequest_UnmarshalJSON(t *testing.T) { tests := []struct { - name string - data []byte - wantErr bool + name string + data []byte + wantCert bool + wantErr bool }{ - {"no data", nil, true}, - {"empty string", []byte(`""`), true}, - {"incomplete string 1", []byte(`"foobar`), true}, {"incomplete string 2", []byte(`foobar"`), true}, - {"invalid string", []byte(`"foobar"`), true}, - {"invalid bytes 0", []byte{}, true}, {"invalid bytes 1", []byte{1}, true}, - {"empty csr", []byte(`"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST----\n"`), true}, - {"invalid type", []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `"`), true}, - {"valid csr", []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"`), false}, + {"no data", nil, false, true}, + {"incomplete string 1", []byte(`"foobar`), false, true}, {"incomplete string 2", []byte(`foobar"`), false, true}, + {"invalid string", []byte(`"foobar"`), false, true}, + {"invalid bytes 0", []byte{}, false, true}, {"invalid bytes 1", []byte{1}, false, true}, + {"empty csr", []byte(`"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST----\n"`), false, true}, + {"invalid type", []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `"`), false, true}, + {"empty string", []byte(`""`), false, false}, + {"json null", []byte(`null`), false, false}, + {"valid csr", []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"`), true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -332,7 +339,7 @@ func TestCertificateRequest_UnmarshalJSON(t *testing.T) { if err := c.UnmarshalJSON(tt.data); (err != nil) != tt.wantErr { t.Errorf("CertificateRequest.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } - if !tt.wantErr && c.CertificateRequest == nil { + if tt.wantCert && c.CertificateRequest == nil { t.Error("CertificateRequest.UnmarshalJSON() failed, CertificateRequet is nil") } }) @@ -341,16 +348,18 @@ func TestCertificateRequest_UnmarshalJSON(t *testing.T) { func TestCertificateRequest_UnmarshalJSON_json(t *testing.T) { tests := []struct { - name string - data string - wantErr bool + name string + data string + wantCert bool + wantErr bool }{ - {"invalid type (null)", `{"csr":null}`, true}, - {"invalid type (bool)", `{"csr":true}`, true}, - {"invalid type (number)", `{"csr":123}`, true}, - {"invalid type (object)", `{"csr":{}}`, true}, - {"empty csr", `{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST----\n"}`, true}, - {"valid csr", `{"csr":"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"}`, false}, + {"invalid type (bool)", `{"csr":true}`, false, true}, + {"invalid type (number)", `{"csr":123}`, false, true}, + {"invalid type (object)", `{"csr":{}}`, false, true}, + {"empty csr (null)", `{"csr":null}`, false, false}, + {"empty csr (string)", `{"csr":""}`, false, false}, + {"empty csr", `{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST----\n"}`, false, true}, + {"valid csr", `{"csr":"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"}`, true, false}, } type request struct { @@ -364,12 +373,12 @@ func TestCertificateRequest_UnmarshalJSON_json(t *testing.T) { t.Errorf("json.Unmarshal() error = %v, wantErr %v", err, tt.wantErr) } - switch tt.wantErr { - case false: + switch tt.wantCert { + case true: if body.CSR.CertificateRequest == nil { t.Error("json.Unmarshal() failed, CertificateRequest is nil") } - case true: + case false: if body.CSR.CertificateRequest != nil { t.Error("json.Unmarshal() failed, CertificateRequest is not nil") } @@ -552,12 +561,13 @@ type mockAuthority struct { getFederation func() ([]*x509.Certificate, error) renewSSH func(cert *ssh.Certificate) (*ssh.Certificate, error) rekeySSH func(cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - getSSHHosts func() ([]string, error) + getSSHHosts func(*x509.Certificate) ([]sshutil.Host, error) getSSHRoots func() (*authority.SSHKeys, error) getSSHFederation func() (*authority.SSHKeys, error) getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) checkSSHHost func(principal string) (bool, error) getSSHBastion func(user string, hostname string) (*authority.Bastion, error) + version func() authority.Version } // TODO: remove once Authorize is deprecated. @@ -677,11 +687,11 @@ func (m *mockAuthority) RekeySSH(cert *ssh.Certificate, key ssh.PublicKey, signO return m.ret1.(*ssh.Certificate), m.err } -func (m *mockAuthority) GetSSHHosts() ([]string, error) { +func (m *mockAuthority) GetSSHHosts(cert *x509.Certificate) ([]sshutil.Host, error) { if m.getSSHHosts != nil { - return m.getSSHHosts() + return m.getSSHHosts(cert) } - return m.ret1.([]string), m.err + return m.ret1.([]sshutil.Host), m.err } func (m *mockAuthority) GetSSHRoots() (*authority.SSHKeys, error) { @@ -719,6 +729,13 @@ func (m *mockAuthority) GetSSHBastion(user string, hostname string) (*authority. return m.ret1.(*authority.Bastion), m.err } +func (m *mockAuthority) Version() authority.Version { + if m.version != nil { + return m.version() + } + return m.ret1.(authority.Version) +} + func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority From 5d355864025510617b9b5ba3ba09e558e3a966d8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:44:43 -0800 Subject: [PATCH 072/163] Update template tests. --- authority/ssh_test.go | 2 +- authority/testdata/templates/config.tpl | 11 ++++++++++- authority/testdata/templates/include.tpl | 6 +++++- templates/templates_test.go | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/authority/ssh_test.go b/authority/ssh_test.go index c2f4ceb7..3ea4e98d 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -407,7 +407,7 @@ func TestAuthority_GetSSHConfig(t *testing.T) { } userOutputWithUserData := []templates.Output{ {Name: "include.tpl", Type: templates.File, Comment: "#", Path: "ssh/include", Content: []byte("Host *\n\tInclude /home/user/.step/ssh/config")}, - {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("Match exec \"step ssh check-host %h\"\n\tForwardAgent yes\n\tUserKnownHostsFile /home/user/.step/ssh/known_hosts")}, + {Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("Match exec \"step ssh check-host %h\"\n\tForwardAgent yes\n\tUserKnownHostsFile /home/user/.step/ssh/known_hosts\n\tProxyCommand step ssh proxycommand %r %h %p\n")}, } hostOutputWithUserData := []templates.Output{ {Name: "sshd_config.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/sshd_config", Content: []byte("TrustedUserCAKeys /etc/ssh/ca.pub\nHostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub\nHostKey /etc/ssh/ssh_host_ecdsa_key")}, diff --git a/authority/testdata/templates/config.tpl b/authority/testdata/templates/config.tpl index 96233680..63269a4c 100644 --- a/authority/testdata/templates/config.tpl +++ b/authority/testdata/templates/config.tpl @@ -1,3 +1,12 @@ Match exec "step ssh check-host %h" ForwardAgent yes - UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts \ No newline at end of file +{{- if .User.User }} + User {{.User.User}} +{{- end }} +{{- if or .User.GOOS "none" | eq "windows" }} + UserKnownHostsFile {{.User.StepPath}}\ssh\known_hosts + ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p +{{- else }} + UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts + ProxyCommand step ssh proxycommand %r %h %p +{{- end }} diff --git a/authority/testdata/templates/include.tpl b/authority/testdata/templates/include.tpl index e7113727..5d21dd45 100644 --- a/authority/testdata/templates/include.tpl +++ b/authority/testdata/templates/include.tpl @@ -1,2 +1,6 @@ Host * - Include {{.User.StepPath}}/ssh/config \ No newline at end of file +{{- if or .User.GOOS "linux" | eq "windows" }} + Include {{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config +{{- else }} + Include {{.User.StepPath}}/ssh/config +{{- end }} \ No newline at end of file diff --git a/templates/templates_test.go b/templates/templates_test.go index 537fab4c..c3d252ad 100644 --- a/templates/templates_test.go +++ b/templates/templates_test.go @@ -230,6 +230,8 @@ func TestTemplate_Render(t *testing.T) { }, "User": map[string]string{ "StepPath": "/tmp/.step", + "User": "john", + "GOOS": "linux", }, } @@ -271,7 +273,7 @@ func TestTemplate_Render(t *testing.T) { return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Template.Render() = %v, want %v", got, tt.want) + t.Errorf("Template.Render() = %v, want %v", string(got), string(tt.want)) } }) } From b29dd3ad0c906809d8397904bcaec233876fb53c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:47:10 -0800 Subject: [PATCH 073/163] Update templates. --- pki/templates.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pki/templates.go b/pki/templates.go index b78d61e4..e6c1655b 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -31,7 +31,7 @@ var SSHTemplateData = map[string]string{ // // Note: on windows `Include C:\...` is treated as a relative path. "include.tpl": `Host * -{{- if eq .User.GOOS "windows" }} +{{- if or .User.GOOS "none" | eq "windows" }} Include {{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config {{- else }} Include {{.User.StepPath}}/ssh/config @@ -46,7 +46,7 @@ var SSHTemplateData = map[string]string{ {{- if .User.User }} User {{.User.User}} {{- end }} -{{- if eq .User.GOOS "windows" }} +{{- if or .User.GOOS "none" | eq "windows" }} UserKnownHostsFile {{.User.StepPath}}\ssh\known_hosts ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p {{- else }} From 92d1db1616028ba97ff8529b19a28f82ae5f7dcf Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:47:34 -0800 Subject: [PATCH 074/163] Make test compilable. --- authority/authorize_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/authorize_test.go b/authority/authorize_test.go index fa14caa0..5e112e95 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -146,7 +146,7 @@ func TestAuthority_authorizeToken(t *testing.T) { } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) - _, err = _a.authorizeToken(raw) + _, err = _a.authorizeToken(context.TODO(), raw) assert.FatalError(t, err) return &authorizeTest{ auth: _a, @@ -234,7 +234,7 @@ func TestAuthority_authorizeToken(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - p, err := tc.auth.authorizeToken(tc.ott) + p, err := tc.auth.authorizeToken(context.TODO(), tc.ott) if err != nil { if assert.NotNil(t, tc.err) { switch v := err.(type) { From b97aeedb78900647c0a4fd1a4f2819f1cb9625f9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:48:28 -0800 Subject: [PATCH 075/163] Fix tests. --- ca/ca_test.go | 6 ++++++ ca/client.go | 10 ++++++++++ ca/identity.go | 3 +++ 3 files changed, 19 insertions(+) diff --git a/ca/ca_test.go b/ca/ca_test.go index cbbd6d48..ef00132c 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -12,6 +12,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -50,6 +51,11 @@ func getCSR(priv interface{}) (*x509.CertificateRequest, error) { return x509.ParseCertificateRequest(csrBytes) } +func TestMain(m *testing.M) { + DisableIdentity = true + os.Exit(m.Run()) +} + func TestCASign(t *testing.T) { pub, priv, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) diff --git a/ca/client.go b/ca/client.go index bf26e4c5..21b52025 100644 --- a/ca/client.go +++ b/ca/client.go @@ -63,6 +63,10 @@ func (o *clientOptions) apply(opts []ClientOption) (err error) { // applyDefaultIdentity sets the options for the default identity if the // identity file is present. The identity is enabled by default. func (o *clientOptions) applyDefaultIdentity() error { + if DisableIdentity { + return nil + } + b, err := ioutil.ReadFile(IdentityFile) if err != nil { return nil @@ -132,10 +136,16 @@ func (o *clientOptions) getTransport(endpoint string) (tr http.RoundTripper, err if o.certificate.Certificate != nil { switch tr := tr.(type) { case *http.Transport: + if tr.TLSClientConfig == nil { + tr.TLSClientConfig = &tls.Config{} + } if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil { tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate} } case *http2.Transport: + if tr.TLSClientConfig == nil { + tr.TLSClientConfig = &tls.Config{} + } if len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil { tr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate} } diff --git a/ca/identity.go b/ca/identity.go index 1d3699c6..fea77d35 100644 --- a/ca/identity.go +++ b/ca/identity.go @@ -23,6 +23,9 @@ import ( // IdentityType represents the different types of identity files. type IdentityType string +// DisableIdentity is a global variable to disable the identity. +var DisableIdentity bool = false + // Disabled represents a disabled identity type const Disabled IdentityType = "" From f99d1007bc4f5bc0b789d46ea51f57b9bbb73976 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:53:16 -0800 Subject: [PATCH 076/163] Update cli dependency. --- go.mod | 12 +++++++----- go.sum | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 449237b9..807f8bfc 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,18 @@ go 1.13 require ( github.com/Masterminds/sprig/v3 v3.0.0 - github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible - github.com/newrelic/go-agent v1.11.0 + github.com/go-chi/chi v4.0.2+incompatible + github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 - github.com/sirupsen/logrus v1.1.1 + github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 + github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 github.com/smallstep/nosql v0.1.1 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a - golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 gopkg.in/square/go-jose.v2 v2.4.0 ) + +// replace github.com/smallstep/cli => ../cli diff --git a/go.sum b/go.sum index ce311f89..70517090 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,19 @@ 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/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= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk= github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= +github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -15,7 +21,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/corpix/uarand v0.0.0-20170903190822-2b8494104d86/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M= +github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,6 +33,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= @@ -37,6 +47,7 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 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= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -55,18 +66,29 @@ 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/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= +github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -80,40 +102,56 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1: github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go.mod h1:/WOAB2LkcjkEbKG5rDol+A22Lp3UsttkLPLkY7tVtuk= github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go.mod h1:043iBnsMvNhQ+QFwSh0N6JR3H2yamHPPAc78vCf+8Tc= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191126035953-e88034bea402/go.mod h1:r2UTcAZNriKlwvNNXymNAcF3iKL6mTYOYrOCtBYYGJU= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= +github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= +github.com/smallstep/cli v0.13.3/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3KzjZKH3ojxLQolUnHSXuomfFjm+/KbIdpY= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 h1:kHHsScwMUDlepa7LkxR55r6NT9ra+U9KsP6qJGZb5jM= github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03/go.mod h1:dklnISxr+GzUmurBngEF9Jvj0aI9KK5uVgZwOdFniNs= +github.com/smallstep/cli v0.14.0-rc.1.0.20191127003637-bf0d6274e86f h1:NOch88Di/v87Mlg+l3JUgkfYzOb/aOIpHV5O7B7bZmA= +github.com/smallstep/cli v0.14.0-rc.1.0.20191127003637-bf0d6274e86f/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= +github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 h1:yAB5yZI+iqulxALQoAPv6CNBBYTLeGYcU9ZbwBiD9Es= +github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= github.com/smallstep/nosql v0.1.1/go.mod h1:qyxCqeyGwkuM6bfJSY3sg+aiXEiD0GbQOPzIF8/ZD8Q= github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= +github.com/smallstep/zcrypto v0.0.0-20191122194514-76530dff70e7/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.10.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -123,10 +161,15 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= From 1a94c0df94a7dfca64b18754e045aa20050d50b4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 19:09:01 -0800 Subject: [PATCH 077/163] Use default duration for host certificates identity files. --- api/ssh.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index e3fff0b3..6382a27d 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -306,9 +306,13 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { // Sign identity certificate if available. var identityCertificate []Certificate if cr := body.IdentityCSR.CertificateRequest; cr != nil { - opts := provisioner.Options{ - NotBefore: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidAfter), 0)), - NotAfter: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidBefore), 0)), + var opts provisioner.Options + // Use same duration as ssh certificate for user certificates + if body.CertType == provisioner.SSHUserCert { + opts = provisioner.Options{ + NotBefore: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidAfter), 0)), + NotAfter: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidBefore), 0)), + } } ctx := authority.NewContextWithSkipTokenReuse(context.Background()) ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) From 967d1137269ab3315bda02a32345a9e6751dc2a0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 12:25:40 -0800 Subject: [PATCH 078/163] Add error marshaling tests. --- api/errors_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 api/errors_test.go diff --git a/api/errors_test.go b/api/errors_test.go new file mode 100644 index 00000000..a252e4c3 --- /dev/null +++ b/api/errors_test.go @@ -0,0 +1,65 @@ +package api + +import ( + "fmt" + "reflect" + "testing" +) + +func TestError_MarshalJSON(t *testing.T) { + type fields struct { + Status int + Err error + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + {"ok", fields{400, fmt.Errorf("bad request")}, []byte(`{"status":400,"message":"Bad Request"}`), false}, + {"ok no error", fields{500, nil}, []byte(`{"status":500,"message":"Internal Server Error"}`), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Error{ + Status: tt.fields.Status, + Err: tt.fields.Err, + } + got, err := e.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("Error.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Error.MarshalJSON() = %s, want %s", got, tt.want) + } + }) + } +} + +func TestError_UnmarshalJSON(t *testing.T) { + type args struct { + data []byte + } + tests := []struct { + name string + args args + expected *Error + wantErr bool + }{ + {"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &Error{Status: 400, Err: fmt.Errorf("bad request")}, false}, + {"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &Error{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := new(Error) + if err := e.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr { + t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(tt.expected, e) { + t.Errorf("Error.UnmarshalJSON() wants = %v, got %v", tt.expected, e) + } + }) + } +} From c5e34f777c6695899f2d347615b3a296d84c5f2a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 14:27:23 -0800 Subject: [PATCH 079/163] Replace /ssh/get-hosts to /ssh/hosts --- api/api.go | 3 ++- ca/client.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 1fb0230a..0c16168f 100644 --- a/api/api.go +++ b/api/api.go @@ -297,12 +297,13 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost) - r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts) + r.MethodFunc("GET", "/ssh/hosts", h.SSHGetHosts) r.MethodFunc("POST", "/ssh/bastion", h.SSHBastion) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) r.MethodFunc("POST", "/sign-ssh", h.SSHSign) + r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts) } // Version is an HTTP handler that returns the version of the server. diff --git a/ca/client.go b/ca/client.go index 21b52025..2a8e9ca8 100644 --- a/ca/client.go +++ b/ca/client.go @@ -929,7 +929,7 @@ retry: // SSHGetHosts performs the GET /ssh/get-hosts request to the CA. func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) { var retried bool - u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/get-hosts"}) + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/hosts"}) retry: resp, err := c.client.Get(u.String()) if err != nil { From a049e1f7e786cf77201954cdcc421ed08956d7d9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 14:48:14 -0800 Subject: [PATCH 080/163] Check at the cert type instead of at the body. --- api/ssh.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 6382a27d..b559c27a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -56,7 +56,7 @@ func (s *SSHSignRequest) Validate() error { // Validate identity signature if provided if s.IdentityCSR.CertificateRequest != nil { if err := s.IdentityCSR.CertificateRequest.CheckSignature(); err != nil { - return errors.Wrap(err, "invalid csr") + return errors.Wrap(err, "invalid identityCSR") } } return nil @@ -308,7 +308,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { if cr := body.IdentityCSR.CertificateRequest; cr != nil { var opts provisioner.Options // Use same duration as ssh certificate for user certificates - if body.CertType == provisioner.SSHUserCert { + if cert.CertType == ssh.UserCert { opts = provisioner.Options{ NotBefore: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidAfter), 0)), NotAfter: provisioner.NewTimeDuration(time.Unix(int64(cert.ValidBefore), 0)), From 15a222d354051bada6222abda99c6dfeeffd630e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 14:48:34 -0800 Subject: [PATCH 081/163] Add missing unit tests for ssh. --- api/ssh_test.go | 133 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 26 deletions(-) diff --git a/api/ssh_test.go b/api/ssh_test.go index cc615ee7..b5ff7002 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" "encoding/base64" "encoding/json" "fmt" @@ -20,6 +21,7 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "golang.org/x/crypto/ssh" ) @@ -197,6 +199,10 @@ func TestSSHCertificate_UnmarshalJSON(t *testing.T) { } func TestSignSSHRequest_Validate(t *testing.T) { + csr := parseCertificateRequest(csrPEM) + badCSR := parseCertificateRequest(csrPEM) + badCSR.SignatureAlgorithm = x509.SHA1WithRSA + type fields struct { PublicKey []byte OTT string @@ -205,19 +211,24 @@ func TestSignSSHRequest_Validate(t *testing.T) { ValidAfter TimeDuration ValidBefore TimeDuration AddUserPublicKey []byte + KeyID string + IdentityCSR CertificateRequest } tests := []struct { name string fields fields wantErr bool }{ - {"ok-empty", fields{[]byte("Zm9v"), "ott", "", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, false}, - {"ok-user", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, false}, - {"ok-host", fields{[]byte("Zm9v"), "ott", "host", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, false}, - {"key", fields{nil, "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, true}, - {"key", fields{[]byte(""), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, true}, - {"type", fields{[]byte("Zm9v"), "ott", "foo", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, true}, - {"ott", fields{[]byte("Zm9v"), "", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil}, true}, + {"ok-empty", fields{[]byte("Zm9v"), "ott", "", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, false}, + {"ok-user", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, false}, + {"ok-host", fields{[]byte("Zm9v"), "ott", "host", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, false}, + {"ok-keyID", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "key-id", CertificateRequest{}}, false}, + {"ok-identityCSR", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "key-id", CertificateRequest{CertificateRequest: csr}}, false}, + {"key", fields{nil, "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true}, + {"key", fields{[]byte(""), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true}, + {"type", fields{[]byte("Zm9v"), "ott", "foo", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true}, + {"ott", fields{[]byte("Zm9v"), "", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true}, + {"identityCSR", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "key-id", CertificateRequest{CertificateRequest: badCSR}}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -229,6 +240,8 @@ func TestSignSSHRequest_Validate(t *testing.T) { ValidAfter: tt.fields.ValidAfter, ValidBefore: tt.fields.ValidBefore, AddUserPublicKey: tt.fields.AddUserPublicKey, + KeyID: tt.fields.KeyID, + IdentityCSR: tt.fields.IdentityCSR, } if err := s.Validate(); (err != nil) != tt.wantErr { t.Errorf("SignSSHRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) @@ -262,28 +275,42 @@ func Test_caHandler_SSHSign(t *testing.T) { AddUserPublicKey: user.Key.Marshal(), }) assert.FatalError(t, err) + userIdentityReq, err := json.Marshal(SSHSignRequest{ + PublicKey: user.Key.Marshal(), + OTT: "ott", + IdentityCSR: CertificateRequest{parseCertificateRequest(csrPEM)}, + }) + assert.FatalError(t, err) + identityCerts := []*x509.Certificate{ + parseCertificate(certPEM), + } + identityCertsPEM := []byte(`"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n"`) tests := []struct { - name string - req []byte - authErr error - signCert *ssh.Certificate - signErr error - addUserCert *ssh.Certificate - addUserErr error - body []byte - statusCode int + name string + req []byte + authErr error + signCert *ssh.Certificate + signErr error + addUserCert *ssh.Certificate + addUserErr error + tlsSignCerts []*x509.Certificate + tlsSignErr error + body []byte + statusCode int }{ - {"ok-user", userReq, nil, user, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":"%s"}`, userB64)), http.StatusCreated}, - {"ok-host", hostReq, nil, host, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":"%s"}`, hostB64)), http.StatusCreated}, - {"ok-user-add", userAddReq, nil, user, nil, user, nil, []byte(fmt.Sprintf(`{"crt":"%s","addUserCrt":"%s"}`, userB64, userB64)), http.StatusCreated}, - {"fail-body", []byte("bad-json"), nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, - {"fail-validate", []byte("{}"), nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, - {"fail-publicKey", []byte(`{"publicKey":"Zm9v","ott":"ott"}`), nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, - {"fail-publicKey", []byte(fmt.Sprintf(`{"publicKey":"%s","ott":"ott","addUserPublicKey":"Zm9v"}`, base64.StdEncoding.EncodeToString(user.Key.Marshal()))), nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, - {"fail-authorize", userReq, fmt.Errorf("an-error"), nil, nil, nil, nil, nil, http.StatusUnauthorized}, - {"fail-signSSH", userReq, nil, nil, fmt.Errorf("an-error"), nil, nil, nil, http.StatusForbidden}, - {"fail-SignSSHAddUser", userAddReq, nil, user, nil, nil, fmt.Errorf("an-error"), nil, http.StatusForbidden}, + {"ok-user", userReq, nil, user, nil, nil, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":"%s"}`, userB64)), http.StatusCreated}, + {"ok-host", hostReq, nil, host, nil, nil, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":"%s"}`, hostB64)), http.StatusCreated}, + {"ok-user-add", userAddReq, nil, user, nil, user, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":"%s","addUserCrt":"%s"}`, userB64, userB64)), http.StatusCreated}, + {"ok-user-identity", userIdentityReq, nil, user, nil, user, nil, identityCerts, nil, []byte(fmt.Sprintf(`{"crt":"%s","identityCrt":[%s]}`, userB64, identityCertsPEM)), http.StatusCreated}, + {"fail-body", []byte("bad-json"), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, + {"fail-validate", []byte("{}"), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, + {"fail-publicKey", []byte(`{"publicKey":"Zm9v","ott":"ott"}`), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, + {"fail-publicKey", []byte(fmt.Sprintf(`{"publicKey":"%s","ott":"ott","addUserPublicKey":"Zm9v"}`, base64.StdEncoding.EncodeToString(user.Key.Marshal()))), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest}, + {"fail-authorize", userReq, fmt.Errorf("an-error"), nil, nil, nil, nil, nil, nil, nil, http.StatusUnauthorized}, + {"fail-signSSH", userReq, nil, nil, fmt.Errorf("an-error"), nil, nil, nil, nil, nil, http.StatusForbidden}, + {"fail-SignSSHAddUser", userAddReq, nil, user, nil, nil, fmt.Errorf("an-error"), nil, nil, nil, http.StatusForbidden}, + {"fail-user-identity", userIdentityReq, nil, user, nil, user, nil, nil, fmt.Errorf("an-error"), nil, http.StatusForbidden}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -297,6 +324,9 @@ func Test_caHandler_SSHSign(t *testing.T) { signSSHAddUser: func(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { return tt.addUserCert, tt.addUserErr }, + sign: func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + return tt.tlsSignCerts, tt.tlsSignErr + }, }).(*caHandler) req := httptest.NewRequest("POST", "http://example.com/ssh/sign", bytes.NewReader(tt.req)) @@ -537,6 +567,57 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { } } +func Test_caHandler_SSHGetHosts(t *testing.T) { + hosts := []sshutil.Host{ + {HostID: "1", HostGroups: []sshutil.HostGroup{{ID: "1", Name: "group 1"}}, Hostname: "host1"}, + {HostID: "2", HostGroups: []sshutil.HostGroup{{ID: "1", Name: "group 1"}, {ID: "2", Name: "group 2"}}, Hostname: "host2"}, + } + hostsJSON, err := json.Marshal(hosts) + assert.FatalError(t, err) + + tests := []struct { + name string + hosts []sshutil.Host + err error + body []byte + statusCode int + }{ + {"ok", hosts, nil, []byte(fmt.Sprintf(`{"hosts":%s}`, hostsJSON)), http.StatusOK}, + {"empty (array)", []sshutil.Host{}, nil, []byte(`{"hosts":[]}`), http.StatusOK}, + {"empty (nil)", nil, nil, []byte(`{"hosts":null}`), http.StatusOK}, + {"error", nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ + getSSHHosts: func(*x509.Certificate) ([]sshutil.Host, error) { + return tt.hosts, tt.err + }, + }).(*caHandler) + + req := httptest.NewRequest("GET", "http://example.com/ssh/host", http.NoBody) + w := httptest.NewRecorder() + h.SSHGetHosts(logging.NewResponseLogger(w), req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.SSHGetHosts StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.SSHGetHosts unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), tt.body) { + t.Errorf("caHandler.SSHGetHosts Body = %s, wants %s", body, tt.body) + } + } + }) + } +} + func Test_caHandler_SSHBastion(t *testing.T) { bastion := &authority.Bastion{ Hostname: "bastion.local", From b25cbbe6caa13a21c911af8a061c5812daba7a07 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 17:30:06 -0800 Subject: [PATCH 082/163] Create a custom client that sends a custom User-Agent. --- ca/bootstrap_test.go | 4 +-- ca/client.go | 78 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 3449b45a..9b78d0ee 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -156,8 +156,8 @@ func TestBootstrap(t *testing.T) { if !reflect.DeepEqual(got.endpoint, tt.want.endpoint) { t.Errorf("Bootstrap() endpoint = %v, want %v", got.endpoint, tt.want.endpoint) } - gotTR := got.client.Transport.(*http.Transport) - wantTR := tt.want.client.Transport.(*http.Transport) + gotTR := got.client.GetTransport().(*http.Transport) + wantTR := tt.want.client.GetTransport().(*http.Transport) if !reflect.DeepEqual(gotTR.TLSClientConfig.RootCAs, wantTR.TLSClientConfig.RootCAs) { t.Errorf("Bootstrap() certPool = %v, want %v", gotTR.TLSClientConfig.RootCAs, wantTR.TLSClientConfig.RootCAs) } diff --git a/ca/client.go b/ca/client.go index 2a8e9ca8..0267dfa3 100644 --- a/ca/client.go +++ b/ca/client.go @@ -32,6 +32,58 @@ import ( "gopkg.in/square/go-jose.v2/jwt" ) +// UserAgent will set the User-Agent header in the client requests. +var UserAgent = "step-http-client/1.0" + +type uaClient struct { + Client *http.Client +} + +func newClient(transport http.RoundTripper) *uaClient { + return &uaClient{ + Client: &http.Client{ + Transport: transport, + }, + } +} + +func newInsecureClient() *uaClient { + return &uaClient{ + Client: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + }, + } +} + +func (c *uaClient) GetTransport() http.RoundTripper { + return c.Client.Transport +} + +func (c *uaClient) SetTransport(tr http.RoundTripper) { + c.Client.Transport = tr +} + +func (c *uaClient) Get(url string) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, errors.Wrapf(err, "new request GET %s failed", url) + } + req.Header.Set("User-Agent", UserAgent) + return c.Client.Do(req) +} + +func (c *uaClient) Post(url, contentType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + req.Header.Set("User-Agent", UserAgent) + return c.Client.Do(req) +} + // RetryFunc defines the method used to retry a request. If it returns true, the // request will be retried once. type RetryFunc func(code int) bool @@ -354,7 +406,7 @@ func WithProvisionerLimit(limit int) ProvisionerOption { // Client implements an HTTP client for the CA server. type Client struct { - client *http.Client + client *uaClient endpoint *url.URL retryFunc RetryFunc opts []ClientOption @@ -377,9 +429,7 @@ func NewClient(endpoint string, opts ...ClientOption) (*Client, error) { } return &Client{ - client: &http.Client{ - Transport: tr, - }, + client: newClient(tr), endpoint: u, retryFunc: o.retryFunc, opts: opts, @@ -398,7 +448,7 @@ func (c *Client) retryOnError(r *http.Response) bool { return false } r.Body.Close() - c.client.Transport = tr + c.client.SetTransport(tr) return true } } @@ -408,7 +458,7 @@ func (c *Client) retryOnError(r *http.Response) bool { // GetRootCAs returns the RootCAs certificate pool from the configured // transport. func (c *Client) GetRootCAs() *x509.CertPool { - switch t := c.client.Transport.(type) { + switch t := c.client.GetTransport().(type) { case *http.Transport: if t.TLSClientConfig != nil { return t.TLSClientConfig.RootCAs @@ -426,7 +476,7 @@ func (c *Client) GetRootCAs() *x509.CertPool { // SetTransport updates the transport of the internal HTTP client. func (c *Client) SetTransport(tr http.RoundTripper) { - c.client.Transport = tr + c.client.SetTransport(tr) } // Version performs the version request to the CA and returns the @@ -486,7 +536,7 @@ func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) { sha256Sum = strings.ToLower(strings.Replace(sha256Sum, "-", "", -1)) u := c.endpoint.ResolveReference(&url.URL{Path: "/root/" + sha256Sum}) retry: - resp, err := getInsecureClient().Get(u.String()) + resp, err := newInsecureClient().Get(u.String()) if err != nil { return nil, errors.Wrapf(err, "client GET %s failed", u) } @@ -573,10 +623,10 @@ func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.Revo if err != nil { return nil, errors.Wrap(err, "error marshaling request") } - var client *http.Client + var client *uaClient retry: if tr != nil { - client = &http.Client{Transport: tr} + client = newClient(tr) } else { client = c.client } @@ -1082,14 +1132,6 @@ func createCertificateRequest(commonName string, sans []string, key crypto.Priva return &api.CertificateRequest{CertificateRequest: cr}, key, nil } -func getInsecureClient() *http.Client { - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } -} - // getRootCAPath returns the path where the root CA is stored based on the // STEPPATH environment variable. func getRootCAPath() string { From 55237d635cfa8ab2a5b9f0c4644127f9ba57ed1b Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 2 Dec 2019 19:11:27 -0500 Subject: [PATCH 083/163] Fix authority calling wrong revoke method --- authority/authorize.go | 4 ++-- authority/provisioner/jwk.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/authority/authorize.go b/authority/authorize.go index db2b2414..3353c6b1 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -77,7 +77,7 @@ func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner if reuseKey, err := p.GetTokenID(ott); err == nil { ok, err := a.db.UseToken(reuseKey, ott) if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when checking if token already used"), + return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when attempting to store token"), http.StatusInternalServerError, errContext} } if !ok { @@ -163,7 +163,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { if err != nil { return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} } - if err = p.AuthorizeSSHRevoke(ctx, token); err != nil { + if err = p.AuthorizeRevoke(ctx, token); err != nil { return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} } return nil diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index c47960f9..fa61ee2c 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -118,7 +118,8 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errors.New("invalid token: invalid audience claim (aud)") + return nil, errors.Errorf("invalid token: invalid audience claim (aud); want %s, but got %s", + audiences, claims.Audience) } if claims.Subject == "" { From 83129fd59f43e852b6e22acf2f2da69a4dc9429b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 4 Dec 2019 12:04:46 -0800 Subject: [PATCH 084/163] Add quotes in configuration paths. --- pki/templates.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pki/templates.go b/pki/templates.go index e6c1655b..3b2ba56f 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -32,9 +32,9 @@ var SSHTemplateData = map[string]string{ // Note: on windows `Include C:\...` is treated as a relative path. "include.tpl": `Host * {{- if or .User.GOOS "none" | eq "windows" }} - Include {{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config + Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config" {{- else }} - Include {{.User.StepPath}}/ssh/config + Include "{{.User.StepPath}}/ssh/config" {{- end }}`, // config.tpl is the step ssh config file, it includes the Match rule and @@ -47,10 +47,10 @@ var SSHTemplateData = map[string]string{ User {{.User.User}} {{- end }} {{- if or .User.GOOS "none" | eq "windows" }} - UserKnownHostsFile {{.User.StepPath}}\ssh\known_hosts + UserKnownHostsFile "{{.User.StepPath}}\ssh\known_hosts" ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p {{- else }} - UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts + UserKnownHostsFile "{{.User.StepPath}}/ssh/known_hosts" ProxyCommand step ssh proxycommand %r %h %p {{- end }} `, From 2676d525c484bbc51f19700708df22e91a5f6c59 Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 9 Dec 2019 12:54:32 -0800 Subject: [PATCH 085/163] redundant variable type def --- ca/identity.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ca/identity.go b/ca/identity.go index fea77d35..a63ae671 100644 --- a/ca/identity.go +++ b/ca/identity.go @@ -24,7 +24,7 @@ import ( type IdentityType string // DisableIdentity is a global variable to disable the identity. -var DisableIdentity bool = false +var DisableIdentity = false // Disabled represents a disabled identity type const Disabled IdentityType = "" From 50152391a397461364c909615a6348e8af6fe183 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 9 Dec 2019 16:54:48 -0800 Subject: [PATCH 086/163] Add leeway in identity not before. --- authority/config.go | 3 +-- ca/identity.go | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/authority/config.go b/authority/config.go index 462a764b..812a1db4 100644 --- a/authority/config.go +++ b/authority/config.go @@ -7,11 +7,10 @@ import ( "os" "time" - "github.com/smallstep/certificates/templates" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" ) diff --git a/ca/identity.go b/ca/identity.go index a63ae671..d7ad7042 100644 --- a/ca/identity.go +++ b/ca/identity.go @@ -32,6 +32,9 @@ const Disabled IdentityType = "" // MutualTLS represents the identity using mTLS const MutualTLS IdentityType = "mTLS" +// DefaultLeeway is the duration for matching not before claims. +const DefaultLeeway = 1 * time.Minute + // IdentityFile contains the location of the identity file. var IdentityFile = filepath.Join(config.StepPath(), "config", "identity.json") @@ -179,8 +182,8 @@ func (i *Identity) Options() ([]ClientOption, error) { if err != nil { return nil, errors.Wrap(err, "error creating identity certificate") } - now := time.Now() - if now.Before(x509Cert.NotBefore) || now.After(x509Cert.NotAfter) { + now := time.Now().Truncate(time.Second) + if now.Add(DefaultLeeway).Before(x509Cert.NotBefore) || now.After(x509Cert.NotAfter) { return nil, nil } return []ClientOption{WithCertificate(crt)}, nil From 8eeb82d0ce6ae5437d70266becf5781f18c5902b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 13:10:45 -0800 Subject: [PATCH 087/163] Store renew certificate in the database. --- authority/tls.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/authority/tls.go b/authority/tls.go index 2baa71f0..0dd4f323 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -202,6 +202,13 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error http.StatusInternalServerError, apiCtx{}} } + if err = a.db.StoreCertificate(serverCert); err != nil { + if err != db.ErrNotImplemented { + return nil, &apiError{errors.Wrap(err, "error storing certificate in db"), + http.StatusInternalServerError, apiCtx{}} + } + } + return []*x509.Certificate{serverCert, caCert}, nil } From 40ec0b435a6abeba88d09ef1d89fbd4630bc2e11 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 13:40:14 -0800 Subject: [PATCH 088/163] Add method to create an ssh token. --- ca/provisioner.go | 35 ++++++++++++++ ca/provisioner_test.go | 102 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/ca/provisioner.go b/ca/provisioner.go index bc1acb94..3f86c068 100644 --- a/ca/provisioner.go +++ b/ca/provisioner.go @@ -22,6 +22,7 @@ type Provisioner struct { name string kid string audience string + sshAudience string fingerprint string jwk *jose.JSONWebKey tokenLifetime time.Duration @@ -60,6 +61,7 @@ func NewProvisioner(name, kid, caURL string, password []byte, opts ...ClientOpti name: name, kid: jwk.KeyID, audience: client.endpoint.ResolveReference(&url.URL{Path: "/1.0/sign"}).String(), + sshAudience: client.endpoint.ResolveReference(&url.URL{Path: "/1.0/ssh/sign"}).String(), fingerprint: fp, jwk: jwk, tokenLifetime: tokenLifetime, @@ -116,6 +118,39 @@ func (p *Provisioner) Token(subject string, sans ...string) (string, error) { return tok.SignedString(p.jwk.Algorithm, p.jwk.Key) } +func (p *Provisioner) SSHToken(certType, keyID string, principals []string) (string, error) { + jwtID, err := randutil.Hex(64) + if err != nil { + return "", err + } + + notBefore := time.Now() + notAfter := notBefore.Add(tokenLifetime) + tokOptions := []token.Options{ + token.WithJWTID(jwtID), + token.WithKid(p.kid), + token.WithIssuer(p.name), + token.WithAudience(p.sshAudience), + token.WithValidity(notBefore, notAfter), + token.WithSSH(provisioner.SSHOptions{ + CertType: certType, + Principals: principals, + KeyID: keyID, + }), + } + + if p.fingerprint != "" { + tokOptions = append(tokOptions, token.WithSHA(p.fingerprint)) + } + + tok, err := provision.New(keyID, tokOptions...) + if err != nil { + return "", err + } + + return tok.SignedString(p.jwk.Algorithm, p.jwk.Key) +} + func decryptProvisionerJWK(encryptedKey string, password []byte) (*jose.JSONWebKey, error) { enc, err := jose.ParseEncrypted(encryptedKey) if err != nil { diff --git a/ca/provisioner_test.go b/ca/provisioner_test.go index 40015df7..fcfaeb10 100644 --- a/ca/provisioner_test.go +++ b/ca/provisioner_test.go @@ -198,3 +198,105 @@ func TestProvisioner_Token(t *testing.T) { }) } } + +func TestProvisioner_SSHToken(t *testing.T) { + p := getTestProvisioner(t, "https://127.0.0.1:9000") + sha := "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7" + + type fields struct { + name string + kid string + fingerprint string + jwk *jose.JSONWebKey + tokenLifetime time.Duration + } + type args struct { + certType string + keyID string + principals []string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + {"ok", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"user", "foo@smallstep.com", []string{"foo"}}, false}, + {"ok host", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"host", "foo.smallstep.com", []string{"foo.smallstep.com"}}, false}, + {"ok multiple principals", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"user", "foo@smallstep.com", []string{"foo", "bar"}}, false}, + {"fail-no-subject", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"user", "", []string{"foo"}}, true}, + {"fail-no-key", fields{p.name, p.kid, sha, &jose.JSONWebKey{}, p.tokenLifetime}, args{"user", "foo@smallstep.com", []string{"foo"}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Provisioner{ + name: tt.fields.name, + kid: tt.fields.kid, + audience: "https://127.0.0.1:9000/1.0/sign", + sshAudience: "https://127.0.0.1:9000/1.0/ssh/sign", + fingerprint: tt.fields.fingerprint, + jwk: tt.fields.jwk, + tokenLifetime: tt.fields.tokenLifetime, + } + got, err := p.SSHToken(tt.args.certType, tt.args.keyID, tt.args.principals) + if (err != nil) != tt.wantErr { + t.Errorf("Provisioner.SSHToken() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr == false { + jwt, err := jose.ParseSigned(got) + if err != nil { + t.Error(err) + return + } + var claims jose.Claims + if err := jwt.Claims(tt.fields.jwk.Public(), &claims); err != nil { + t.Error(err) + return + } + if err := claims.ValidateWithLeeway(jose.Expected{ + Audience: []string{"https://127.0.0.1:9000/1.0/ssh/sign"}, + Issuer: tt.fields.name, + Subject: tt.args.keyID, + Time: time.Now().UTC(), + }, time.Minute); err != nil { + t.Error(err) + return + } + lifetime := claims.Expiry.Time().Sub(claims.NotBefore.Time()) + if lifetime != tt.fields.tokenLifetime { + t.Errorf("Claims token life time = %s, want %s", lifetime, tt.fields.tokenLifetime) + } + allClaims := make(map[string]interface{}) + if err := jwt.Claims(tt.fields.jwk.Public(), &allClaims); err != nil { + t.Error(err) + return + } + if v, ok := allClaims["sha"].(string); !ok || v != sha { + t.Errorf("Claim sha = %s, want %s", v, sha) + } + + principals := make([]interface{}, len(tt.args.principals)) + for i, p := range tt.args.principals { + principals[i] = p + } + want := map[string]interface{}{ + "ssh": map[string]interface{}{ + "certType": tt.args.certType, + "keyID": tt.args.keyID, + "principals": principals, + "validAfter": "", + "validBefore": "", + }, + } + if !reflect.DeepEqual(allClaims["step"], want) { + t.Errorf("Claim step = %s, want %s", allClaims["step"], want) + } + if v, ok := allClaims["jti"].(string); !ok || v == "" { + t.Errorf("Claim jti = %s, want not blank", v) + } + } + }) + } +} From 014d2c7ccd19ea3f9c9ca3b635230344085be0de Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 13:41:06 -0800 Subject: [PATCH 089/163] Go mod tidy. --- go.sum | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go.sum b/go.sum index 70517090..bf91885d 100644 --- a/go.sum +++ b/go.sum @@ -114,13 +114,12 @@ github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHP github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= +github.com/smallstep/cli v0.13.3 h1:S29UydCtDVy0QQBtGdatq064tnks1/0DYxxnEtNiQpc= github.com/smallstep/cli v0.13.3/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2 h1:Q0B9XBAn3KzjZKH3ojxLQolUnHSXuomfFjm+/KbIdpY= github.com/smallstep/cli v0.14.0-rc.1.0.20191024214139-914a67ed80c2/go.mod h1:GoA1cE4YrZRRvVbFlPKJUsMuWHnFBX+R88j1pmpbGgk= github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 h1:kHHsScwMUDlepa7LkxR55r6NT9ra+U9KsP6qJGZb5jM= github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03/go.mod h1:dklnISxr+GzUmurBngEF9Jvj0aI9KK5uVgZwOdFniNs= -github.com/smallstep/cli v0.14.0-rc.1.0.20191127003637-bf0d6274e86f h1:NOch88Di/v87Mlg+l3JUgkfYzOb/aOIpHV5O7B7bZmA= -github.com/smallstep/cli v0.14.0-rc.1.0.20191127003637-bf0d6274e86f/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 h1:yAB5yZI+iqulxALQoAPv6CNBBYTLeGYcU9ZbwBiD9Es= github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= From e841a86b48de376f2ba4d71f12ad812738471fd0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 16:34:01 -0800 Subject: [PATCH 090/163] Make sure to define the KeyID from the token if available. --- authority/provisioner/jwk.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index fa61ee2c..231b1580 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -209,8 +209,9 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, if !opts.ValidBefore.IsZero() { signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) } - // Make sure to define the the KeyID - if opts.KeyID == "" { + if opts.KeyID != "" { + signOptions = append(signOptions, sshCertificateKeyIDModifier(opts.KeyID)) + } else { signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject)) } From 7fe1eb8686a039b1af6b1e6a1de7f37d74c97953 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 16:34:24 -0800 Subject: [PATCH 091/163] Add GetTransport to client. --- ca/client.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ca/client.go b/ca/client.go index 0267dfa3..a5d88808 100644 --- a/ca/client.go +++ b/ca/client.go @@ -474,6 +474,11 @@ func (c *Client) GetRootCAs() *x509.CertPool { } } +// GetTransport returns the transport of the internal HTTP client. +func (c *Client) GetTransport() http.RoundTripper { + return c.client.GetTransport() +} + // SetTransport updates the transport of the internal HTTP client. func (c *Client) SetTransport(tr http.RoundTripper) { c.client.SetTransport(tr) From 1e17ec7d33de04e9ed5782ae9658f198209f563d Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 9 Dec 2019 23:14:56 -0800 Subject: [PATCH 092/163] Use x5cInsecure token for /ssh/check-host endpoint --- api/ssh.go | 5 +++-- authority/authority.go | 8 +++++--- authority/options.go | 10 ++++++++++ authority/ssh.go | 12 +++++++++++- ca/client.go | 3 ++- go.mod | 2 +- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index b559c27a..546c8f1e 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -25,7 +25,7 @@ type SSHAuthority interface { GetSSHRoots() (*authority.SSHKeys, error) GetSSHFederation() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) - CheckSSHHost(principal string) (bool, error) + CheckSSHHost(ctx context.Context, principal string, token string) (bool, error) GetSSHHosts(cert *x509.Certificate) ([]sshutil.Host, error) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) } @@ -199,6 +199,7 @@ type SSHConfigResponse struct { type SSHCheckPrincipalRequest struct { Type string `json:"type"` Principal string `json:"principal"` + Token string `json:"token,omitempty"` } // Validate checks the check principal request. @@ -431,7 +432,7 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { return } - exists, err := h.Authority.CheckSSHHost(body.Principal) + exists, err := h.Authority.CheckSSHHost(r.Context(), body.Principal, body.Token) if err != nil { WriteError(w, InternalServerError(err)) return diff --git a/authority/authority.go b/authority/authority.go index 9d04f339..25b40350 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -1,6 +1,7 @@ package authority import ( + "context" "crypto" "crypto/sha256" "crypto/x509" @@ -40,9 +41,10 @@ type Authority struct { // Do not re-initialize initOnce bool // Custom functions - sshBastionFunc func(user, hostname string) (*Bastion, error) - sshGetHostsFunc func(cert *x509.Certificate) ([]sshutil.Host, error) - getIdentityFunc provisioner.GetIdentityFunc + sshBastionFunc func(user, hostname string) (*Bastion, error) + sshCheckHostFunc func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error) + sshGetHostsFunc func(cert *x509.Certificate) ([]sshutil.Host, error) + getIdentityFunc provisioner.GetIdentityFunc } // New creates and initiates a new Authority type. diff --git a/authority/options.go b/authority/options.go index a2e19edb..10f0ec1a 100644 --- a/authority/options.go +++ b/authority/options.go @@ -1,6 +1,7 @@ package authority import ( + "context" "crypto/x509" "github.com/smallstep/certificates/authority/provisioner" @@ -42,3 +43,12 @@ func WithSSHGetHosts(fn func(cert *x509.Certificate) ([]sshutil.Host, error)) Op a.sshGetHostsFunc = fn } } + +// WithSSHCheckHost sets a custom function to check whether a given host is +// 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) { + a.sshCheckHostFunc = fn + } +} diff --git a/authority/ssh.go b/authority/ssh.go index 232527a8..fbf97545 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -656,7 +656,17 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) } // CheckSSHHost checks the given principal has been registered before. -func (a *Authority) CheckSSHHost(principal string) (bool, error) { +func (a *Authority) CheckSSHHost(ctx context.Context, principal string, token string) (bool, error) { + if a.sshCheckHostFunc != nil { + exists, err := a.sshCheckHostFunc(ctx, principal, token, a.GetRootCertificates()) + if err != nil { + return false, &apiError{ + err: errors.Wrap(err, "checkSSHHost: error from injected checkSSHHost func"), + code: http.StatusInternalServerError, + } + } + return exists, nil + } exists, err := a.db.IsSSHHost(principal) if err != nil { if err == db.ErrNotImplemented { diff --git a/ca/client.go b/ca/client.go index 0267dfa3..2d3bf6cc 100644 --- a/ca/client.go +++ b/ca/client.go @@ -947,11 +947,12 @@ retry: // SSHCheckHost performs the POST /ssh/check-host request to the CA with the // given principal. -func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, error) { +func (c *Client) SSHCheckHost(principal string, token string) (*api.SSHCheckPrincipalResponse, error) { var retried bool body, err := json.Marshal(&api.SSHCheckPrincipalRequest{ Type: provisioner.SSHHostCert, Principal: principal, + Token: token, }) if err != nil { return nil, errors.Wrap(err, "error marshaling request") diff --git a/go.mod b/go.mod index 807f8bfc..67464278 100644 --- a/go.mod +++ b/go.mod @@ -18,4 +18,4 @@ require ( gopkg.in/square/go-jose.v2 v2.4.0 ) -// replace github.com/smallstep/cli => ../cli +//replace github.com/smallstep/cli => ../cli From 623be4ef09c870ec303062587a752b963d99a911 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 11 Dec 2019 14:56:50 -0800 Subject: [PATCH 093/163] update cli dep --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 67464278..214aabbe 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 + github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 github.com/smallstep/nosql v0.1.1 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 diff --git a/go.sum b/go.sum index bf91885d..a5a99869 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,7 @@ github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+R github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go.mod h1:/WOAB2LkcjkEbKG5rDol+A22Lp3UsttkLPLkY7tVtuk= github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go.mod h1:043iBnsMvNhQ+QFwSh0N6JR3H2yamHPPAc78vCf+8Tc= github.com/smallstep/certificates v0.14.0-rc.1.0.20191126035953-e88034bea402/go.mod h1:r2UTcAZNriKlwvNNXymNAcF3iKL6mTYOYrOCtBYYGJU= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191210005525-50152391a397/go.mod h1:8leACUXHFo0JVm9YcrcX09aar2H8hz1BAWxD1D/GpsU= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= @@ -122,6 +123,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03 h1:kHHsScwMU github.com/smallstep/cli v0.14.0-rc.1.0.20191105013638-8cf838b56d03/go.mod h1:dklnISxr+GzUmurBngEF9Jvj0aI9KK5uVgZwOdFniNs= github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 h1:yAB5yZI+iqulxALQoAPv6CNBBYTLeGYcU9ZbwBiD9Es= github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= +github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 h1:zU1JWNx/Hm518TE2VgfIa0RkaeH9Av3WsZw5OlDbZyI= +github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407/go.mod h1:1DDxP5W6pSuPL7DudNMbr/qVVjToo8qz3tlRt8ka8TA= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From 401fc20e96a775caea3b09d2acbc0cd104d10e40 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 16:26:38 -0800 Subject: [PATCH 094/163] Re-enable profiler. --- cmd/step-ca/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index d3ed089c..468f7084 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -13,6 +13,9 @@ import ( "strconv" "time" + // Server profiler + _ "net/http/pprof" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/commands" "github.com/smallstep/cli/command" From 28b08ef46b76e0f8e2c7fe6eb5ec53a6c094eedb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 16:26:53 -0800 Subject: [PATCH 095/163] Fail silently if the identity fails. --- ca/client.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ca/client.go b/ca/client.go index 66e97275..95b383e5 100644 --- a/ca/client.go +++ b/ca/client.go @@ -119,20 +119,21 @@ func (o *clientOptions) applyDefaultIdentity() error { return nil } + // Do not load an identity if something fails b, err := ioutil.ReadFile(IdentityFile) if err != nil { return nil } var identity Identity if err := json.Unmarshal(b, &identity); err != nil { - return errors.Wrapf(err, "error unmarshaling %s", IdentityFile) + return nil } if err := identity.Validate(); err != nil { - return err + return nil } opts, err := identity.Options() if err != nil { - return err + return nil } for _, fn := range opts { if err := fn(o); err != nil { From bde29b1bbd51b882e5713487337e48c5011662b9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 18:18:13 -0800 Subject: [PATCH 096/163] Addapt tests to the api change. --- api/api_test.go | 6 +++--- api/ssh_test.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 98d612ab..70ba6a89 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -565,7 +565,7 @@ type mockAuthority struct { getSSHRoots func() (*authority.SSHKeys, error) getSSHFederation func() (*authority.SSHKeys, error) getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error) - checkSSHHost func(principal string) (bool, error) + checkSSHHost func(ctx context.Context, principal, token string) (bool, error) getSSHBastion func(user string, hostname string) (*authority.Bastion, error) version func() authority.Version } @@ -715,9 +715,9 @@ func (m *mockAuthority) GetSSHConfig(typ string, data map[string]string) ([]temp return m.ret1.([]templates.Output), m.err } -func (m *mockAuthority) CheckSSHHost(principal string) (bool, error) { +func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { if m.checkSSHHost != nil { - return m.checkSSHHost(principal) + return m.checkSSHHost(ctx, principal, token) } return m.ret1.(bool), m.err } diff --git a/api/ssh_test.go b/api/ssh_test.go index b5ff7002..cb5c7904 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -539,7 +540,7 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := New(&mockAuthority{ - checkSSHHost: func(_ string) (bool, error) { + checkSSHHost: func(ctx context.Context, principal, token string) (bool, error) { return tt.exists, tt.err }, }).(*caHandler) From 96b69896588711532082ec39914abfbf2ec46224 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 18:21:20 -0800 Subject: [PATCH 097/163] Addapt test to api change. --- authority/ssh_test.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 3ea4e98d..9b403132 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -1,6 +1,7 @@ package authority import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -476,7 +477,9 @@ func TestAuthority_CheckSSHHost(t *testing.T) { err error } type args struct { + ctx context.Context principal string + token string } tests := []struct { name string @@ -485,12 +488,12 @@ func TestAuthority_CheckSSHHost(t *testing.T) { want bool wantErr bool }{ - {"true", fields{true, nil}, args{"foo.internal.com"}, true, false}, - {"false", fields{false, nil}, args{"foo.internal.com"}, false, false}, - {"notImplemented", fields{false, db.ErrNotImplemented}, args{"foo.internal.com"}, false, true}, - {"notImplemented", fields{true, db.ErrNotImplemented}, args{"foo.internal.com"}, false, true}, - {"internal", fields{false, fmt.Errorf("an error")}, args{"foo.internal.com"}, false, true}, - {"internal", fields{true, fmt.Errorf("an error")}, args{"foo.internal.com"}, false, true}, + {"true", fields{true, nil}, args{context.TODO(), "foo.internal.com", ""}, true, false}, + {"false", fields{false, nil}, args{context.TODO(), "foo.internal.com", ""}, false, false}, + {"notImplemented", fields{false, db.ErrNotImplemented}, args{context.TODO(), "foo.internal.com", ""}, false, true}, + {"notImplemented", fields{true, db.ErrNotImplemented}, args{context.TODO(), "foo.internal.com", ""}, false, true}, + {"internal", fields{false, fmt.Errorf("an error")}, args{context.TODO(), "foo.internal.com", ""}, false, true}, + {"internal", fields{true, fmt.Errorf("an error")}, args{context.TODO(), "foo.internal.com", ""}, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -500,7 +503,7 @@ func TestAuthority_CheckSSHHost(t *testing.T) { return tt.fields.exists, tt.fields.err }, } - got, err := a.CheckSSHHost(tt.args.principal) + got, err := a.CheckSSHHost(tt.args.ctx, tt.args.principal, tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("Authority.CheckSSHHost() error = %v, wantErr %v", err, tt.wantErr) return From 89b216c21ebb9750695fe70ce660047d0949c1ec Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 18:24:32 -0800 Subject: [PATCH 098/163] Fix test. --- ca/provisioner_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ca/provisioner_test.go b/ca/provisioner_test.go index fcfaeb10..1d20eff6 100644 --- a/ca/provisioner_test.go +++ b/ca/provisioner_test.go @@ -33,6 +33,7 @@ func getTestProvisioner(t *testing.T, caURL string) *Provisioner { name: "mariano", kid: "FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg", audience: client.endpoint.ResolveReference(&url.URL{Path: "/1.0/sign"}).String(), + sshAudience: client.endpoint.ResolveReference(&url.URL{Path: "/1.0/ssh/sign"}).String(), fingerprint: x509util.Fingerprint(cert), jwk: jwk, tokenLifetime: 5 * time.Minute, From 0d9a9e083ed63c2f32a1129b986c2a01112d578b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 20:23:44 -0800 Subject: [PATCH 099/163] Add identity client and move identity to a new package. --- ca/client.go | 36 ++-- ca/identity/client.go | 105 +++++++++++ ca/identity/client_test.go | 133 ++++++++++++++ ca/{ => identity}/identity.go | 113 ++++-------- ca/identity/identity_test.go | 166 ++++++++++++++++++ .../testdata/certs/intermediate_ca.crt | 11 ++ ca/identity/testdata/certs/root_ca.crt | 10 ++ ca/identity/testdata/certs/server.crt | 25 +++ ca/identity/testdata/config/ca.json | 41 +++++ ca/identity/testdata/config/defaults.json | 6 + ca/identity/testdata/config/fail.json | 1 + ca/identity/testdata/config/identity.json | 5 + ca/identity/testdata/identity/expired.crt | 25 +++ ca/identity/testdata/identity/identity.crt | 25 +++ ca/identity/testdata/identity/identity_key | 5 + ca/identity/testdata/identity/not_before.crt | 25 +++ .../testdata/secrets/intermediate_ca_key | 8 + ca/identity/testdata/secrets/root_ca_key | 8 + ca/identity/testdata/secrets/server_key | 5 + 19 files changed, 667 insertions(+), 86 deletions(-) create mode 100644 ca/identity/client.go create mode 100644 ca/identity/client_test.go rename ca/{ => identity}/identity.go (60%) create mode 100644 ca/identity/identity_test.go create mode 100644 ca/identity/testdata/certs/intermediate_ca.crt create mode 100644 ca/identity/testdata/certs/root_ca.crt create mode 100644 ca/identity/testdata/certs/server.crt create mode 100644 ca/identity/testdata/config/ca.json create mode 100644 ca/identity/testdata/config/defaults.json create mode 100644 ca/identity/testdata/config/fail.json create mode 100644 ca/identity/testdata/config/identity.json create mode 100644 ca/identity/testdata/identity/expired.crt create mode 100644 ca/identity/testdata/identity/identity.crt create mode 100644 ca/identity/testdata/identity/identity_key create mode 100644 ca/identity/testdata/identity/not_before.crt create mode 100644 ca/identity/testdata/secrets/intermediate_ca_key create mode 100644 ca/identity/testdata/secrets/root_ca_key create mode 100644 ca/identity/testdata/secrets/server_key diff --git a/ca/client.go b/ca/client.go index 95b383e5..0bfe386f 100644 --- a/ca/client.go +++ b/ca/client.go @@ -25,13 +25,18 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca/identity" "github.com/smallstep/cli/config" "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/x509util" "golang.org/x/net/http2" "gopkg.in/square/go-jose.v2/jwt" ) +// DisableIdentity is a global variable to disable the identity. +var DisableIdentity = false + // UserAgent will set the User-Agent header in the client requests. var UserAgent = "step-http-client/1.0" @@ -120,26 +125,18 @@ func (o *clientOptions) applyDefaultIdentity() error { } // Do not load an identity if something fails - b, err := ioutil.ReadFile(IdentityFile) + i, err := identity.LoadDefaultIdentity() if err != nil { return nil } - var identity Identity - if err := json.Unmarshal(b, &identity); err != nil { - return nil - } - if err := identity.Validate(); err != nil { + if err := i.Validate(); err != nil { return nil } - opts, err := identity.Options() + crt, err := i.TLSCertificate() if err != nil { return nil } - for _, fn := range opts { - if err := fn(o); err != nil { - return err - } - } + o.certificate = crt return nil } @@ -1111,6 +1108,21 @@ func CreateCertificateRequest(commonName string, sans ...string) (*api.Certifica return createCertificateRequest(commonName, sans, key) } +// CreateIdentityRequest returns a new CSR to create the identity. If an +// identity was already present it reuses the private key. +func CreateIdentityRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) { + var identityKey crypto.PrivateKey + if i, err := identity.LoadDefaultIdentity(); err == nil && i.Key != "" { + if k, err := pemutil.Read(i.Key); err == nil { + identityKey = k + } + } + if identityKey == nil { + return CreateCertificateRequest(commonName, sans...) + } + return createCertificateRequest(commonName, sans, identityKey) +} + func createCertificateRequest(commonName string, sans []string, key crypto.PrivateKey) (*api.CertificateRequest, crypto.PrivateKey, error) { if len(sans) == 0 { sans = []string{commonName} diff --git a/ca/identity/client.go b/ca/identity/client.go new file mode 100644 index 00000000..d615a019 --- /dev/null +++ b/ca/identity/client.go @@ -0,0 +1,105 @@ +package identity + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/pkg/errors" +) + +// Client wraps http.Client with a transport using the step root and identity. +type Client struct { + CaURL *url.URL + *http.Client +} + +// ResolveReference resolves the given reference from the CaURL. +func (c *Client) ResolveReference(ref *url.URL) *url.URL { + return c.CaURL.ResolveReference(ref) +} + +// LoadStepClient configures an http.Client with the root in +// $STEPPATH/config/defaults.json and the identity defined in +// $STEPPATH/config/identity.json +func LoadClient() (*Client, error) { + b, err := ioutil.ReadFile(DefaultsFile) + if err != nil { + return nil, errors.Wrapf(err, "error reading %s", DefaultsFile) + } + + var defaults defaultsConfig + if err := json.Unmarshal(b, &defaults); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling %s", DefaultsFile) + } + if err := defaults.Validate(); err != nil { + return nil, errors.Wrapf(err, "error validating %s", DefaultsFile) + } + caURL, err := url.Parse(defaults.CaURL) + if err != nil { + return nil, errors.Wrapf(err, "error validating %s", DefaultsFile) + } + if caURL.Scheme == "" { + caURL.Scheme = "https" + } + + identity, err := LoadDefaultIdentity() + if err != nil { + return nil, err + } + if err := identity.Validate(); err != nil { + return nil, errors.Wrapf(err, "error validating %s", IdentityFile) + } + if kind := identity.Kind(); kind != MutualTLS { + return nil, errors.Errorf("unsupported identity %s: only mTLS is currently supported", kind) + } + + // Prepare transport with information in defaults.json and identity.json + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{} + + // RootCAs + b, err = ioutil.ReadFile(defaults.Root) + if err != nil { + return nil, errors.Wrapf(err, "error loading %s", defaults.Root) + } + pool := x509.NewCertPool() + if pool.AppendCertsFromPEM(b) { + tr.TLSClientConfig.RootCAs = pool + } + + // Certificate + crt, err := tls.LoadX509KeyPair(identity.Certificate, identity.Key) + if err != nil { + return nil, fmt.Errorf("error loading certificate: %v", err) + } + tr.TLSClientConfig.Certificates = []tls.Certificate{crt} + + return &Client{ + CaURL: caURL, + Client: &http.Client{ + Transport: tr, + }, + }, nil + +} + +type defaultsConfig struct { + CaURL string `json:"ca-url"` + Root string `json:"root"` +} + +func (c *defaultsConfig) Validate() error { + switch { + case c.CaURL == "": + return fmt.Errorf("missing or invalid `ca-url` property") + case c.Root == "": + return fmt.Errorf("missing or invalid `root` property") + default: + return nil + } +} diff --git a/ca/identity/client_test.go b/ca/identity/client_test.go new file mode 100644 index 00000000..4cbcc3a2 --- /dev/null +++ b/ca/identity/client_test.go @@ -0,0 +1,133 @@ +package identity + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "testing" +) + +func TestClient_ResolveReference(t *testing.T) { + type fields struct { + CaURL *url.URL + } + type args struct { + ref *url.URL + } + tests := []struct { + name string + fields fields + args args + want *url.URL + }{ + {"ok", fields{&url.URL{Scheme: "https", Host: "localhost"}}, args{&url.URL{Path: "/foo"}}, &url.URL{Scheme: "https", Host: "localhost", Path: "/foo"}}, + {"ok", fields{&url.URL{Scheme: "https", Host: "localhost", Path: "/bar"}}, args{&url.URL{Path: "/foo"}}, &url.URL{Scheme: "https", Host: "localhost", Path: "/foo"}}, + {"ok", fields{&url.URL{Scheme: "https", Host: "localhost"}}, args{&url.URL{Path: "/foo", RawQuery: "foo=bar"}}, &url.URL{Scheme: "https", Host: "localhost", Path: "/foo", RawQuery: "foo=bar"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + CaURL: tt.fields.CaURL, + } + if got := c.ResolveReference(tt.args.ref); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.ResolveReference() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLoadClient(t *testing.T) { + oldIdentityFile := IdentityFile + oldDefaultsFile := DefaultsFile + defer func() { + IdentityFile = oldIdentityFile + DefaultsFile = oldDefaultsFile + }() + + crt, err := tls.LoadX509KeyPair("testdata/identity/identity.crt", "testdata/identity/identity_key") + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadFile("testdata/certs/root_ca.crt") + if err != nil { + t.Fatal(err) + } + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(b) + + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + Certificates: []tls.Certificate{crt}, + RootCAs: pool, + } + expected := &Client{ + CaURL: &url.URL{Scheme: "https", Host: "127.0.0.1"}, + Client: &http.Client{ + Transport: tr, + }, + } + + tests := []struct { + name string + prepare func() + want *Client + wantErr bool + }{ + {"ok", func() { IdentityFile = "testdata/config/identity.json"; DefaultsFile = "testdata/config/defaults.json" }, expected, false}, + {"fail identity", func() { IdentityFile = "testdata/config/missing.json"; DefaultsFile = "testdata/config/defaults.json" }, nil, true}, + {"fail identity", func() { IdentityFile = "testdata/config/fail.json"; DefaultsFile = "testdata/config/defaults.json" }, nil, true}, + {"fail defaults", func() { IdentityFile = "testdata/config/identity.json"; DefaultsFile = "testdata/config/missing.json" }, nil, true}, + {"fail defaults", func() { IdentityFile = "testdata/config/identity.json"; DefaultsFile = "testdata/config/fail.json" }, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + got, err := LoadClient() + if (err != nil) != tt.wantErr { + t.Errorf("LoadClient() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.want == nil { + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("LoadClient() = %#v, want %#v", got, tt.want) + } + } else { + if !reflect.DeepEqual(got.CaURL, tt.want.CaURL) || + !reflect.DeepEqual(got.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs, tt.want.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs) || + !reflect.DeepEqual(got.Client.Transport.(*http.Transport).TLSClientConfig.Certificates, tt.want.Client.Transport.(*http.Transport).TLSClientConfig.Certificates) { + t.Errorf("LoadClient() = %#v, want %#v", got, tt.want) + } + } + }) + } +} + +func Test_defaultsConfig_Validate(t *testing.T) { + type fields struct { + CaURL string + Root string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{"https://127.0.0.1", "root_ca.crt"}, false}, + {"fail ca-url", fields{"", "root_ca.crt"}, true}, + {"fail root", fields{"https://127.0.0.1", ""}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &defaultsConfig{ + CaURL: tt.fields.CaURL, + Root: tt.fields.Root, + } + if err := c.Validate(); (err != nil) != tt.wantErr { + t.Errorf("defaultsConfig.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/ca/identity.go b/ca/identity/identity.go similarity index 60% rename from ca/identity.go rename to ca/identity/identity.go index d7ad7042..35736236 100644 --- a/ca/identity.go +++ b/ca/identity/identity.go @@ -1,4 +1,4 @@ -package ca +package identity import ( "bytes" @@ -8,7 +8,6 @@ import ( "encoding/json" "encoding/pem" "io/ioutil" - "net/http" "os" "path/filepath" "strings" @@ -20,17 +19,14 @@ import ( "github.com/smallstep/cli/crypto/pemutil" ) -// IdentityType represents the different types of identity files. -type IdentityType string - -// DisableIdentity is a global variable to disable the identity. -var DisableIdentity = false +// Type represents the different types of identity files. +type Type string // Disabled represents a disabled identity type -const Disabled IdentityType = "" +const Disabled Type = "" // MutualTLS represents the identity using mTLS -const MutualTLS IdentityType = "mTLS" +const MutualTLS Type = "mTLS" // DefaultLeeway is the duration for matching not before claims. const DefaultLeeway = 1 * time.Minute @@ -38,6 +34,9 @@ const DefaultLeeway = 1 * time.Minute // IdentityFile contains the location of the identity file. var IdentityFile = filepath.Join(config.StepPath(), "config", "identity.json") +// DefaultsFile contains the location of the defaults file. +var DefaultsFile = filepath.Join(config.StepPath(), "config", "defaults.json") + // Identity represents the identity file that can be used to authenticate with // the CA. type Identity struct { @@ -46,26 +45,11 @@ type Identity struct { Key string `json:"key"` } -// NewIdentityRequest returns a new CSR to create the identity. If an identity -// was already present it reuses the private key. -func NewIdentityRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) { - var identityKey crypto.PrivateKey - if i, err := LoadDefaultIdentity(); err == nil && i.Key != "" { - if k, err := pemutil.Read(i.Key); err == nil { - identityKey = k - } - } - if identityKey == nil { - return CreateCertificateRequest(commonName, sans...) - } - return createCertificateRequest(commonName, sans, identityKey) -} - // LoadDefaultIdentity loads the default identity. func LoadDefaultIdentity() (*Identity, error) { b, err := ioutil.ReadFile(IdentityFile) if err != nil { - return nil, errors.Wrap(err, "error reading identity json") + return nil, errors.Wrapf(err, "error reading %s", IdentityFile) } identity := new(Identity) if err := json.Unmarshal(b, &identity); err != nil { @@ -137,14 +121,14 @@ func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) er } // Kind returns the type for the given identity. -func (i *Identity) Kind() IdentityType { +func (i *Identity) Kind() Type { switch strings.ToLower(i.Type) { case "": return Disabled case "mtls": return MutualTLS default: - return IdentityType(i.Type) + return Type(i.Type) } } @@ -160,74 +144,55 @@ func (i *Identity) Validate() error { if i.Key == "" { return errors.New("identity.key cannot be empty") } + if err := fileExists(i.Certificate); err != nil { + return err + } + if err := fileExists(i.Key); err != nil { + return err + } return nil default: return errors.Errorf("unsupported identity type %s", i.Type) } } -// Options returns the ClientOptions used for the given identity. -func (i *Identity) Options() ([]ClientOption, error) { +// TLSCertificate returns a tls.Certificate for the identity. +func (i *Identity) TLSCertificate() (tls.Certificate, error) { + fail := func(err error) (tls.Certificate, error) { return tls.Certificate{}, err } switch i.Kind() { case Disabled: - return nil, nil + return tls.Certificate{}, nil case MutualTLS: crt, err := tls.LoadX509KeyPair(i.Certificate, i.Key) if err != nil { - return nil, errors.Wrap(err, "error creating identity certificate") + return fail(errors.Wrap(err, "error creating identity certificate")) } + // Check if certificate is expired. - // Do not return any options if expired. x509Cert, err := x509.ParseCertificate(crt.Certificate[0]) if err != nil { - return nil, errors.Wrap(err, "error creating identity certificate") + return fail(errors.Wrap(err, "error creating identity certificate")) } now := time.Now().Truncate(time.Second) - if now.Add(DefaultLeeway).Before(x509Cert.NotBefore) || now.After(x509Cert.NotAfter) { - return nil, nil + if now.Add(DefaultLeeway).Before(x509Cert.NotBefore) { + return fail(errors.New("certificate is not yet valid")) } - return []ClientOption{WithCertificate(crt)}, nil + if now.After(x509Cert.NotAfter) { + return fail(errors.New("certificate is already expired")) + } + return crt, nil default: - return nil, errors.Errorf("unsupported identity type %s", i.Type) + return fail(errors.Errorf("unsupported identity type %s", i.Type)) } } -// Renew renews the identity certificate using the given client. -func (i *Identity) Renew(client *Client) error { - switch i.Kind() { - case Disabled: - return nil - case MutualTLS: - cert, err := tls.LoadX509KeyPair(i.Certificate, i.Key) - if err != nil { - return errors.Wrap(err, "error creating identity certificate") - } - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: client.GetRootCAs(), - PreferServerCipherSuites: true, - }, - } - resp, err := client.Renew(tr) - if err != nil { - return err - } - buf := new(bytes.Buffer) - for _, crt := range resp.CertChainPEM { - block := &pem.Block{ - Type: "CERTIFICATE", - Bytes: crt.Raw, - } - if err := pem.Encode(buf, block); err != nil { - return errors.Wrap(err, "error encoding identity certificate") - } - } - if err := ioutil.WriteFile(i.Certificate, buf.Bytes(), 0600); err != nil { - return errors.Wrap(err, "error writing identity certificate") - } - return nil - default: - return errors.Errorf("unsupported identity type %s", i.Type) +func fileExists(filename string) error { + info, err := os.Stat(filename) + if err != nil { + return errors.Wrapf(err, "error reading %s", filename) + } + if info.IsDir() { + return errors.Errorf("error reading %s: file is a directory", filename) } + return nil } diff --git a/ca/identity/identity_test.go b/ca/identity/identity_test.go new file mode 100644 index 00000000..58f5db71 --- /dev/null +++ b/ca/identity/identity_test.go @@ -0,0 +1,166 @@ +package identity + +import ( + "crypto/tls" + "reflect" + "testing" +) + +func TestLoadDefaultIdentity(t *testing.T) { + oldFile := IdentityFile + defer func() { + IdentityFile = oldFile + }() + + expected := &Identity{ + Type: "mTLS", + Certificate: "testdata/identity/identity.crt", + Key: "testdata/identity/identity_key", + } + tests := []struct { + name string + prepare func() + want *Identity + wantErr bool + }{ + {"ok", func() { IdentityFile = "testdata/config/identity.json" }, expected, false}, + {"fail read", func() { IdentityFile = "testdata/config/missing.json" }, nil, true}, + {"fail unmarshal", func() { IdentityFile = "testdata/config/fail.json" }, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + got, err := LoadDefaultIdentity() + if (err != nil) != tt.wantErr { + t.Errorf("LoadDefaultIdentity() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("LoadDefaultIdentity() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIdentity_Kind(t *testing.T) { + type fields struct { + Type string + } + tests := []struct { + name string + fields fields + want Type + }{ + {"disabled", fields{""}, Disabled}, + {"mutualTLS", fields{"mTLS"}, MutualTLS}, + {"unknown", fields{"unknown"}, Type("unknown")}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &Identity{ + Type: tt.fields.Type, + } + if got := i.Kind(); got != tt.want { + t.Errorf("Identity.Kind() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIdentity_Validate(t *testing.T) { + type fields struct { + Type string + Certificate string + Key string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, false}, + {"ok disabled", fields{}, false}, + {"fail type", fields{"foo", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, true}, + {"fail certificate", fields{"mTLS", "", "testdata/identity/identity_key"}, true}, + {"fail key", fields{"mTLS", "testdata/identity/identity.crt", ""}, true}, + {"fail missing certificate", fields{"mTLS", "testdata/identity/missing.crt", "testdata/identity/identity_key"}, true}, + {"fail missing key", fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/missing_key"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &Identity{ + Type: tt.fields.Type, + Certificate: tt.fields.Certificate, + Key: tt.fields.Key, + } + if err := i.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Identity.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIdentity_TLSCertificate(t *testing.T) { + expected, err := tls.LoadX509KeyPair("testdata/identity/identity.crt", "testdata/identity/identity_key") + if err != nil { + t.Fatal(err) + } + + type fields struct { + Type string + Certificate string + Key string + } + tests := []struct { + name string + fields fields + want tls.Certificate + wantErr bool + }{ + {"ok", fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, expected, false}, + {"ok disabled", fields{}, tls.Certificate{}, false}, + {"fail type", fields{"foo", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, tls.Certificate{}, true}, + {"fail certificate", fields{"mTLS", "testdata/certs/server.crt", "testdata/identity/identity_key"}, tls.Certificate{}, true}, + {"fail not after", fields{"mTLS", "testdata/identity/expired.crt", "testdata/identity/identity_key"}, tls.Certificate{}, true}, + {"fail not before", fields{"mTLS", "testdata/identity/not_before.crt", "testdata/identity/identity_key"}, tls.Certificate{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &Identity{ + Type: tt.fields.Type, + Certificate: tt.fields.Certificate, + Key: tt.fields.Key, + } + got, err := i.TLSCertificate() + if (err != nil) != tt.wantErr { + t.Errorf("Identity.TLSCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Identity.TLSCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_fileExists(t *testing.T) { + type args struct { + filename string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"ok", args{"testdata/identity/identity.crt"}, false}, + {"missing", args{"testdata/identity/missing.crt"}, true}, + {"directory", args{"testdata/identity"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := fileExists(tt.args.filename); (err != nil) != tt.wantErr { + t.Errorf("fileExists() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/ca/identity/testdata/certs/intermediate_ca.crt b/ca/identity/testdata/certs/intermediate_ca.crt new file mode 100644 index 00000000..8d7a1b87 --- /dev/null +++ b/ca/identity/testdata/certs/intermediate_ca.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy +MDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5 +PoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO +BgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au +B82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK +CXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT +VQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7 +-----END CERTIFICATE----- diff --git a/ca/identity/testdata/certs/root_ca.crt b/ca/identity/testdata/certs/root_ca.crt new file mode 100644 index 00000000..3488f0bc --- /dev/null +++ b/ca/identity/testdata/certs/root_ca.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBfDCCASGgAwIBAgIQE8W0gyMruWxRDfegdPHrdDAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy +MDkwMjQ1MThaMBwxGjAYBgNVBAMTEVNtYWxsc3RlcCBSb290IENBMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEgd74QbUDcEj3aV5Oxv5eAMzwnejj7S/iDFAp89t9 +kEb+Ux4NZC3Pay+92yRL//dBUI5WOopLXBniYomH4SFJg6NFMEMwDgYDVR0PAQH/ +BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFIMGbnL2/h/9XIUz +y0NRmEycCgl0MAoGCCqGSM49BAMCA0kAMEYCIQD3/IUBL5/9Hpdp2+t4XnA42cwQ +j5WkGY5hJIhdQ5P8qgIhAMf19nAIUlSbXKPf21Gv6eYEoNuuLfpcqnfBt5NJX64M +-----END CERTIFICATE----- diff --git a/ca/identity/testdata/certs/server.crt b/ca/identity/testdata/certs/server.crt new file mode 100644 index 00000000..05f02310 --- /dev/null +++ b/ca/identity/testdata/certs/server.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIICHDCCAcKgAwIBAgIQQ4n25nGGKm6uGyVQ4cDNCTAKBggqhkjOPQQDAjAkMSIw +IAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTE5MTIxMjAyNTAz +OVoXDTI5MTIwOTAyNTAzOVowFjEUMBIGA1UEAxMLdGVzdCBzZXJ2ZXIwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAATmQRMCzRP1hBcYhAXlbiyR9QtsQosQfCZTS+en +g6TtL9VjWsQXqd1SSStfi0grPyiTQLIPhPbSho/VJzSpf59Do4HjMIHgMA4GA1Ud +DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O +BBYEFBvz34jDFrb3G4qiGkZZj99BnabAMB8GA1UdIwQYMBaAFPeQLgfNr67dlCcg +0zQUB9wfLHj1MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATBTBgwrBgEEAYKk +ZMYoQAEEQzBBAgEBBA9qb2VAZXhhbXBsZS5jb20EKzJ3U05fQ21leFhXaWdfRG5w +VlpzWUZkTUgxU3RjODZCSUJ6TjBydDVpcEUwCgYIKoZIzj0EAwIDSAAwRQIhAOt6 +/x9LWQyBtx3RcyyALF2//OCfGjAx0zLGmUsXIHGIAiAZGVwTxbhxiYU95AXncS3F +3tXNaaIJyyO7atiVPhCR1A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy +MDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5 +PoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO +BgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au +B82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK +CXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT +VQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7 +-----END CERTIFICATE----- diff --git a/ca/identity/testdata/config/ca.json b/ca/identity/testdata/config/ca.json new file mode 100644 index 00000000..4201b3e1 --- /dev/null +++ b/ca/identity/testdata/config/ca.json @@ -0,0 +1,41 @@ +{ + "root": "testdata/certs/root_ca.crt", + "federatedRoots": [], + "crt": "testdata/certs/intermediate_ca.crt", + "key": "testdata/secrets/intermediate_ca_key", + "address": ":443", + "dnsNames": [ + "127.0.0.1", + "localhost" + ], + "logger": { + "format": "text" + }, + "authority": { + "provisioners": [ + { + "type": "jwk", + "name": "joe@example.com", + "key": { + "use": "sig", + "kty": "EC", + "kid": "2wSN_CmexXWig_DnpVZsYFdMH1Stc86BIBzN0rt5ipE", + "crv": "P-256", + "alg": "ES256", + "x": "QqYaIULUQqP0EOmogorCcQIxEtI7-zCRcUVFxyNwq4Q", + "y": "YeIMipM7uMHjlxpFIUbfCBC1xEXczXNYRzJCMyrGcH0" + }, + "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiSVQ3MVNUMTNNMTd1S3Y4VHRDczYyUSJ9.TXShNLPcITS0bFvQeMjjCDhQLICQs1ShECkgUkUsAm9ZWpSq6Yu03w.SWxtxscivS3L5Yo5.O-XY9YKK8wEJgVs7X1-FxiM_6w4s7iJQNXRD2JrZRsXtDqUz7diPfXuBOFPUFsNzykvob1qCsU4B23Ek2nbaS2HqPrIOGbOvOsR8Pt6kNoraH1QDp3Hyzkv0S-VGM0MCGYDDmmH33PZmsdS36Aw8v9xBnDHlwlMg4NjTskxpqggfQl01433B0lCJqJdrmeBeGL1ZCKixvc-wAQxU8GH5iiD925ViLY7RlVo-tmIBXpxRgheLgKiuMxmgPvf15qCdgU5TRqeuJbYJLzvPpoai0W4WHjpM1zLjjmp5OYRFW4m4ZRZf5g1Cm4lstFPUlTn85fkMZFdBh4_bFbjAv7k.epXp8DZKHj_dxP9EohwDIg" + } + ] + }, + "tls": { + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + ], + "minVersion": 1.2, + "maxVersion": 1.2, + "renegotiation": false + } +} \ No newline at end of file diff --git a/ca/identity/testdata/config/defaults.json b/ca/identity/testdata/config/defaults.json new file mode 100644 index 00000000..7d26ed19 --- /dev/null +++ b/ca/identity/testdata/config/defaults.json @@ -0,0 +1,6 @@ +{ + "ca-url": "https://127.0.0.1", + "ca-config": "testdata/config/ca.json", + "fingerprint": "9dc35eef23a234b2520516a3169090d7ec2fc61323bdd6e4fde08bcfec5d0931", + "root": "testdata/certs/root_ca.crt" +} \ No newline at end of file diff --git a/ca/identity/testdata/config/fail.json b/ca/identity/testdata/config/fail.json new file mode 100644 index 00000000..5ce77559 --- /dev/null +++ b/ca/identity/testdata/config/fail.json @@ -0,0 +1 @@ +This is not a json file \ No newline at end of file diff --git a/ca/identity/testdata/config/identity.json b/ca/identity/testdata/config/identity.json new file mode 100644 index 00000000..f3d5e8f1 --- /dev/null +++ b/ca/identity/testdata/config/identity.json @@ -0,0 +1,5 @@ +{ + "type": "mTLS", + "crt": "testdata/identity/identity.crt", + "key": "testdata/identity/identity_key" +} \ No newline at end of file diff --git a/ca/identity/testdata/identity/expired.crt b/ca/identity/testdata/identity/expired.crt new file mode 100644 index 00000000..5d723ddd --- /dev/null +++ b/ca/identity/testdata/identity/expired.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIICIDCCAcegAwIBAgIRAM1GK1TLmvWLVOjP0dqVCiEwCgYIKoZIzj0EAwIwJDEi +MCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0xODEyMTIwMzI2 +MzZaFw0xODEyMTMwMzI2MzZaMBoxGDAWBgNVBAMMD2pvZUBleGFtcGxlLmNvbTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABI0+NSjg3+vGhAeZGrxPksrXFqq0AIUB +D3nQPmGPuUWIEmbt6qp3EVF/o+KwzWgDv5fzBmDlBkdBRz9xc3XIcQ2jgeMwgeAw +HwYDVR0jBBgwFoAU95AuB82vrt2UJyDTNBQH3B8sePUwDgYDVR0PAQH/BAQDAgWg +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU1Ht6zX2M +eVXcnxhM4hxU0RCblNowGgYDVR0RBBMwEYEPam9lQGV4YW1wbGUuY29tMFMGDCsG +AQQBgqRkxihAAQRDMEECAQEED2pvZUBleGFtcGxlLmNvbQQrMndTTl9DbWV4WFdp +Z19EbnBWWnNZRmRNSDFTdGM4NkJJQnpOMHJ0NWlwRTAKBggqhkjOPQQDAgNHADBE +AiBgoPACCRJ6s+C5Yz3BWeyM6VnWewctnaMsVJKyPdb98AIgV/7HRZsc5Xgi8iVt +D4XxVOZDu/y1V4VIH5W4INfg6JA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy +MDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5 +PoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO +BgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au +B82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK +CXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT +VQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7 +-----END CERTIFICATE----- diff --git a/ca/identity/testdata/identity/identity.crt b/ca/identity/testdata/identity/identity.crt new file mode 100644 index 00000000..b8824136 --- /dev/null +++ b/ca/identity/testdata/identity/identity.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIICHzCCAcagAwIBAgIQfVgJ4dZ2AhS88uthvlIzyjAKBggqhkjOPQQDAjAkMSIw +IAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTE5MTIxMjAyNDgy +MVoXDTI5MTIwOTAyNDgyMVowGjEYMBYGA1UEAwwPam9lQGV4YW1wbGUuY29tMFkw +EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjT41KODf68aEB5kavE+SytcWqrQAhQEP +edA+YY+5RYgSZu3qqncRUX+j4rDNaAO/l/MGYOUGR0FHP3FzdchxDaOB4zCB4DAO +BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0G +A1UdDgQWBBTUe3rNfYx5VdyfGEziHFTREJuU2jAfBgNVHSMEGDAWgBT3kC4Hza+u +3ZQnINM0FAfcHyx49TAaBgNVHREEEzARgQ9qb2VAZXhhbXBsZS5jb20wUwYMKwYB +BAGCpGTGKEABBEMwQQIBAQQPam9lQGV4YW1wbGUuY29tBCsyd1NOX0NtZXhYV2ln +X0RucFZac1lGZE1IMVN0Yzg2QklCek4wcnQ1aXBFMAoGCCqGSM49BAMCA0cAMEQC +IHkYnKUBrXc/GIosKgnhHqVeRMi2O1JhnZdTE1uoy2C0AiA9ZrmGqPvpQ86f5yq5 +llsieqBTzIum6A45q0/4XeN3QA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy +MDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5 +PoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO +BgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au +B82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK +CXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT +VQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7 +-----END CERTIFICATE----- diff --git a/ca/identity/testdata/identity/identity_key b/ca/identity/testdata/identity/identity_key new file mode 100644 index 00000000..332f0ebb --- /dev/null +++ b/ca/identity/testdata/identity/identity_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJ4A5QcJioS5I89uT/hkuWPy/nlW5qy8vM8Tm2sgUCDyoAoGCCqGSM49 +AwEHoUQDQgAEjT41KODf68aEB5kavE+SytcWqrQAhQEPedA+YY+5RYgSZu3qqncR +UX+j4rDNaAO/l/MGYOUGR0FHP3FzdchxDQ== +-----END EC PRIVATE KEY----- diff --git a/ca/identity/testdata/identity/not_before.crt b/ca/identity/testdata/identity/not_before.crt new file mode 100644 index 00000000..71cbc8d8 --- /dev/null +++ b/ca/identity/testdata/identity/not_before.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIICIDCCAcagAwIBAgIQHRUI8eJv55I9/5IHi1mpmjAKBggqhkjOPQQDAjAkMSIw +IAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTI5MTIwOTAzMzAx +NFoXDTI5MTIxMDAzMzAxNFowGjEYMBYGA1UEAwwPam9lQGV4YW1wbGUuY29tMFkw +EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjT41KODf68aEB5kavE+SytcWqrQAhQEP +edA+YY+5RYgSZu3qqncRUX+j4rDNaAO/l/MGYOUGR0FHP3FzdchxDaOB4zCB4DAf +BgNVHSMEGDAWgBT3kC4Hza+u3ZQnINM0FAfcHyx49TAOBgNVHQ8BAf8EBAMCBaAw +HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTUe3rNfYx5 +VdyfGEziHFTREJuU2jAaBgNVHREEEzARgQ9qb2VAZXhhbXBsZS5jb20wUwYMKwYB +BAGCpGTGKEABBEMwQQIBAQQPam9lQGV4YW1wbGUuY29tBCsyd1NOX0NtZXhYV2ln +X0RucFZac1lGZE1IMVN0Yzg2QklCek4wcnQ1aXBFMAoGCCqGSM49BAMCA0gAMEUC +IQDJVzxQ0lY9+haZLs5qxhbaWoTmXwCbYdkwhThDfM/izwIgRZCmshc1flfimIPO +eblT85Gk16ND/diV6pmtUaMT73I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy +MDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5 +PoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO +BgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au +B82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK +CXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT +VQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7 +-----END CERTIFICATE----- diff --git a/ca/identity/testdata/secrets/intermediate_ca_key b/ca/identity/testdata/secrets/intermediate_ca_key new file mode 100644 index 00000000..107a6c9d --- /dev/null +++ b/ca/identity/testdata/secrets/intermediate_ca_key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,37e3019a1aa420225bbd4f342a3ce330 + +3SNIIXzE11cGKTPnErv8S1HIrd2lbQo+lsMT9GrU33GAi/MTvp0hx0txy7E3CsrU +DbuPXs3zLCjgoNLOeyAWLqGjPLRt4YNnZGVDi3F/dFUAWxgXH8gZQ2d9ZqAXwxdd +bhT4ZcRFgFzCPlHExtxBrJe+Tmeuq1HqD+8gpOSYbt0= +-----END EC PRIVATE KEY----- diff --git a/ca/identity/testdata/secrets/root_ca_key b/ca/identity/testdata/secrets/root_ca_key new file mode 100644 index 00000000..c11f2909 --- /dev/null +++ b/ca/identity/testdata/secrets/root_ca_key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,48fc92ab6885b2377d8bbac5b035bde2 + +BE07EXlLmJbAfjt2c9GwQoTT07DzjLWgiGWqxMKC0bOLQdmHe2pFudeQldDhTOme +xnr9rRj9h+GRWV+sIzp+ilGd4/F6lfzWMl44GA5y7uBNWKhnI1uB9m9oo69hBNRg +dQuDmAx5EWXvg7Mgg1MQZIPY8539RXWJdAs+uRSI12g= +-----END EC PRIVATE KEY----- diff --git a/ca/identity/testdata/secrets/server_key b/ca/identity/testdata/secrets/server_key new file mode 100644 index 00000000..4ae87816 --- /dev/null +++ b/ca/identity/testdata/secrets/server_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIGgfuMfx7h1VaCYzzEPZhrbTLsAr6dtyuQ2RLl6jKqBoAoGCCqGSM49 +AwEHoUQDQgAE5kETAs0T9YQXGIQF5W4skfULbEKLEHwmU0vnp4Ok7S/VY1rEF6nd +UkkrX4tIKz8ok0CyD4T20oaP1Sc0qX+fQw== +-----END EC PRIVATE KEY----- From 3717c7a8d3ab6d3f575de323114be4c152508684 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Dec 2019 12:23:53 -0800 Subject: [PATCH 100/163] Improve identity tests. --- ca/identity/client_test.go | 37 ++++++-- ca/identity/identity.go | 17 ++-- ca/identity/identity_test.go | 88 ++++++++++++++++++++ ca/identity/testdata/config/badIdentity.json | 5 ++ ca/identity/testdata/config/badca.json | 6 ++ ca/identity/testdata/config/badroot.json | 6 ++ 6 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 ca/identity/testdata/config/badIdentity.json create mode 100644 ca/identity/testdata/config/badca.json create mode 100644 ca/identity/testdata/config/badroot.json diff --git a/ca/identity/client_test.go b/ca/identity/client_test.go index 4cbcc3a2..9ab14e94 100644 --- a/ca/identity/client_test.go +++ b/ca/identity/client_test.go @@ -76,11 +76,38 @@ func TestLoadClient(t *testing.T) { want *Client wantErr bool }{ - {"ok", func() { IdentityFile = "testdata/config/identity.json"; DefaultsFile = "testdata/config/defaults.json" }, expected, false}, - {"fail identity", func() { IdentityFile = "testdata/config/missing.json"; DefaultsFile = "testdata/config/defaults.json" }, nil, true}, - {"fail identity", func() { IdentityFile = "testdata/config/fail.json"; DefaultsFile = "testdata/config/defaults.json" }, nil, true}, - {"fail defaults", func() { IdentityFile = "testdata/config/identity.json"; DefaultsFile = "testdata/config/missing.json" }, nil, true}, - {"fail defaults", func() { IdentityFile = "testdata/config/identity.json"; DefaultsFile = "testdata/config/fail.json" }, nil, true}, + {"ok", func() { + IdentityFile = "testdata/config/identity.json" + DefaultsFile = "testdata/config/defaults.json" + }, expected, false}, + {"fail identity", func() { + IdentityFile = "testdata/config/missing.json" + DefaultsFile = "testdata/config/defaults.json" + }, nil, true}, + {"fail identity", func() { + IdentityFile = "testdata/config/fail.json" + DefaultsFile = "testdata/config/defaults.json" + }, nil, true}, + {"fail defaults", func() { + IdentityFile = "testdata/config/identity.json" + DefaultsFile = "testdata/config/missing.json" + }, nil, true}, + {"fail defaults", func() { + IdentityFile = "testdata/config/identity.json" + DefaultsFile = "testdata/config/fail.json" + }, nil, true}, + {"fail ca", func() { + IdentityFile = "testdata/config/identity.json" + DefaultsFile = "testdata/config/badca.json" + }, nil, true}, + {"fail root", func() { + IdentityFile = "testdata/config/identity.json" + DefaultsFile = "testdata/config/badroot.json" + }, nil, true}, + {"fail type", func() { + IdentityFile = "testdata/config/badIdentity.json" + DefaultsFile = "testdata/config/defaults.json" + }, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/ca/identity/identity.go b/ca/identity/identity.go index 35736236..48ed66e6 100644 --- a/ca/identity/identity.go +++ b/ca/identity/identity.go @@ -58,21 +58,26 @@ func LoadDefaultIdentity() (*Identity, error) { return identity, nil } +// configDir and identityDir are used in WriteDefaultIdentity for testing +// purposes. +var ( + configDir = filepath.Join(config.StepPath(), "config") + identityDir = filepath.Join(config.StepPath(), "identity") +) + // WriteDefaultIdentity writes the given certificates and key and the // identity.json pointing to the new files. func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error { - base := filepath.Join(config.StepPath(), "config") - if err := os.MkdirAll(base, 0700); err != nil { + if err := os.MkdirAll(configDir, 0700); err != nil { return errors.Wrap(err, "error creating config directory") } - base = filepath.Join(config.StepPath(), "identity") - if err := os.MkdirAll(base, 0700); err != nil { + if err := os.MkdirAll(identityDir, 0700); err != nil { return errors.Wrap(err, "error creating identity directory") } - certFilename := filepath.Join(base, "identity.crt") - keyFilename := filepath.Join(base, "identity_key") + certFilename := filepath.Join(identityDir, "identity.crt") + keyFilename := filepath.Join(identityDir, "identity_key") // Write certificate buf := new(bytes.Buffer) diff --git a/ca/identity/identity_test.go b/ca/identity/identity_test.go index 58f5db71..1a73afdb 100644 --- a/ca/identity/identity_test.go +++ b/ca/identity/identity_test.go @@ -1,9 +1,17 @@ package identity import ( + "crypto" "crypto/tls" + "io/ioutil" + "os" + "path/filepath" "reflect" "testing" + + "github.com/smallstep/cli/crypto/pemutil" + + "github.com/smallstep/certificates/api" ) func TestLoadDefaultIdentity(t *testing.T) { @@ -164,3 +172,83 @@ func Test_fileExists(t *testing.T) { }) } } + +func TestWriteDefaultIdentity(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "go-tests") + if err != nil { + t.Fatal(err) + } + + oldConfigDir := configDir + oldIdentityDir := identityDir + oldIdentityFile := IdentityFile + defer func() { + configDir = oldConfigDir + identityDir = oldIdentityDir + IdentityFile = oldIdentityFile + os.RemoveAll(tmpDir) + }() + + certs, err := pemutil.ReadCertificateBundle("testdata/identity/identity.crt") + if err != nil { + t.Fatal(err) + } + key, err := pemutil.Read("testdata/identity/identity_key") + if err != nil { + t.Fatal(err) + } + + var certChain []api.Certificate + for _, c := range certs { + certChain = append(certChain, api.Certificate{Certificate: c}) + } + + configDir = filepath.Join(tmpDir, "config") + identityDir = filepath.Join(tmpDir, "identity") + IdentityFile = filepath.Join(tmpDir, "config", "identity.json") + + type args struct { + certChain []api.Certificate + key crypto.PrivateKey + } + tests := []struct { + name string + prepare func() + args args + wantErr bool + }{ + {"ok", func() {}, args{certChain, key}, false}, + {"fail mkdir config", func() { + configDir = filepath.Join(tmpDir, "identity", "identity.crt") + identityDir = filepath.Join(tmpDir, "identity") + }, args{certChain, key}, true}, + {"fail mkdir identity", func() { + configDir = filepath.Join(tmpDir, "config") + identityDir = filepath.Join(tmpDir, "identity", "identity.crt") + }, args{certChain, key}, true}, + {"fail certificate", func() { + configDir = filepath.Join(tmpDir, "config") + identityDir = filepath.Join(tmpDir, "bad-dir") + os.MkdirAll(identityDir, 0600) + }, args{certChain, key}, true}, + {"fail key", func() { + configDir = filepath.Join(tmpDir, "config") + identityDir = filepath.Join(tmpDir, "identity") + }, args{certChain, "badKey"}, true}, + {"fail write identity", func() { + configDir = filepath.Join(tmpDir, "bad-dir") + identityDir = filepath.Join(tmpDir, "identity") + IdentityFile = filepath.Join(configDir, "identity.json") + os.MkdirAll(configDir, 0600) + }, args{certChain, key}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + if err := WriteDefaultIdentity(tt.args.certChain, tt.args.key); (err != nil) != tt.wantErr { + t.Errorf("WriteDefaultIdentity() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/ca/identity/testdata/config/badIdentity.json b/ca/identity/testdata/config/badIdentity.json new file mode 100644 index 00000000..f1a73ecd --- /dev/null +++ b/ca/identity/testdata/config/badIdentity.json @@ -0,0 +1,5 @@ +{ + "type": "", + "crt": "testdata/identity/identity.crt", + "key": "testdata/identity/identity_key" +} \ No newline at end of file diff --git a/ca/identity/testdata/config/badca.json b/ca/identity/testdata/config/badca.json new file mode 100644 index 00000000..29327ffb --- /dev/null +++ b/ca/identity/testdata/config/badca.json @@ -0,0 +1,6 @@ +{ + "ca-url": ":", + "ca-config": "testdata/config/ca.json", + "fingerprint": "9dc35eef23a234b2520516a3169090d7ec2fc61323bdd6e4fde08bcfec5d0931", + "root": "testdata/certs/root_ca.crt" +} \ No newline at end of file diff --git a/ca/identity/testdata/config/badroot.json b/ca/identity/testdata/config/badroot.json new file mode 100644 index 00000000..50e86d5e --- /dev/null +++ b/ca/identity/testdata/config/badroot.json @@ -0,0 +1,6 @@ +{ + "ca-url": "https://127.0.0.1", + "ca-config": "testdata/config/ca.json", + "fingerprint": "9dc35eef23a234b2520516a3169090d7ec2fc61323bdd6e4fde08bcfec5d0931", + "root": "testdata/certs/missing.crt" +} \ No newline at end of file From 3f71b8debd5f6e1b1fad228e92ae5f15ab1a3e95 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Dec 2019 12:48:34 -0800 Subject: [PATCH 101/163] Add mTLS test for identity client. --- ca/identity/client_test.go | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/ca/identity/client_test.go b/ca/identity/client_test.go index 9ab14e94..8ff27bba 100644 --- a/ca/identity/client_test.go +++ b/ca/identity/client_test.go @@ -5,11 +5,74 @@ import ( "crypto/x509" "io/ioutil" "net/http" + "net/http/httptest" "net/url" "reflect" "testing" ) +func TestClient(t *testing.T) { + oldIdentityFile := IdentityFile + oldDefaultsFile := DefaultsFile + defer func() { + IdentityFile = oldIdentityFile + DefaultsFile = oldDefaultsFile + }() + + IdentityFile = "testdata/config/identity.json" + DefaultsFile = "testdata/config/defaults.json" + + client, err := LoadClient() + if err != nil { + t.Fatal(err) + } + + okServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + w.WriteHeader(http.StatusUnauthorized) + } else { + w.WriteHeader(http.StatusOK) + } + })) + defer okServer.Close() + + crt, err := tls.LoadX509KeyPair("testdata/certs/server.crt", "testdata/secrets/server_key") + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadFile("testdata/certs/root_ca.crt") + if err != nil { + t.Fatal(err) + } + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(b) + + okServer.TLS = &tls.Config{ + Certificates: []tls.Certificate{crt}, + ClientCAs: pool, + ClientAuth: tls.VerifyClientCertIfGiven, + } + okServer.StartTLS() + + badServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + })) + defer badServer.Close() + + if resp, err := client.Get(okServer.URL); err != nil { + t.Errorf("client.Get() error = %v", err) + } else { + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("client.Get() = %d, want %d", resp.StatusCode, http.StatusOK) + } + } + + if _, err := client.Get(badServer.URL); err == nil { + t.Errorf("client.Get() error = %v, wantErr true", err) + } +} + func TestClient_ResolveReference(t *testing.T) { type fields struct { CaURL *url.URL From 7ecb831e07d00c5947000fc5ea7519eeb4a58c8b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Dec 2019 13:16:17 -0800 Subject: [PATCH 102/163] Add wrappers to identity methods in the ca package. --- ca/client.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ca/client.go b/ca/client.go index 0bfe386f..a3bda21b 100644 --- a/ca/client.go +++ b/ca/client.go @@ -1123,6 +1123,16 @@ func CreateIdentityRequest(commonName string, sans ...string) (*api.CertificateR return createCertificateRequest(commonName, sans, identityKey) } +// LoadDefaultIdentity is a wrapper for identity.LoadDefaultIdentity. +func LoadDefaultIdentity() (*identity.Identity, error) { + return identity.LoadDefaultIdentity() +} + +// WriteDefaultIdentity is a wrapper for identity.WriteDefaultIdentity. +func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error { + return identity.WriteDefaultIdentity(certChain, key) +} + func createCertificateRequest(commonName string, sans []string, key crypto.PrivateKey) (*api.CertificateRequest, crypto.PrivateKey, error) { if len(sans) == 0 { sans = []string{commonName} From d2100821138ce1cdca339a0e411c09ef1ef2dbd2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 13 Dec 2019 13:56:56 -0800 Subject: [PATCH 103/163] Use new version of nosql. --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 214aabbe..752ad11c 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 - github.com/smallstep/nosql v0.1.1 + github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 diff --git a/go.sum b/go.sum index a5a99869..aa9196d1 100644 --- a/go.sum +++ b/go.sum @@ -129,6 +129,8 @@ github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQ github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= github.com/smallstep/nosql v0.1.1/go.mod h1:qyxCqeyGwkuM6bfJSY3sg+aiXEiD0GbQOPzIF8/ZD8Q= +github.com/smallstep/nosql v0.2.0 h1:IscXK9m9hRyl5GoYgn+Iml//5Bpad3LyIj6R0dZosKM= +github.com/smallstep/nosql v0.2.0/go.mod h1:qyxCqeyGwkuM6bfJSY3sg+aiXEiD0GbQOPzIF8/ZD8Q= github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zcrypto v0.0.0-20191122194514-76530dff70e7/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= From ba11f6acb77ec8a0e546cae6cd97c439d2c0ef88 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 13 Dec 2019 13:59:11 -0800 Subject: [PATCH 104/163] Update dependencies. --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 752ad11c..b8311236 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 + github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385 github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 diff --git a/go.sum b/go.sum index aa9196d1..9e9246ba 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,7 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go. github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go.mod h1:043iBnsMvNhQ+QFwSh0N6JR3H2yamHPPAc78vCf+8Tc= github.com/smallstep/certificates v0.14.0-rc.1.0.20191126035953-e88034bea402/go.mod h1:r2UTcAZNriKlwvNNXymNAcF3iKL6mTYOYrOCtBYYGJU= github.com/smallstep/certificates v0.14.0-rc.1.0.20191210005525-50152391a397/go.mod h1:8leACUXHFo0JVm9YcrcX09aar2H8hz1BAWxD1D/GpsU= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191213215656-d2100821138c/go.mod h1:HMXt9hWBm7M7ZrUy0uZ/T/077te2x9bnXZCxrdVsBf4= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= @@ -125,6 +126,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 h1:yAB5yZI+i github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 h1:zU1JWNx/Hm518TE2VgfIa0RkaeH9Av3WsZw5OlDbZyI= github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407/go.mod h1:1DDxP5W6pSuPL7DudNMbr/qVVjToo8qz3tlRt8ka8TA= +github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385 h1:5e1azRUUFvWaE0hCbbXxTKpzPmjjcQ4A/TotE6etaa4= +github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385/go.mod h1:DC6mnMaYNejkAstQrMxkobEgB1QET7zwibISGoMSQis= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From 6d6f496331bbcf86b4bf2cd808eb9c4148cb4842 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 16 Dec 2019 11:22:24 -0800 Subject: [PATCH 105/163] Allow no provisioners. --- authority/config.go | 3 --- authority/config_test.go | 12 ++++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/authority/config.go b/authority/config.go index 812a1db4..8cc742e4 100644 --- a/authority/config.go +++ b/authority/config.go @@ -76,9 +76,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { if c == nil { return errors.New("authority cannot be undefined") } - if len(c.Provisioners) == 0 { - return errors.New("authority.provisioners cannot be empty") - } // Check that only one K8sSA is enabled var k8sCount int diff --git a/authority/config_test.go b/authority/config_test.go index eb5e7a5a..40ae639b 100644 --- a/authority/config_test.go +++ b/authority/config_test.go @@ -255,12 +255,6 @@ func TestAuthConfigValidate(t *testing.T) { err: errors.New("authority cannot be undefined"), } }, - "fail-empty-provisioners": func(t *testing.T) AuthConfigValidateTest { - return AuthConfigValidateTest{ - ac: &AuthConfig{}, - err: errors.New("authority.provisioners cannot be empty"), - } - }, "fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest { return AuthConfigValidateTest{ ac: &AuthConfig{ @@ -283,6 +277,12 @@ func TestAuthConfigValidate(t *testing.T) { err: errors.New("claims: MinTLSCertDuration must be greater than 0"), } }, + "ok-empty-provisioners": func(t *testing.T) AuthConfigValidateTest { + return AuthConfigValidateTest{ + ac: &AuthConfig{}, + asn1dn: x509util.ASN1DN{}, + } + }, "ok-empty-asn1dn-template": func(t *testing.T) AuthConfigValidateTest { return AuthConfigValidateTest{ ac: &AuthConfig{ From f9ef5070f9af040ceaa773af87ff57cc429b8992 Mon Sep 17 00:00:00 2001 From: max furman Date: Sun, 15 Dec 2019 23:54:25 -0800 Subject: [PATCH 106/163] Move api errors to their own package and modify the typedef --- api/api.go | 29 +++--- api/errors.go | 106 +------------------- api/revoke.go | 17 ++-- api/ssh.go | 49 +++++----- api/sshRekey.go | 13 +-- api/sshRenew.go | 11 ++- api/sshRevoke.go | 15 +-- api/utils.go | 3 +- authority/ssh.go | 19 ++-- ca/client.go | 21 ++-- errs/error.go | 250 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 4 + 13 files changed, 350 insertions(+), 190 deletions(-) create mode 100644 errs/error.go diff --git a/api/api.go b/api/api.go index 0c16168f..33aa0f44 100644 --- a/api/api.go +++ b/api/api.go @@ -21,6 +21,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" "github.com/smallstep/cli/crypto/tlsutil" ) @@ -233,13 +234,13 @@ type ProvisionerKeyResponse struct { // or an error if something is wrong. func (s *SignRequest) Validate() error { if s.CsrPEM.CertificateRequest == nil { - return BadRequest(errors.New("missing csr")) + return errs.BadRequest(errors.New("missing csr")) } if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil { - return BadRequest(errors.Wrap(err, "invalid csr")) + return errs.BadRequest(errors.Wrap(err, "invalid csr")) } if s.OTT == "" { - return BadRequest(errors.New("missing ott")) + return errs.BadRequest(errors.New("missing ott")) } return nil @@ -328,7 +329,7 @@ func (h *caHandler) Root(w http.ResponseWriter, r *http.Request) { // Load root certificate with the cert, err := h.Authority.Root(sum) if err != nil { - WriteError(w, NotFound(errors.Wrapf(err, "%s was not found", r.RequestURI))) + WriteError(w, errs.NotFound(errors.Wrapf(err, "%s was not found", r.RequestURI))) return } @@ -349,7 +350,7 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate { func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { var body SignRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } @@ -366,13 +367,13 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { signOpts, err := h.Authority.AuthorizeSign(body.OTT) if err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } certChainPEM := certChainToPEM(certChain) @@ -393,13 +394,13 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { // new one. func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { - WriteError(w, BadRequest(errors.New("missing peer certificate"))) + WriteError(w, errs.BadRequest(errors.New("missing peer certificate"))) return } certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } certChainPEM := certChainToPEM(certChain) @@ -421,13 +422,13 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := parseCursor(r) if err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } p, next, err := h.Authority.GetProvisioners(cursor, limit) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } JSON(w, &ProvisionersResponse{ @@ -441,7 +442,7 @@ func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) { kid := chi.URLParam(r, "kid") key, err := h.Authority.GetEncryptedKey(kid) if err != nil { - WriteError(w, NotFound(err)) + WriteError(w, errs.NotFound(err)) return } JSON(w, &ProvisionerKeyResponse{key}) @@ -451,7 +452,7 @@ func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) { func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { roots, err := h.Authority.GetRoots() if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } @@ -469,7 +470,7 @@ func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { func (h *caHandler) Federation(w http.ResponseWriter, r *http.Request) { federated, err := h.Authority.GetFederation() if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } diff --git a/api/errors.go b/api/errors.go index 90b41565..93057ed2 100644 --- a/api/errors.go +++ b/api/errors.go @@ -8,106 +8,10 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" ) -// StatusCoder interface is used by errors that returns the HTTP response code. -type StatusCoder interface { - StatusCode() int -} - -// StackTracer must be by those errors that return an stack trace. -type StackTracer interface { - StackTrace() errors.StackTrace -} - -// Error represents the CA API errors. -type Error struct { - Status int - Err error -} - -// ErrorResponse represents an error in JSON format. -type ErrorResponse struct { - Status int `json:"status"` - Message string `json:"message"` -} - -// Cause implements the errors.Causer interface and returns the original error. -func (e *Error) Cause() error { - return e.Err -} - -// Error implements the error interface and returns the error string. -func (e *Error) Error() string { - return e.Err.Error() -} - -// StatusCode implements the StatusCoder interface and returns the HTTP response -// code. -func (e *Error) StatusCode() int { - return e.Status -} - -// MarshalJSON implements json.Marshaller interface for the Error struct. -func (e *Error) MarshalJSON() ([]byte, error) { - return json.Marshal(&ErrorResponse{Status: e.Status, Message: http.StatusText(e.Status)}) -} - -// UnmarshalJSON implements json.Unmarshaler interface for the Error struct. -func (e *Error) UnmarshalJSON(data []byte) error { - var er ErrorResponse - if err := json.Unmarshal(data, &er); err != nil { - return err - } - e.Status = er.Status - e.Err = fmt.Errorf(er.Message) - return nil -} - -// NewError returns a new Error. If the given error implements the StatusCoder -// interface we will ignore the given status. -func NewError(status int, err error) error { - if sc, ok := err.(StatusCoder); ok { - return &Error{Status: sc.StatusCode(), Err: err} - } - cause := errors.Cause(err) - if sc, ok := cause.(StatusCoder); ok { - return &Error{Status: sc.StatusCode(), Err: err} - } - return &Error{Status: status, Err: err} -} - -// InternalServerError returns a 500 error with the given error. -func InternalServerError(err error) error { - return NewError(http.StatusInternalServerError, err) -} - -// NotImplemented returns a 500 error with the given error. -func NotImplemented(err error) error { - return NewError(http.StatusNotImplemented, err) -} - -// BadRequest returns an 400 error with the given error. -func BadRequest(err error) error { - return NewError(http.StatusBadRequest, err) -} - -// Unauthorized returns an 401 error with the given error. -func Unauthorized(err error) error { - return NewError(http.StatusUnauthorized, err) -} - -// Forbidden returns an 403 error with the given error. -func Forbidden(err error) error { - return NewError(http.StatusForbidden, err) -} - -// NotFound returns an 404 error with the given error. -func NotFound(err error) error { - return NewError(http.StatusNotFound, err) -} - // WriteError writes to w a JSON representation of the given error. func WriteError(w http.ResponseWriter, err error) { switch k := err.(type) { @@ -118,10 +22,10 @@ func WriteError(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") } cause := errors.Cause(err) - if sc, ok := err.(StatusCoder); ok { + if sc, ok := err.(errs.StatusCoder); ok { w.WriteHeader(sc.StatusCode()) } else { - if sc, ok := cause.(StatusCoder); ok { + if sc, ok := cause.(errs.StatusCoder); ok { w.WriteHeader(sc.StatusCode()) } else { w.WriteHeader(http.StatusInternalServerError) @@ -134,12 +38,12 @@ func WriteError(w http.ResponseWriter, err error) { "error": err, }) if os.Getenv("STEPDEBUG") == "1" { - if e, ok := err.(StackTracer); ok { + if e, ok := err.(errs.StackTracer); ok { rl.WithFields(map[string]interface{}{ "stack-trace": fmt.Sprintf("%+v", e), }) } else { - if e, ok := cause.(StackTracer); ok { + if e, ok := cause.(errs.StackTracer); ok { rl.WithFields(map[string]interface{}{ "stack-trace": fmt.Sprintf("%+v", e), }) diff --git a/api/revoke.go b/api/revoke.go index aceb8305..df974cbe 100644 --- a/api/revoke.go +++ b/api/revoke.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" "golang.org/x/crypto/ocsp" ) @@ -29,13 +30,13 @@ type RevokeRequest struct { // or an error if something is wrong. func (r *RevokeRequest) Validate() (err error) { if r.Serial == "" { - return BadRequest(errors.New("missing serial")) + return errs.BadRequest(errors.New("missing serial")) } if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise { - return BadRequest(errors.New("reasonCode out of bounds")) + return errs.BadRequest(errors.New("reasonCode out of bounds")) } if !r.Passive { - return NotImplemented(errors.New("non-passive revocation not implemented")) + return errs.NotImplemented(errors.New("non-passive revocation not implemented")) } return @@ -49,7 +50,7 @@ func (r *RevokeRequest) Validate() (err error) { func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { var body RevokeRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } @@ -71,7 +72,7 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { if len(body.OTT) > 0 { logOtt(w, body.OTT) if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } opts.OTT = body.OTT @@ -80,12 +81,12 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { // the client certificate Serial Number must match the serial number // being revoked. if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { - WriteError(w, BadRequest(errors.New("missing ott or peer certificate"))) + WriteError(w, errs.BadRequest(errors.New("missing ott or peer certificate"))) return } opts.Crt = r.TLS.PeerCertificates[0] if opts.Crt.SerialNumber.String() != opts.Serial { - WriteError(w, BadRequest(errors.New("revoke: serial number in mtls certificate different than body"))) + WriteError(w, errs.BadRequest(errors.New("revoke: serial number in mtls certificate different than body"))) return } // TODO: should probably be checking if the certificate was revoked here. @@ -96,7 +97,7 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { } if err := h.Authority.Revoke(ctx, opts); err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } diff --git a/api/ssh.go b/api/ssh.go index 546c8f1e..f125a95a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "golang.org/x/crypto/ssh" @@ -248,19 +249,19 @@ type SSHBastionResponse struct { func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { var body SSHSignRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } logOtt(w, body.OTT) if err := body.Validate(); err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } publicKey, err := ssh.ParsePublicKey(body.PublicKey) if err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error parsing publicKey"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error parsing publicKey"))) return } @@ -268,7 +269,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { if body.AddUserPublicKey != nil { addUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey) if err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error parsing addUserPublicKey"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error parsing addUserPublicKey"))) return } } @@ -284,13 +285,13 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignSSHMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } cert, err := h.Authority.SignSSH(publicKey, opts, signOpts...) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } @@ -298,7 +299,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { if addUserPublicKey != nil && cert.CertType == ssh.UserCert && len(cert.ValidPrincipals) == 1 { addUserCert, err := h.Authority.SignSSHAddUser(addUserPublicKey, cert) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } addUserCertificate = &SSHCertificate{addUserCert} @@ -319,12 +320,12 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } certChain, err := h.Authority.Sign(cr, opts, signOpts...) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } identityCertificate = certChainToPEM(certChain) @@ -342,12 +343,12 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHRoots() if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { - WriteError(w, NotFound(errors.New("no keys found"))) + WriteError(w, errs.NotFound(errors.New("no keys found"))) return } @@ -367,12 +368,12 @@ func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHFederation() if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { - WriteError(w, NotFound(errors.New("no keys found"))) + WriteError(w, errs.NotFound(errors.New("no keys found"))) return } @@ -392,17 +393,17 @@ func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { var body SSHConfigRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } if err := body.Validate(); err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } ts, err := h.Authority.GetSSHConfig(body.Type, body.Data) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } @@ -413,7 +414,7 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { case provisioner.SSHHostCert: config.HostTemplates = ts default: - WriteError(w, InternalServerError(errors.New("it should hot get here"))) + WriteError(w, errs.InternalServerError(errors.New("it should hot get here"))) return } @@ -424,17 +425,17 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { var body SSHCheckPrincipalRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } if err := body.Validate(); err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } exists, err := h.Authority.CheckSSHHost(r.Context(), body.Principal, body.Token) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } JSON(w, &SSHCheckPrincipalResponse{ @@ -451,7 +452,7 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { hosts, err := h.Authority.GetSSHHosts(cert) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } JSON(w, &SSHGetHostsResponse{ @@ -463,17 +464,17 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) { var body SSHBastionRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } if err := body.Validate(); err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } bastion, err := h.Authority.GetSSHBastion(body.User, body.Hostname) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) return } diff --git a/api/sshRekey.go b/api/sshRekey.go index 234a6df5..6b7ef5d7 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "golang.org/x/crypto/ssh" ) @@ -38,36 +39,36 @@ type SSHRekeyResponse struct { func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { var body SSHRekeyRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } logOtt(w, body.OTT) if err := body.Validate(); err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } publicKey, err := ssh.ParsePublicKey(body.PublicKey) if err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error parsing publicKey"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error parsing publicKey"))) return } ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RekeySSHMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) } newCert, err := h.Authority.RekeySSH(oldCert, publicKey, signOpts...) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } diff --git a/api/sshRenew.go b/api/sshRenew.go index 4324ebba..5a847796 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" ) // SSHRenewRequest is the request body of an SSH certificate request. @@ -34,30 +35,30 @@ type SSHRenewResponse struct { func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { var body SSHRenewRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } logOtt(w, body.OTT) if err := body.Validate(); err != nil { - WriteError(w, BadRequest(err)) + WriteError(w, errs.BadRequest(err)) return } ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RenewSSHMethod) _, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT) if err != nil { - WriteError(w, InternalServerError(err)) + WriteError(w, errs.InternalServerError(err)) } newCert, err := h.Authority.RenewSSH(oldCert) if err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } diff --git a/api/sshRevoke.go b/api/sshRevoke.go index 9355e5a4..93e0e450 100644 --- a/api/sshRevoke.go +++ b/api/sshRevoke.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" "golang.org/x/crypto/ocsp" ) @@ -29,16 +30,16 @@ type SSHRevokeRequest struct { // or an error if something is wrong. func (r *SSHRevokeRequest) Validate() (err error) { if r.Serial == "" { - return BadRequest(errors.New("missing serial")) + return errs.BadRequest(errors.New("missing serial")) } if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise { - return BadRequest(errors.New("reasonCode out of bounds")) + return errs.BadRequest(errors.New("reasonCode out of bounds")) } if !r.Passive { - return NotImplemented(errors.New("non-passive revocation not implemented")) + return errs.NotImplemented(errors.New("non-passive revocation not implemented")) } if len(r.OTT) == 0 { - return BadRequest(errors.New("missing ott")) + return errs.BadRequest(errors.New("missing ott")) } return } @@ -49,7 +50,7 @@ func (r *SSHRevokeRequest) Validate() (err error) { func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { var body SSHRevokeRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) return } @@ -70,13 +71,13 @@ func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { // otherwise it is assumed that the certificate is revoking itself over mTLS. logOtt(w, body.OTT) if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { - WriteError(w, Unauthorized(err)) + WriteError(w, errs.Unauthorized(err)) return } opts.OTT = body.OTT if err := h.Authority.Revoke(ctx, opts); err != nil { - WriteError(w, Forbidden(err)) + WriteError(w, errs.Forbidden(err)) return } diff --git a/api/utils.go b/api/utils.go index 89adedb7..56beb2b5 100644 --- a/api/utils.go +++ b/api/utils.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" ) @@ -68,7 +69,7 @@ func JSONStatus(w http.ResponseWriter, v interface{}, status int) { // pointed by v. func ReadJSON(r io.Reader, v interface{}) error { if err := json.NewDecoder(r).Decode(v); err != nil { - return BadRequest(errors.Wrap(err, "error decoding json")) + return errs.BadRequest(errors.Wrap(err, "error decoding json")) } return nil } diff --git a/authority/ssh.go b/authority/ssh.go index fbf97545..8148a6bd 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/randutil" @@ -660,25 +661,19 @@ func (a *Authority) CheckSSHHost(ctx context.Context, principal string, token st if a.sshCheckHostFunc != nil { exists, err := a.sshCheckHostFunc(ctx, principal, token, a.GetRootCertificates()) if err != nil { - return false, &apiError{ - err: errors.Wrap(err, "checkSSHHost: error from injected checkSSHHost func"), - code: http.StatusInternalServerError, - } + return false, errs.Wrap(http.StatusInternalServerError, err, + "checkSSHHost: error from injected checkSSHHost func") } return exists, nil } exists, err := a.db.IsSSHHost(principal) if err != nil { if err == db.ErrNotImplemented { - return false, &apiError{ - err: errors.Wrap(err, "checkSSHHost: isSSHHost is not implemented"), - code: http.StatusNotImplemented, - } - } - return false, &apiError{ - err: errors.Wrap(err, "checkSSHHost: error checking if hosts exists"), - code: http.StatusInternalServerError, + return false, errs.Wrap(http.StatusNotImplemented, err, + "checkSSHHost: isSSHHost is not implemented") } + return false, errs.Wrap(http.StatusInternalServerError, err, + "checkSSHHost: error checking if hosts exists") } return exists, nil diff --git a/ca/client.go b/ca/client.go index a3bda21b..d42b1bd4 100644 --- a/ca/client.go +++ b/ca/client.go @@ -26,6 +26,7 @@ import ( "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca/identity" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/config" "github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/pemutil" @@ -134,7 +135,7 @@ func (o *clientOptions) applyDefaultIdentity() error { } crt, err := i.TLSCertificate() if err != nil { - return nil + return err } o.certificate = crt return nil @@ -472,11 +473,6 @@ func (c *Client) GetRootCAs() *x509.CertPool { } } -// GetTransport returns the transport of the internal HTTP client. -func (c *Client) GetTransport() http.RoundTripper { - return c.client.GetTransport() -} - // SetTransport updates the transport of the internal HTTP client. func (c *Client) SetTransport(tr http.RoundTripper) { c.client.SetTransport(tr) @@ -958,24 +954,27 @@ func (c *Client) SSHCheckHost(principal string, token string) (*api.SSHCheckPrin Token: token, }) if err != nil { - return nil, errors.Wrap(err, "error marshaling request") + return nil, errs.Wrap(http.StatusInternalServerError, err, + "error marshaling check-host request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"}) retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { - return nil, errors.Wrapf(err, "client POST %s failed", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client POST %s failed", u, + errs.WithMessage("Failed to perform POST request to %s", u)) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { retried = true goto retry } - return nil, readError(resp.Body) + + return nil, errs.StatusCodeError(resp.StatusCode, readError(resp.Body)) } var check api.SSHCheckPrincipalResponse if err := readJSON(resp.Body, &check); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "error reading %s response", u) } return &check, nil } @@ -1174,7 +1173,7 @@ func readJSON(r io.ReadCloser, v interface{}) error { func readError(r io.ReadCloser) error { defer r.Close() - apiErr := new(api.Error) + apiErr := new(errs.Error) if err := json.NewDecoder(r).Decode(apiErr); err != nil { return err } diff --git a/errs/error.go b/errs/error.go new file mode 100644 index 00000000..825cf549 --- /dev/null +++ b/errs/error.go @@ -0,0 +1,250 @@ +package errs + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" +) + +// StatusCoder interface is used by errors that returns the HTTP response code. +type StatusCoder interface { + StatusCode() int +} + +// StackTracer must be by those errors that return an stack trace. +type StackTracer interface { + StackTrace() errors.StackTrace +} + +// Option modifies the Error type. +type Option func(e *Error) error + +// WithMessage returns an Option that modifies the error by overwriting the +// message only if it is empty. +func WithMessage(format string, args ...interface{}) Option { + return func(e *Error) error { + if len(e.Msg) > 0 { + return e + } + e.Msg = fmt.Sprintf(format, args...) + return e + } +} + +// Error represents the CA API errors. +type Error struct { + Status int + Err error + Msg string +} + +// New returns a new Error. If the given error implements the StatusCoder +// interface we will ignore the given status. +func New(status int, err error, opts ...Option) error { + var e *Error + if sc, ok := err.(StatusCoder); ok { + e = &Error{Status: sc.StatusCode(), Err: err} + } else { + cause := errors.Cause(err) + if sc, ok := cause.(StatusCoder); ok { + e = &Error{Status: sc.StatusCode(), Err: err} + } else { + e = &Error{Status: status, Err: err} + } + } + for _, o := range opts { + o(e) + } + return e +} + +// ErrorResponse represents an error in JSON format. +type ErrorResponse struct { + Status int `json:"status"` + Message string `json:"message"` +} + +// Cause implements the errors.Causer interface and returns the original error. +func (e *Error) Cause() error { + return e.Err +} + +// Error implements the error interface and returns the error string. +func (e *Error) Error() string { + return e.Err.Error() +} + +// StatusCode implements the StatusCoder interface and returns the HTTP response +// code. +func (e *Error) StatusCode() int { + return e.Status +} + +// Message returns a user friendly error, if one is set. +func (e *Error) Message() string { + if len(e.Msg) > 0 { + return e.Msg + } + return e.Err.Error() +} + +// Wrap returns an error annotating err with a stack trace at the point Wrap is +// called, and the supplied message. If err is nil, Wrap returns nil. +func Wrap(status int, e error, m string, opts ...Option) error { + if e == nil { + return nil + } + if err, ok := e.(*Error); ok { + err.Err = errors.Wrap(err.Err, m) + e = err + } else { + e = errors.Wrap(e, m) + } + return StatusCodeError(status, e, opts...) +} + +// Wrapf returns an error annotating err with a stack trace at the point Wrap is +// called, and the supplied message. If err is nil, Wrap returns nil. +func Wrapf(status int, e error, format string, args ...interface{}) error { + if e == nil { + return nil + } + var opts []Option + for i, arg := range args { + // Once we find the first Option, assume that all further arguments are Options. + if _, ok := arg.(Option); ok { + for _, a := range args[i:] { + // Ignore any arguments after the first Option that are not Options. + if opt, ok := a.(Option); ok { + opts = append(opts, opt) + } + } + args = args[:i] + break + } + } + if err, ok := e.(*Error); ok { + err.Err = errors.Wrapf(err.Err, format, args...) + e = err + } else { + e = errors.Wrapf(e, format, args...) + } + return StatusCodeError(status, e, opts...) +} + +// MarshalJSON implements json.Marshaller interface for the Error struct. +func (e *Error) MarshalJSON() ([]byte, error) { + var msg string + if len(e.Msg) > 0 { + msg = e.Msg + } else { + msg = http.StatusText(e.Status) + } + return json.Marshal(&ErrorResponse{Status: e.Status, Message: msg}) +} + +// UnmarshalJSON implements json.Unmarshaler interface for the Error struct. +func (e *Error) UnmarshalJSON(data []byte) error { + var er ErrorResponse + if err := json.Unmarshal(data, &er); err != nil { + return err + } + e.Status = er.Status + e.Err = fmt.Errorf(er.Message) + return nil +} + +// Format implements the fmt.Formatter interface. +func (e *Error) Format(f fmt.State, c rune) { + if err, ok := e.Err.(fmt.Formatter); ok { + err.Format(f, c) + return + } + fmt.Fprint(f, e.Err.Error()) +} + +// Messenger is a friendly message interface that errors can implement. +type Messenger interface { + Message() string +} + +// StatusCodeError selects the proper error based on the status code. +func StatusCodeError(code int, e error, opts ...Option) error { + switch code { + case http.StatusBadRequest: + return BadRequest(e, opts...) + case http.StatusUnauthorized: + return Unauthorized(e, opts...) + case http.StatusForbidden: + return Forbidden(e, opts...) + case http.StatusInternalServerError: + return InternalServerError(e, opts...) + case http.StatusNotImplemented: + return NotImplemented(e, opts...) + default: + return UnexpectedError(code, e, opts...) + } +} + +var seeLogs = "Please see the certificate authority logs for more info." + +// InternalServerError returns a 500 error with the given error. +func InternalServerError(err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The certificate authority encountered an Internal Server Error. "+seeLogs)) + } + return New(http.StatusInternalServerError, err, opts...) +} + +// NotImplemented returns a 501 error with the given error. +func NotImplemented(err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The requested method is not implemented by the certificate authority. "+seeLogs)) + } + return New(http.StatusNotImplemented, err, opts...) +} + +// BadRequest returns an 400 error with the given error. +func BadRequest(err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The request could not be completed due to being poorly formatted or "+ + "missing critical data. "+seeLogs)) + } + return New(http.StatusBadRequest, err, opts...) +} + +// Unauthorized returns an 401 error with the given error. +func Unauthorized(err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The request lacked necessary authorization to be completed. "+seeLogs)) + } + return New(http.StatusUnauthorized, err, opts...) +} + +// Forbidden returns an 403 error with the given error. +func Forbidden(err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The request was Forbidden by the certificate authority. "+seeLogs)) + } + return New(http.StatusForbidden, err, opts...) +} + +// NotFound returns an 404 error with the given error. +func NotFound(err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The requested resource could not be found. "+seeLogs)) + } + return New(http.StatusNotFound, err, opts...) +} + +// UnexpectedError will be used when the certificate authority makes an outgoing +// request and receives an unhandled status code. +func UnexpectedError(code int, err error, opts ...Option) error { + if len(opts) == 0 { + opts = append(opts, WithMessage("The certificate authority received an "+ + "unexpected HTTP status code - '%d'. "+seeLogs, code)) + } + return New(code, err, opts...) +} diff --git a/go.mod b/go.mod index b8311236..c80084af 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/Masterminds/sprig/v3 v3.0.0 github.com/go-chi/chi v4.0.2+incompatible + github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 @@ -18,4 +19,4 @@ require ( gopkg.in/square/go-jose.v2 v2.4.0 ) -//replace github.com/smallstep/cli => ../cli +replace github.com/smallstep/cli => ../cli diff --git a/go.sum b/go.sum index 9e9246ba..2f65e71d 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -177,6 +179,8 @@ golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= From cb78a087d54f47cb4d10927062ef8f5b901e1032 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 14:30:18 -0800 Subject: [PATCH 107/163] Update cli dep --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c80084af..b0c1e80d 100644 --- a/go.mod +++ b/go.mod @@ -19,4 +19,4 @@ require ( gopkg.in/square/go-jose.v2 v2.4.0 ) -replace github.com/smallstep/cli => ../cli +//replace github.com/smallstep/cli => ../cli From e5a8629a218e53ab351c6111f832270827d70c49 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 14:31:22 -0800 Subject: [PATCH 108/163] updating dependencies --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index b0c1e80d..b8311236 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.13 require ( github.com/Masterminds/sprig/v3 v3.0.0 github.com/go-chi/chi v4.0.2+incompatible - github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 From 6200aeaad0d482f30c14af8e501f368fc46eda6a Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 14:39:08 -0800 Subject: [PATCH 109/163] cli dep update --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b8311236..51f0fe56 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385 + github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45 github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 diff --git a/go.sum b/go.sum index 2f65e71d..e56acbb0 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 h1:zU1JWNx/H github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407/go.mod h1:1DDxP5W6pSuPL7DudNMbr/qVVjToo8qz3tlRt8ka8TA= github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385 h1:5e1azRUUFvWaE0hCbbXxTKpzPmjjcQ4A/TotE6etaa4= github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385/go.mod h1:DC6mnMaYNejkAstQrMxkobEgB1QET7zwibISGoMSQis= +github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45 h1:ff5cEYAUCVGxQXaKezPQhqOKORFRIpZMmXQV6u+CwL0= +github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45/go.mod h1:6pTiWJKfIQcUYtK7lVnI0pOXRiYAWuy0qrlFVnn9q8M= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From aa58940582263aaa7cb98e3352f719af039cb379 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 15:53:37 -0800 Subject: [PATCH 110/163] Should be returning nil from applyIdentity if cert expired. --- ca/client.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ca/client.go b/ca/client.go index d42b1bd4..051bac5b 100644 --- a/ca/client.go +++ b/ca/client.go @@ -135,7 +135,7 @@ func (o *clientOptions) applyDefaultIdentity() error { } crt, err := i.TLSCertificate() if err != nil { - return err + return nil } o.certificate = crt return nil @@ -954,8 +954,8 @@ func (c *Client) SSHCheckHost(principal string, token string) (*api.SSHCheckPrin Token: token, }) if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, - "error marshaling check-host request") + return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request", + errs.WithMessage("Failed to marshal the check-host request")) } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"}) retry: @@ -974,7 +974,8 @@ retry: } var check api.SSHCheckPrincipalResponse if err := readJSON(resp.Body, &check); err != nil { - return nil, errs.Wrapf(http.StatusInternalServerError, err, "error reading %s response", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "error reading %s response", u, + errs.WithMessage("Failed to parse response from /ssh/check-host endpoint")) } return &check, nil } From 839fe6b952b01002b50b918b4ddfb7a47a591873 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 12:46:46 -0800 Subject: [PATCH 111/163] Add method to renew the identity. --- ca/identity/identity.go | 57 ++++++++++++++++++++ ca/identity/identity_test.go | 100 ++++++++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/ca/identity/identity.go b/ca/identity/identity.go index 48ed66e6..fa286a50 100644 --- a/ca/identity/identity.go +++ b/ca/identity/identity.go @@ -8,6 +8,7 @@ import ( "encoding/json" "encoding/pem" "io/ioutil" + "net/http" "os" "path/filepath" "strings" @@ -191,6 +192,62 @@ func (i *Identity) TLSCertificate() (tls.Certificate, error) { } } +// Renewer is that interface that a renew client must implement. +type Renewer interface { + GetRootCAs() *x509.CertPool + Renew(tr http.RoundTripper) (*api.SignResponse, error) +} + +// Renew renews the current identity certificate using a client with a renew +// method. +func (i *Identity) Renew(client Renewer) error { + switch i.Kind() { + case Disabled: + return nil + case MutualTLS: + cert, err := i.TLSCertificate() + if err != nil { + return err + } + + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: client.GetRootCAs(), + PreferServerCipherSuites: true, + } + + sign, err := client.Renew(tr) + if err != nil { + return err + } + + if sign.CertChainPEM == nil || len(sign.CertChainPEM) == 0 { + sign.CertChainPEM = []api.Certificate{sign.ServerPEM, sign.CaPEM} + } + + // Write certificate + buf := new(bytes.Buffer) + for _, crt := range sign.CertChainPEM { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + } + if err := pem.Encode(buf, block); err != nil { + return errors.Wrap(err, "error encoding identity certificate") + } + } + certFilename := filepath.Join(identityDir, "identity.crt") + if err := ioutil.WriteFile(certFilename, buf.Bytes(), 0600); err != nil { + return errors.Wrap(err, "error writing identity certificate") + } + + return nil + default: + return errors.Errorf("unsupported identity type %s", i.Type) + } +} + func fileExists(filename string) error { info, err := os.Stat(filename) if err != nil { diff --git a/ca/identity/identity_test.go b/ca/identity/identity_test.go index 1a73afdb..3c04f982 100644 --- a/ca/identity/identity_test.go +++ b/ca/identity/identity_test.go @@ -3,15 +3,17 @@ package identity import ( "crypto" "crypto/tls" + "crypto/x509" + "fmt" "io/ioutil" + "net/http" "os" "path/filepath" "reflect" "testing" - "github.com/smallstep/cli/crypto/pemutil" - "github.com/smallstep/certificates/api" + "github.com/smallstep/cli/crypto/pemutil" ) func TestLoadDefaultIdentity(t *testing.T) { @@ -252,3 +254,97 @@ func TestWriteDefaultIdentity(t *testing.T) { }) } } + +type renewer struct { + pool *x509.CertPool + sign *api.SignResponse + err error +} + +func (r *renewer) GetRootCAs() *x509.CertPool { + return r.pool +} + +func (r *renewer) Renew(tr http.RoundTripper) (*api.SignResponse, error) { + return r.sign, r.err +} + +func TestIdentity_Renew(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "go-tests") + if err != nil { + t.Fatal(err) + } + + oldIdentityDir := identityDir + defer func() { + identityDir = oldIdentityDir + os.RemoveAll(tmpDir) + }() + + certs, err := pemutil.ReadCertificateBundle("testdata/identity/identity.crt") + if err != nil { + t.Fatal(err) + } + + ok := &renewer{ + sign: &api.SignResponse{ + ServerPEM: api.Certificate{Certificate: certs[0]}, + CaPEM: api.Certificate{Certificate: certs[1]}, + CertChainPEM: []api.Certificate{ + {Certificate: certs[0]}, + {Certificate: certs[1]}, + }, + }, + } + + okOld := &renewer{ + sign: &api.SignResponse{ + ServerPEM: api.Certificate{Certificate: certs[0]}, + CaPEM: api.Certificate{Certificate: certs[1]}, + }, + } + + fail := &renewer{ + err: fmt.Errorf("an error"), + } + + type fields struct { + Type string + Certificate string + Key string + } + type args struct { + client Renewer + } + tests := []struct { + name string + prepare func() + fields fields + args args + wantErr bool + }{ + {"ok", func() {}, fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{ok}, false}, + {"ok old", func() {}, fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{okOld}, false}, + {"ok disabled", func() {}, fields{}, args{nil}, false}, + {"fail type", func() {}, fields{"foo", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{ok}, true}, + {"fail renew", func() {}, fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{fail}, true}, + {"fail certificate", func() {}, fields{"mTLS", "testdata/certs/server.crt", "testdata/identity/identity_key"}, args{ok}, true}, + {"fail write identity", func() { + identityDir = filepath.Join(tmpDir, "bad-dir") + os.MkdirAll(identityDir, 0600) + }, fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{ok}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + i := &Identity{ + Type: tt.fields.Type, + Certificate: tt.fields.Certificate, + Key: tt.fields.Key, + } + if err := i.Renew(tt.args.client); (err != nil) != tt.wantErr { + t.Errorf("Identity.Renew() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From 0b5d37b284d1e80750fd9e9bedd94cc0c2f62ec6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:39:01 -0800 Subject: [PATCH 112/163] Add method to just write the identity certificate. --- ca/identity/identity.go | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/ca/identity/identity.go b/ca/identity/identity.go index fa286a50..d6aee85b 100644 --- a/ca/identity/identity.go +++ b/ca/identity/identity.go @@ -81,22 +81,12 @@ func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) er keyFilename := filepath.Join(identityDir, "identity_key") // Write certificate - buf := new(bytes.Buffer) - for _, crt := range certChain { - block := &pem.Block{ - Type: "CERTIFICATE", - Bytes: crt.Raw, - } - if err := pem.Encode(buf, block); err != nil { - return errors.Wrap(err, "error encoding identity certificate") - } - } - if err := ioutil.WriteFile(certFilename, buf.Bytes(), 0600); err != nil { - return errors.Wrap(err, "error writing identity certificate") + if err := WriteIdentityCertificate(certChain); err != nil { + return err } // Write key - buf.Reset() + buf := new(bytes.Buffer) block, err := pemutil.Serialize(key) if err != nil { return err @@ -126,6 +116,27 @@ func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) er return nil } +// WriteIdentityCertificate writes the identity certificate in disk. +func WriteIdentityCertificate(certChain []api.Certificate) error { + buf := new(bytes.Buffer) + certFilename := filepath.Join(identityDir, "identity.crt") + for _, crt := range certChain { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + } + if err := pem.Encode(buf, block); err != nil { + return errors.Wrap(err, "error encoding identity certificate") + } + } + + if err := ioutil.WriteFile(certFilename, buf.Bytes(), 0600); err != nil { + return errors.Wrap(err, "error writing identity certificate") + } + + return nil +} + // Kind returns the type for the given identity. func (i *Identity) Kind() Type { switch strings.ToLower(i.Type) { From a6deea7d8d0a9e50e2a44e62d9a25ead29902c33 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:43:38 -0800 Subject: [PATCH 113/163] Renew identity certificate in /ssh/rekey and /ssh/renew --- api/sshRekey.go | 14 +++++++++++--- api/sshRenew.go | 26 ++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/api/sshRekey.go b/api/sshRekey.go index 6b7ef5d7..aa70cf4f 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -30,7 +30,8 @@ func (s *SSHRekeyRequest) Validate() error { // SSHRekeyResponse is the response object that returns the SSH certificate. type SSHRekeyResponse struct { - Certificate SSHCertificate `json:"crt"` + Certificate SSHCertificate `json:"crt"` + IdentityCertificate []Certificate `json:"identityCrt,omitempty"` } // SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token @@ -72,7 +73,14 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { return } - JSONStatus(w, &SSHSignResponse{ - Certificate: SSHCertificate{newCert}, + identity, err := h.renewIdentityCertificate(r) + if err != nil { + WriteError(w, errs.Forbidden(err)) + return + } + + JSONStatus(w, &SSHRekeyResponse{ + Certificate: SSHCertificate{newCert}, + IdentityCertificate: identity, }, http.StatusCreated) } diff --git a/api/sshRenew.go b/api/sshRenew.go index 5a847796..5165bf33 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -26,7 +26,8 @@ func (s *SSHRenewRequest) Validate() error { // SSHRenewResponse is the response object that returns the SSH certificate. type SSHRenewResponse struct { - Certificate SSHCertificate `json:"crt"` + Certificate SSHCertificate `json:"crt"` + IdentityCertificate []Certificate `json:"identityCrt,omitempty"` } // SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token @@ -62,7 +63,28 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { return } + identity, err := h.renewIdentityCertificate(r) + if err != nil { + WriteError(w, errs.Forbidden(err)) + return + } + JSONStatus(w, &SSHSignResponse{ - Certificate: SSHCertificate{newCert}, + Certificate: SSHCertificate{newCert}, + IdentityCertificate: identity, }, http.StatusCreated) } + +// renewIdentityCertificate request the client TLS certificate if present. +func (h *caHandler) renewIdentityCertificate(r *http.Request) ([]Certificate, error) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + return nil, nil + } + + certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) + if err != nil { + return nil, err + } + + return certChainToPEM(certChain), nil +} From eeabf5ba4cdda80109aa558722bfe49824981bc7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:44:08 -0800 Subject: [PATCH 114/163] Fix tests. --- api/errors_test.go | 12 +++++++----- api/revoke_test.go | 13 +++++++------ api/utils_test.go | 3 ++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/api/errors_test.go b/api/errors_test.go index a252e4c3..1f63142a 100644 --- a/api/errors_test.go +++ b/api/errors_test.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" "testing" + + "github.com/smallstep/certificates/errs" ) func TestError_MarshalJSON(t *testing.T) { @@ -22,7 +24,7 @@ func TestError_MarshalJSON(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := &Error{ + e := &errs.Error{ Status: tt.fields.Status, Err: tt.fields.Err, } @@ -45,15 +47,15 @@ func TestError_UnmarshalJSON(t *testing.T) { tests := []struct { name string args args - expected *Error + expected *errs.Error wantErr bool }{ - {"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &Error{Status: 400, Err: fmt.Errorf("bad request")}, false}, - {"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &Error{}, true}, + {"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &errs.Error{Status: 400, Err: fmt.Errorf("bad request")}, false}, + {"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &errs.Error{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := new(Error) + e := new(errs.Error) if err := e.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr { t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/api/revoke_test.go b/api/revoke_test.go index 9aa37d1a..e6aef11a 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -16,18 +16,19 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" ) func TestRevokeRequestValidate(t *testing.T) { type test struct { rr *RevokeRequest - err *Error + err *errs.Error } tests := map[string]test{ "error/missing serial": { rr: &RevokeRequest{}, - err: &Error{Err: errors.New("missing serial"), Status: http.StatusBadRequest}, + err: &errs.Error{Err: errors.New("missing serial"), Status: http.StatusBadRequest}, }, "error/bad reasonCode": { rr: &RevokeRequest{ @@ -35,7 +36,7 @@ func TestRevokeRequestValidate(t *testing.T) { ReasonCode: 15, Passive: true, }, - err: &Error{Err: errors.New("reasonCode out of bounds"), Status: http.StatusBadRequest}, + err: &errs.Error{Err: errors.New("reasonCode out of bounds"), Status: http.StatusBadRequest}, }, "error/non-passive not implemented": { rr: &RevokeRequest{ @@ -43,7 +44,7 @@ func TestRevokeRequestValidate(t *testing.T) { ReasonCode: 8, Passive: false, }, - err: &Error{Err: errors.New("non-passive revocation not implemented"), Status: http.StatusNotImplemented}, + err: &errs.Error{Err: errors.New("non-passive revocation not implemented"), Status: http.StatusNotImplemented}, }, "ok": { rr: &RevokeRequest{ @@ -57,7 +58,7 @@ func TestRevokeRequestValidate(t *testing.T) { t.Run(name, func(t *testing.T) { if err := tc.rr.Validate(); err != nil { switch v := err.(type) { - case *Error: + case *errs.Error: assert.HasPrefix(t, v.Error(), tc.err.Error()) assert.Equals(t, v.StatusCode(), tc.err.Status) default: @@ -189,7 +190,7 @@ func Test_caHandler_Revoke(t *testing.T) { return nil, nil }, revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { - return InternalServerError(errors.New("force")) + return errs.InternalServerError(errors.New("force")) }, }, } diff --git a/api/utils_test.go b/api/utils_test.go index 95ff89d0..81146653 100644 --- a/api/utils_test.go +++ b/api/utils_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" ) @@ -108,7 +109,7 @@ func TestReadJSON(t *testing.T) { t.Errorf("ReadJSON() error = %v, wantErr %v", err, tt.wantErr) } if tt.wantErr { - e, ok := err.(*Error) + e, ok := err.(*errs.Error) if ok { if code := e.StatusCode(); code != 400 { t.Errorf("error.StatusCode() = %v, wants 400", code) From 1fa35491ea070cf96f26ca0996851afd1f667d2f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:44:59 -0800 Subject: [PATCH 115/163] Update cli dependency. --- go.mod | 2 +- go.sum | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 51f0fe56..5da04acb 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 - github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45 + github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838 github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 diff --git a/go.sum b/go.sum index e56acbb0..8053807c 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,7 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go. github.com/smallstep/certificates v0.14.0-rc.1.0.20191126035953-e88034bea402/go.mod h1:r2UTcAZNriKlwvNNXymNAcF3iKL6mTYOYrOCtBYYGJU= github.com/smallstep/certificates v0.14.0-rc.1.0.20191210005525-50152391a397/go.mod h1:8leACUXHFo0JVm9YcrcX09aar2H8hz1BAWxD1D/GpsU= github.com/smallstep/certificates v0.14.0-rc.1.0.20191213215656-d2100821138c/go.mod h1:HMXt9hWBm7M7ZrUy0uZ/T/077te2x9bnXZCxrdVsBf4= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191217235337-aa5894058226/go.mod h1:MTKifeJBe1B/dzH5NDoPFpIPaWD0MzRozzONVkF8egc= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= @@ -128,10 +129,10 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1 h1:yAB5yZI+i github.com/smallstep/cli v0.14.0-rc.1.0.20191127025104-2821b0b811c1/go.mod h1:F6/cZ7VguiUV4nsoqPdDyZtGOgg3oLHz+LstEQsiSAg= github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407 h1:zU1JWNx/Hm518TE2VgfIa0RkaeH9Av3WsZw5OlDbZyI= github.com/smallstep/cli v0.14.0-rc.1.0.20191211225301-a5e848783407/go.mod h1:1DDxP5W6pSuPL7DudNMbr/qVVjToo8qz3tlRt8ka8TA= -github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385 h1:5e1azRUUFvWaE0hCbbXxTKpzPmjjcQ4A/TotE6etaa4= -github.com/smallstep/cli v0.14.0-rc.1.0.20191213215810-4a01db0b2385/go.mod h1:DC6mnMaYNejkAstQrMxkobEgB1QET7zwibISGoMSQis= github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45 h1:ff5cEYAUCVGxQXaKezPQhqOKORFRIpZMmXQV6u+CwL0= github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45/go.mod h1:6pTiWJKfIQcUYtK7lVnI0pOXRiYAWuy0qrlFVnn9q8M= +github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838 h1:UZG/5HqHZbMRZk1KcBkH8HPgH8HSDtQ2jBb9hLFwpT8= +github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838/go.mod h1:JPG34JrC37Pw0HjoB+cAtXT1yFOXfab/5nrM7ZTSw8c= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From e67ccd9e3d71a3e82a970358b032ecc10cc343c2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 2 Jan 2020 17:48:28 -0800 Subject: [PATCH 116/163] Add fault tolerance against clock skew accross system on TLS certificates. --- authority/config.go | 21 +++++++++++++++++---- authority/provisioner/sign_options.go | 27 +++++++++++++++++++++------ authority/tls.go | 13 ++++++++++--- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/authority/config.go b/authority/config.go index 8cc742e4..75f55a12 100644 --- a/authority/config.go +++ b/authority/config.go @@ -28,6 +28,7 @@ var ( MaxVersion: 1.2, Renegotiation: false, } + defaultBackdate = time.Minute defaultDisableRenewal = false defaultEnableSSHCA = false globalProvisionerClaims = provisioner.Claims{ @@ -65,10 +66,11 @@ type Config struct { // AuthConfig represents the configuration options for the authority. type AuthConfig struct { - Provisioners provisioner.List `json:"provisioners"` - Template *x509util.ASN1DN `json:"template,omitempty"` - Claims *provisioner.Claims `json:"claims,omitempty"` - DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"` + Provisioners provisioner.List `json:"provisioners"` + Template *x509util.ASN1DN `json:"template,omitempty"` + Claims *provisioner.Claims `json:"claims,omitempty"` + DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"` + Backdate *provisioner.Duration `json:"backdate,omitempty"` } // Validate validates the authority configuration. @@ -91,6 +93,17 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { if c.Template == nil { c.Template = &x509util.ASN1DN{} } + + if c.Backdate != nil { + if c.Backdate.Duration < 0 { + return errors.New("authority.backdate cannot be less than 0") + } + } else { + c.Backdate = &provisioner.Duration{ + Duration: defaultBackdate, + } + } + return nil } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 53921a3c..ddc985e3 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -15,10 +15,12 @@ import ( "golang.org/x/crypto/ed25519" ) -// Options contains the options that can be passed to the Sign method. +// Options contains the options that can be passed to the Sign method. Backdate +// is automatically filled and can only be configured in the CA. type Options struct { - NotAfter TimeDuration `json:"notAfter"` - NotBefore TimeDuration `json:"notBefore"` + NotAfter TimeDuration `json:"notAfter"` + NotBefore TimeDuration `json:"notBefore"` + Backdate time.Duration `json:"-"` } // SignOption is the interface used to collect all extra options used in the @@ -189,12 +191,22 @@ func (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error { type profileDefaultDuration time.Duration func (v profileDefaultDuration) Option(so Options) x509util.WithOption { + var backdate time.Duration notBefore := so.NotBefore.Time() if notBefore.IsZero() { notBefore = time.Now() + backdate = -1 * so.Backdate } notAfter := so.NotAfter.RelativeTime(notBefore) - return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v)) + return func(p x509util.Profile) error { + fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v)) + if err := fn(p); err != nil { + return err + } + crt := p.Subject() + crt.NotBefore = crt.NotBefore.Add(backdate) + return nil + } } // profileLimitDuration is an x509 profile option that modifies an x509 validity @@ -208,10 +220,12 @@ type profileLimitDuration struct { // certificate to one that is superficially imposed. func (v profileLimitDuration) Option(so Options) x509util.WithOption { return func(p x509util.Profile) error { + var backdate time.Duration n := now() notBefore := so.NotBefore.Time() if notBefore.IsZero() { notBefore = n + backdate = -1 * so.Backdate } if notBefore.After(v.notAfter) { return errors.Errorf("provisioning credential expiration (%s) is before "+ @@ -232,7 +246,7 @@ func (v profileLimitDuration) Option(so Options) x509util.WithOption { } } crt := p.Subject() - crt.NotBefore = notBefore + crt.NotBefore = notBefore.Add(backdate) crt.NotAfter = notAfter return nil } @@ -255,9 +269,10 @@ func (v *validityValidator) Valid(crt *x509.Certificate) error { var ( na = crt.NotAfter nb = crt.NotBefore - d = na.Sub(nb) now = time.Now() ) + // Get duration from to not take into account the backdate. + var d = na.Sub(now) if na.Before(now) { return errors.Errorf("NotAfter: %v cannot be in the past", na) diff --git a/authority/tls.go b/authority/tls.go index 0dd4f323..eb7cb86a 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -65,6 +65,10 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti certValidators = []provisioner.CertificateValidator{} issIdentity = a.intermediateIdentity ) + + // Set backdate with the configured value + signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration + for _, op := range extraOpts { switch k := op.(type) { case provisioner.CertificateValidator: @@ -136,14 +140,17 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error // Issuer issIdentity := a.intermediateIdentity - now := time.Now().UTC() + // Durations + backdate := a.config.AuthorityConfig.Backdate.Duration duration := oldCert.NotAfter.Sub(oldCert.NotBefore) + now := time.Now().UTC() + newCert := &x509.Certificate{ PublicKey: oldCert.PublicKey, Issuer: issIdentity.Crt.Subject, Subject: oldCert.Subject, - NotBefore: now, - NotAfter: now.Add(duration), + NotBefore: now.Add(-1 * backdate), + NotAfter: now.Add(duration - backdate), KeyUsage: oldCert.KeyUsage, UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions, ExtKeyUsage: oldCert.ExtKeyUsage, From 50717b3ffa0d50f31b5d6a85c20c9aec9c7b91c4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 13:27:45 -0800 Subject: [PATCH 117/163] Update assert package. --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5da04acb..407e6164 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/errors v0.8.1 github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 - github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 + github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838 github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a diff --git a/go.sum b/go.sum index 8053807c..c200eeb8 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= +github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 h1:kSImCuenAkXtCaBeQ1UhmzzJGRhSm8sVH7I3sHE2Qdg= +github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/certificates v0.14.0-rc.1.0.20191023014154-4669bef8c700/go.mod h1:/WOAB2LkcjkEbKG5rDol+A22Lp3UsttkLPLkY7tVtuk= github.com/smallstep/certificates v0.14.0-rc.1.0.20191025192352-8ef9b020ed24/go.mod h1:043iBnsMvNhQ+QFwSh0N6JR3H2yamHPPAc78vCf+8Tc= github.com/smallstep/certificates v0.14.0-rc.1.0.20191126035953-e88034bea402/go.mod h1:r2UTcAZNriKlwvNNXymNAcF3iKL6mTYOYrOCtBYYGJU= From 76c14560b0dd2c44822e1fdd192ef3a03b8523b9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 17:41:16 -0800 Subject: [PATCH 118/163] Use errs package for HTTP errors. --- ca/client_test.go | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/ca/client_test.go b/ca/client_test.go index 7a36a64b..c2e0063e 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -16,6 +16,8 @@ import ( "testing" "time" + "github.com/smallstep/certificates/errs" + "github.com/smallstep/assert" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" @@ -152,8 +154,8 @@ func equalJSON(t *testing.T, a interface{}, b interface{}) bool { func TestClient_Version(t *testing.T) { ok := &api.VersionResponse{Version: "test"} - internal := api.InternalServerError(fmt.Errorf("Internal Server Error")) - notFound := api.NotFound(fmt.Errorf("Not Found")) + internal := errs.InternalServerError(fmt.Errorf("Internal Server Error")) + notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string @@ -207,7 +209,7 @@ func TestClient_Version(t *testing.T) { func TestClient_Health(t *testing.T) { ok := &api.HealthResponse{Status: "ok"} - nok := api.InternalServerError(fmt.Errorf("Internal Server Error")) + nok := errs.InternalServerError(fmt.Errorf("Internal Server Error")) tests := []struct { name string @@ -262,7 +264,7 @@ func TestClient_Root(t *testing.T) { ok := &api.RootResponse{ RootPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, } - notFound := api.NotFound(fmt.Errorf("Not Found")) + notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string @@ -332,8 +334,8 @@ func TestClient_Sign(t *testing.T) { NotBefore: api.NewTimeDuration(time.Now()), NotAfter: api.NewTimeDuration(time.Now().AddDate(0, 1, 0)), } - unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) + badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -407,8 +409,8 @@ func TestClient_Revoke(t *testing.T) { OTT: "the-ott", ReasonCode: 4, } - unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) + badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -483,8 +485,8 @@ func TestClient_Renew(t *testing.T) { {Certificate: parseCertificate(rootPEM)}, }, } - unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) + badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -541,7 +543,7 @@ func TestClient_Provisioners(t *testing.T) { ok := &api.ProvisionersResponse{ Provisioners: provisioner.List{}, } - internalServerError := api.InternalServerError(fmt.Errorf("Internal Server Error")) + internalServerError := errs.InternalServerError(fmt.Errorf("Internal Server Error")) tests := []struct { name string @@ -603,7 +605,7 @@ func TestClient_ProvisionerKey(t *testing.T) { ok := &api.ProvisionerKeyResponse{ Key: "an encrypted key", } - notFound := api.NotFound(fmt.Errorf("Not Found")) + notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string @@ -664,8 +666,8 @@ func TestClient_Roots(t *testing.T) { {Certificate: parseCertificate(rootPEM)}, }, } - unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) + badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -724,8 +726,8 @@ func TestClient_Federation(t *testing.T) { {Certificate: parseCertificate(rootPEM)}, }, } - unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) + badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -788,7 +790,7 @@ func TestClient_SSHRoots(t *testing.T) { HostKeys: []api.SSHPublicKey{{PublicKey: key}}, UserKeys: []api.SSHPublicKey{{PublicKey: key}}, } - notFound := api.NotFound(fmt.Errorf("Not Found")) + notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string @@ -879,7 +881,7 @@ func Test_parseEndpoint(t *testing.T) { func TestClient_RootFingerprint(t *testing.T) { ok := &api.HealthResponse{Status: "ok"} - nok := api.InternalServerError(fmt.Errorf("Internal Server Error")) + nok := errs.InternalServerError(fmt.Errorf("Internal Server Error")) httpsServer := httptest.NewTLSServer(nil) defer httpsServer.Close() @@ -946,7 +948,7 @@ func TestClient_SSHBastion(t *testing.T) { Hostname: "bastion.local", }, } - badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string From 64e0a2ca6f291c7d52f40b3e3298667bf9b6eba5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 18:16:45 -0800 Subject: [PATCH 119/163] Disable backdata on ca tests. --- ca/testdata/ca.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ca/testdata/ca.json b/ca/testdata/ca.json index f29f24c6..b094c02e 100644 --- a/ca/testdata/ca.json +++ b/ca/testdata/ca.json @@ -18,6 +18,7 @@ ] }, "authority": { + "backdate": "0s", "provisioners": [ { "name": "max", From 935d0d454244e434941f2f07bedc679ef13b6c71 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 18:22:02 -0800 Subject: [PATCH 120/163] Add support for backdate to SSH certificates. --- authority/provisioner/aws.go | 2 +- authority/provisioner/azure.go | 4 +- authority/provisioner/claims.go | 16 ++ authority/provisioner/claims_test.go | 51 ++++++ authority/provisioner/gcp.go | 2 +- authority/provisioner/jwk.go | 3 +- authority/provisioner/k8sSA.go | 6 +- authority/provisioner/oidc.go | 2 +- authority/provisioner/sign_options.go | 17 +- authority/provisioner/sign_ssh_options.go | 149 +++++++++++------- .../provisioner/sign_ssh_options_test.go | 71 +++++++-- authority/provisioner/utils_test.go | 2 +- authority/provisioner/x5c.go | 3 +- authority/provisioner/x5c_test.go | 4 +- authority/ssh.go | 12 +- 15 files changed, 259 insertions(+), 85 deletions(-) create mode 100644 authority/provisioner/claims_test.go diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index a58ffb7e..74fa3a1f 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -469,7 +469,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Set the default extensions. &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. - sshDefaultValidityModifier(p.claimer), + &sshDefaultDuration{p.claimer}, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 5e338e18..998ef6e1 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -209,7 +209,7 @@ func (p *Azure) Init(config Config) (err error) { return nil } -// parseToken returuns the claims, name, group, error. +// parseToken returns the claims, name, group, error. func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) { jwt, err := jose.ParseSigned(token) if err != nil { @@ -335,7 +335,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Set the default extensions. &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. - sshDefaultValidityModifier(p.claimer), + &sshDefaultDuration{p.claimer}, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. diff --git a/authority/provisioner/claims.go b/authority/provisioner/claims.go index 4eba5ad7..997d9ba3 100644 --- a/authority/provisioner/claims.go +++ b/authority/provisioner/claims.go @@ -4,6 +4,7 @@ import ( "time" "github.com/pkg/errors" + "golang.org/x/crypto/ssh" ) // Claims so that individual provisioners can override global claims. @@ -95,6 +96,21 @@ func (c *Claimer) IsDisableRenewal() bool { return *c.claims.DisableRenewal } +// DefaultSSHCertDuration returns the default SSH certificate duration for the +// given certificate type. +func (c *Claimer) DefaultSSHCertDuration(certType uint32) (time.Duration, error) { + switch certType { + case ssh.UserCert: + return c.DefaultUserSSHCertDuration(), nil + case ssh.HostCert: + return c.DefaultHostSSHCertDuration(), nil + case 0: + return 0, errors.New("ssh certificate type has not been set") + default: + return 0, errors.Errorf("ssh certificate has an unknown type: %d", certType) + } +} + // DefaultUserSSHCertDuration returns the default SSH user cert duration for the // provisioner. If the default is not set within the provisioner, then the // global default from the authority configuration will be used. diff --git a/authority/provisioner/claims_test.go b/authority/provisioner/claims_test.go new file mode 100644 index 00000000..d4794d3c --- /dev/null +++ b/authority/provisioner/claims_test.go @@ -0,0 +1,51 @@ +package provisioner + +import ( + "testing" + "time" + + "golang.org/x/crypto/ssh" +) + +func TestClaimer_DefaultSSHCertDuration(t *testing.T) { + duration := Duration{ + Duration: time.Hour, + } + type fields struct { + global Claims + claims *Claims + } + type args struct { + certType uint32 + } + tests := []struct { + name string + fields fields + args args + want time.Duration + wantErr bool + }{ + {"user", fields{globalProvisionerClaims, &Claims{DefaultUserSSHDur: &duration}}, args{1}, time.Hour, false}, + {"user global", fields{globalProvisionerClaims, nil}, args{ssh.UserCert}, 16 * time.Hour, false}, + {"host global", fields{globalProvisionerClaims, &Claims{DefaultHostSSHDur: &duration}}, args{2}, time.Hour, false}, + {"host global", fields{globalProvisionerClaims, nil}, args{ssh.HostCert}, 30 * 24 * time.Hour, false}, + {"invalid", fields{globalProvisionerClaims, nil}, args{0}, 0, true}, + {"invalid global", fields{globalProvisionerClaims, nil}, args{3}, 0, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Claimer{ + global: tt.fields.global, + claims: tt.fields.claims, + } + got, err := c.DefaultSSHCertDuration(tt.args.certType) + if (err != nil) != tt.wantErr { + t.Errorf("Claimer.DefaultSSHCertDuration() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Claimer.DefaultSSHCertDuration() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 30a65909..bc531e92 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -378,7 +378,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Set the default extensions &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. - sshDefaultValidityModifier(p.claimer), + &sshDefaultDuration{p.claimer}, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 231b1580..b5add3f4 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -188,6 +188,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, if claims.Step == nil || claims.Step.SSH == nil { return nil, errors.New("authorization token must be an SSH provisioning token") } + opts := claims.Step.SSH signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token @@ -222,7 +223,7 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Set the default extensions. &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. - sshDefaultValidityModifier(p.claimer), + &sshDefaultDuration{p.claimer}, // Validate that the keyID is equivalent to the token subject. sshCertKeyIDValidator(claims.Subject), // Validate public key diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 0c90552c..e7d45236 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -235,13 +235,15 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio } // Default to a user certificate with no principals if not set - signOptions := []SignOption{sshCertificateDefaultsModifier{CertType: SSHUserCert}} + signOptions := []SignOption{ + sshCertificateDefaultsModifier{CertType: SSHUserCert}, + } return append(signOptions, // Set the default extensions. &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. - sshDefaultValidityModifier(p.claimer), + &sshDefaultDuration{p.claimer}, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 4538ef81..4c4b68d2 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -360,7 +360,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Set the default extensions &sshDefaultExtensionModifier{}, // Set the validity bounds if not set. - sshDefaultValidityModifier(o.claimer), + &sshDefaultDuration{o.claimer}, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index ddc985e3..2c3cfa16 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -267,12 +267,19 @@ func newValidityValidator(min, max time.Duration) *validityValidator { // and total duration. func (v *validityValidator) Valid(crt *x509.Certificate) error { var ( - na = crt.NotAfter - nb = crt.NotBefore - now = time.Now() + na = crt.NotAfter.Truncate(time.Second) + nb = crt.NotBefore.Truncate(time.Second) + now = time.Now().Truncate(time.Second) ) - // Get duration from to not take into account the backdate. - var d = na.Sub(now) + + // To not take into account the backdate, time.Now() will be used to + // calculate the duration if NotBefore is in the past. + var d time.Duration + if now.After(nb) { + d = na.Sub(now) + } else { + d = na.Sub(nb) + } if na.Before(now) { return errors.Errorf("NotAfter: %v cannot be in the past", na) diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index ceb57105..f5ad8662 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -46,13 +46,22 @@ type SSHCertificateOptionsValidator interface { Valid(got SSHOptions) error } +// sshModifierFunc is an adapter to allow the use of ordinary functions as SSH +// certificate modifiers. +type sshModifierFunc func(cert *ssh.Certificate) error + +func (f sshModifierFunc) Modify(cert *ssh.Certificate) error { + return f(cert) +} + // SSHOptions contains the options that can be passed to the SignSSH method. type SSHOptions struct { - CertType string `json:"certType"` - KeyID string `json:"keyID"` - Principals []string `json:"principals"` - ValidAfter TimeDuration `json:"validAfter,omitempty"` - ValidBefore TimeDuration `json:"validBefore,omitempty"` + CertType string `json:"certType"` + KeyID string `json:"keyID"` + Principals []string `json:"principals"` + ValidAfter TimeDuration `json:"validAfter,omitempty"` + ValidBefore TimeDuration `json:"validBefore,omitempty"` + Backdate time.Duration `json:"-"` } // Type returns the uint32 representation of the CertType. @@ -199,67 +208,92 @@ func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate) error { } } -// sshValidityModifier is an SSHCertificateModifier that checks the -// validity bounds, setting them if they are not provided. It will fail if a +// sshDefaultDuration is an SSHCertificateModifier that sets the certificate +// ValidAfter and ValidBefore if they have not been set. It will fail if a // CertType has not been set or is not valid. -type sshValidityModifier struct { +type sshDefaultDuration struct { *Claimer - validBefore time.Time } -func (m *sshValidityModifier) Modify(cert *ssh.Certificate) error { - var d time.Duration +func (m *sshDefaultDuration) Option(o SSHOptions) SSHCertificateModifier { + return sshModifierFunc(func(cert *ssh.Certificate) error { + d, err := m.DefaultSSHCertDuration(cert.CertType) + if err != nil { + return err + } - switch cert.CertType { - case ssh.UserCert: - d = m.DefaultUserSSHCertDuration() - case ssh.HostCert: - d = m.DefaultHostSSHCertDuration() - case 0: - return errors.New("ssh certificate type has not been set") - default: - return errors.Errorf("unknown ssh certificate type %d", cert.CertType) - } + var backdate uint64 + if cert.ValidAfter == 0 { + backdate = uint64(o.Backdate / time.Second) + cert.ValidAfter = uint64(now().Truncate(time.Second).Unix()) + } + if cert.ValidBefore == 0 { + cert.ValidBefore = cert.ValidAfter + uint64(d/time.Second) + } + // Apply backdate safely + if cert.ValidAfter > backdate { + cert.ValidAfter -= backdate + } + return nil + }) +} - hasLimit := !m.validBefore.IsZero() +// sshLimitDuration adjusts the duration to min(default, remaining provisioning +// credential duration). E.g. if the default is 12hrs but the remaining validity +// of the provisioning credential is only 4hrs, this option will set the value +// to 4hrs (the min of the two values). It will fail if a CertType has not been +// set or is not valid. +type sshLimitDuration struct { + *Claimer + NotAfter time.Time +} - n := now() - if cert.ValidAfter == 0 { - cert.ValidAfter = uint64(n.Truncate(time.Second).Unix()) - } - certValidAfter := time.Unix(int64(cert.ValidAfter), 0) - if hasLimit && certValidAfter.After(m.validBefore) { - return errors.Errorf("provisioning credential expiration (%s) is before "+ - "requested certificate validAfter (%s)", m.validBefore, certValidAfter) +func (m *sshLimitDuration) Option(o SSHOptions) SSHCertificateModifier { + if m.NotAfter.IsZero() { + defaultDuration := &sshDefaultDuration{m.Claimer} + return defaultDuration.Option(o) } - if cert.ValidBefore == 0 { - certValidBefore := certValidAfter.Add(d) - if hasLimit && m.validBefore.Before(certValidBefore) { - certValidBefore = m.validBefore + return sshModifierFunc(func(cert *ssh.Certificate) error { + d, err := m.DefaultSSHCertDuration(cert.CertType) + if err != nil { + return err } - cert.ValidBefore = uint64(certValidBefore.Unix()) - } else if hasLimit { - certValidBefore := time.Unix(int64(cert.ValidBefore), 0) - if m.validBefore.Before(certValidBefore) { - return errors.Errorf("provisioning credential expiration (%s) is before "+ - "requested certificate validBefore (%s)", m.validBefore, certValidBefore) + + var backdate uint64 + if cert.ValidAfter == 0 { + backdate = uint64(o.Backdate / time.Second) + cert.ValidAfter = uint64(now().Truncate(time.Second).Unix()) } - } - return nil -} + certValidAfter := time.Unix(int64(cert.ValidAfter), 0) + if certValidAfter.After(m.NotAfter) { + return errors.Errorf("provisioning credential expiration (%s) is before requested certificate validAfter (%s)", + m.NotAfter, certValidAfter) + } -func sshDefaultValidityModifier(c *Claimer) SSHCertificateModifier { - return &sshValidityModifier{c, time.Time{}} -} + if cert.ValidBefore == 0 { + certValidBefore := certValidAfter.Add(d) + if m.NotAfter.Before(certValidBefore) { + certValidBefore = m.NotAfter + println(2, certValidBefore.String()) + } + cert.ValidBefore = uint64(certValidBefore.Unix()) + } else { + certValidBefore := time.Unix(int64(cert.ValidBefore), 0) + if m.NotAfter.Before(certValidBefore) { + return errors.Errorf("provisioning credential expiration (%s) is before requested certificate validBefore (%s)", + m.NotAfter, certValidBefore) + } + } -// sshLimitValidityModifier adjusts the duration to -// min(default, remaining provisioning credential duration). -// E.g. if the default is 12hrs but the remaining validity of the provisioning -// credential is only 4hrs, this option will set the value to 4hrs (the min of the two values). -func sshLimitValidityModifier(c *Claimer, validBefore time.Time) SSHCertificateModifier { - return &sshValidityModifier{c, validBefore} + // Apply backdate safely + if cert.ValidAfter > backdate { + cert.ValidAfter -= backdate + } + + return nil + }) } // sshCertificateOptionsValidator validates the user SSHOptions with the ones @@ -301,8 +335,15 @@ func (v *sshCertificateValidityValidator) Valid(cert *ssh.Certificate) error { return errors.Errorf("unknown ssh certificate type %d", cert.CertType) } - // seconds - dur := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second + // To not take into account the backdate, time.Now() will be used to + // calculate the duration if ValidAfter is in the past. + var dur time.Duration + if t := now().Unix(); t > int64(cert.ValidAfter) { + dur = time.Duration(int64(cert.ValidBefore)-t) * time.Second + } else { + dur = time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second + } + switch { case dur < min: return errors.Errorf("requested duration of %s is less than minimum "+ diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index 25a44121..3db0f95e 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -1,6 +1,7 @@ package provisioner import ( + "fmt" "testing" "time" @@ -10,6 +11,32 @@ import ( "golang.org/x/crypto/ssh" ) +func TestSSHOptions_Type(t *testing.T) { + type fields struct { + CertType string + } + tests := []struct { + name string + fields fields + want uint32 + }{ + {"user", fields{"user"}, 1}, + {"host", fields{"host"}, 2}, + {"empty", fields{""}, 0}, + {"invalid", fields{"invalid"}, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := SSHOptions{ + CertType: tt.fields.CertType, + } + if got := o.Type(); got != tt.want { + t.Errorf("SSHOptions.Type() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_sshCertificateDefaultValidator_Valid(t *testing.T) { pub, _, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) @@ -276,7 +303,7 @@ func Test_sshValidityModifier(t *testing.T) { p, err := generateX5C(nil) assert.FatalError(t, err) type test struct { - svm *sshValidityModifier + svm *sshLimitDuration cert *ssh.Certificate valid func(*ssh.Certificate) err error @@ -284,7 +311,7 @@ func Test_sshValidityModifier(t *testing.T) { tests := map[string]func() test{ "fail/type-not-set": func() test { return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(6 * time.Hour)}, cert: &ssh.Certificate{ ValidAfter: uint64(n.Unix()), ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), @@ -294,18 +321,18 @@ func Test_sshValidityModifier(t *testing.T) { }, "fail/type-not-recognized": func() test { return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(6 * time.Hour)}, cert: &ssh.Certificate{ CertType: 4, ValidAfter: uint64(n.Unix()), ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), }, - err: errors.New("unknown ssh certificate type 4"), + err: errors.New("ssh certificate has an unknown type: 4"), } }, "fail/requested-validAfter-after-limit": func() test { return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(1 * time.Hour)}, cert: &ssh.Certificate{ CertType: 1, ValidAfter: uint64(n.Add(2 * time.Hour).Unix()), @@ -316,7 +343,7 @@ func Test_sshValidityModifier(t *testing.T) { }, "fail/requested-validBefore-after-limit": func() test { return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(1 * time.Hour)}, cert: &ssh.Certificate{ CertType: 1, ValidAfter: uint64(n.Unix()), @@ -328,7 +355,7 @@ func Test_sshValidityModifier(t *testing.T) { "ok/valid-requested-validBefore": func() test { va, vb := uint64(n.Unix()), uint64(n.Add(2*time.Hour).Unix()) return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(3 * time.Hour)}, cert: &ssh.Certificate{ CertType: 1, ValidAfter: va, @@ -343,21 +370,21 @@ func Test_sshValidityModifier(t *testing.T) { "ok/empty-requested-validBefore-limit-after-default": func() test { va := uint64(n.Unix()) return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(5 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(24 * time.Hour)}, cert: &ssh.Certificate{ CertType: 1, ValidAfter: va, }, valid: func(cert *ssh.Certificate) { assert.Equals(t, cert.ValidAfter, va) - assert.Equals(t, cert.ValidBefore, uint64(n.Add(4*time.Hour).Unix())) + assert.Equals(t, cert.ValidBefore, uint64(n.Add(16*time.Hour).Unix())) }, } }, "ok/empty-requested-validBefore-limit-before-default": func() test { va := uint64(n.Unix()) return test{ - svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)}, + svm: &sshLimitDuration{Claimer: p.claimer, NotAfter: n.Add(3 * time.Hour)}, cert: &ssh.Certificate{ CertType: 1, ValidAfter: va, @@ -372,7 +399,7 @@ func Test_sshValidityModifier(t *testing.T) { for name, run := range tests { t.Run(name, func(t *testing.T) { tt := run() - if err := tt.svm.Modify(tt.cert); err != nil { + if err := tt.svm.Option(SSHOptions{}).Modify(tt.cert); err != nil { if assert.NotNil(t, tt.err) { assert.HasPrefix(t, err.Error(), tt.err.Error()) } @@ -384,3 +411,25 @@ func Test_sshValidityModifier(t *testing.T) { }) } } + +func Test_sshModifierFunc_Modify(t *testing.T) { + type args struct { + cert *ssh.Certificate + } + tests := []struct { + name string + f sshModifierFunc + args args + wantErr bool + }{ + {"ok", func(cert *ssh.Certificate) error { return nil }, args{&ssh.Certificate{}}, false}, + {"fail", func(cert *ssh.Certificate) error { return fmt.Errorf("an error") }, args{&ssh.Certificate{}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.f.Modify(tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("sshModifierFunc.Modify() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index f02c53b4..76c9a567 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -31,7 +31,7 @@ var ( DisableRenewal: &defaultDisableRenewal, MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, + DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour}, MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 651cd136..1be728db 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -228,6 +228,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, if claims.Step == nil || claims.Step.SSH == nil { return nil, errors.New("authorization token must be an SSH provisioning token") } + opts := claims.Step.SSH signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token @@ -261,7 +262,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Set the default extensions. &sshDefaultExtensionModifier{}, // Checks the validity bounds, and set the validity if has not been set. - sshLimitValidityModifier(p.claimer, claims.chains[0][0].NotAfter), + &sshLimitDuration{p.claimer, claims.chains[0][0].NotAfter}, // set the key id to the token subject sshCertKeyIDValidator(claims.Subject), // Validate public key. diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 94018b55..65147d24 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -646,9 +646,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix()) case sshCertificateDefaultsModifier: assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert}) - case *sshValidityModifier: + case *sshLimitDuration: assert.Equals(t, v.Claimer, tc.p.claimer) - assert.Equals(t, v.validBefore, tc.claims.chains[0][0].NotAfter) + assert.Equals(t, v.NotAfter, tc.claims.chains[0][0].NotAfter) case *sshCertificateValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator, diff --git a/authority/ssh.go b/authority/ssh.go index 8148a6bd..e5b2955a 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -209,6 +209,9 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign var mods []provisioner.SSHCertificateModifier var validators []provisioner.SSHCertificateValidator + // Set backdate with the configured value + opts.Backdate = a.config.AuthorityConfig.Backdate.Duration + for _, op := range signOpts { switch o := op.(type) { // modify the ssh.Certificate @@ -365,9 +368,12 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { return nil, errors.New("rewnewSSH: cannot renew certificate without validity period") } - dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second - va := time.Now() - vb := va.Add(dur) + + backdate := a.config.AuthorityConfig.Backdate.Duration + duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second + now := time.Now() + va := now.Add(-1 * backdate) + vb := now.Add(duration - backdate) // Build base certificate with the key and some random values cert := &ssh.Certificate{ From f06db4099e43cfe939456b34b3ec53b931434f23 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 18:30:17 -0800 Subject: [PATCH 121/163] Add backdate support on ssh rekey. --- authority/ssh.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/authority/ssh.go b/authority/ssh.go index e5b2955a..cfd5ed37 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -496,9 +496,12 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { return nil, errors.New("rekeySSH: cannot rekey certificate without validity period") } - dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second - va := time.Now() - vb := va.Add(dur) + + backdate := a.config.AuthorityConfig.Backdate.Duration + duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second + now := time.Now() + va := now.Add(-1 * backdate) + vb := now.Add(duration - backdate) // Build base certificate with the key and some random values cert := &ssh.Certificate{ From 7e33aeb8d3d707b105bd2300c263648159bcfef3 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Jan 2020 12:19:00 -0800 Subject: [PATCH 122/163] Add unit test for profileDefaultDuration. --- authority/provisioner/sign_options.go | 2 +- authority/provisioner/sign_options_test.go | 41 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 2c3cfa16..1e6547b7 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -194,7 +194,7 @@ func (v profileDefaultDuration) Option(so Options) x509util.WithOption { var backdate time.Duration notBefore := so.NotBefore.Time() if notBefore.IsZero() { - notBefore = time.Now() + notBefore = now() backdate = -1 * so.Backdate } notAfter := so.NotAfter.RelativeTime(notBefore) diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index 8a452dab..c805f9d7 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -5,6 +5,7 @@ import ( "crypto/x509/pkix" "net" "net/url" + "reflect" "testing" "time" @@ -357,3 +358,43 @@ func Test_profileLimitDuration_Option(t *testing.T) { }) } } + +func Test_profileDefaultDuration_Option(t *testing.T) { + tm, fn := mockNow() + defer fn() + + v := profileDefaultDuration(24 * time.Hour) + type args struct { + so Options + } + tests := []struct { + name string + v profileDefaultDuration + args args + want *x509.Certificate + }{ + {"default", v, args{Options{}}, &x509.Certificate{NotBefore: tm, NotAfter: tm.Add(24 * time.Hour)}}, + {"backdate", v, args{Options{Backdate: 1 * time.Minute}}, &x509.Certificate{NotBefore: tm.Add(-1 * time.Minute), NotAfter: tm.Add(24 * time.Hour)}}, + {"notBefore", v, args{Options{NotBefore: NewTimeDuration(tm.Add(10 * time.Second))}}, &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(24*time.Hour + 10*time.Second)}}, + {"notAfter", v, args{Options{NotAfter: NewTimeDuration(tm.Add(1 * time.Hour))}}, &x509.Certificate{NotBefore: tm, NotAfter: tm.Add(1 * time.Hour)}}, + {"notBefore and notAfter", v, args{Options{NotBefore: NewTimeDuration(tm.Add(10 * time.Second)), NotAfter: NewTimeDuration(tm.Add(1 * time.Hour))}}, + &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(1 * time.Hour)}}, + {"notBefore and backdate", v, args{Options{Backdate: 1 * time.Minute, NotBefore: NewTimeDuration(tm.Add(10 * time.Second))}}, + &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(24*time.Hour + 10*time.Second)}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cert := &x509.Certificate{} + profile := &x509util.Leaf{} + profile.SetSubject(cert) + + fn := tt.v.Option(tt.args.so) + if err := fn(profile); err != nil { + t.Errorf("profileDefaultDuration.Option() error %v", err) + } + if !reflect.DeepEqual(cert, tt.want) { + t.Errorf("profileDefaultDuration.Option() = %v, \nwant %v", cert, tt.want) + } + }) + } +} From 165a91858e2e6289527d48bf2262f7edb7e408e1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Jan 2020 14:21:13 -0800 Subject: [PATCH 123/163] Add tests for backdate and sshDefaultDuration --- authority/provisioner/sign_options_test.go | 2 +- .../provisioner/sign_ssh_options_test.go | 126 +++++++++++++++++- go.sum | 1 + 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index c805f9d7..6c22625f 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -390,7 +390,7 @@ func Test_profileDefaultDuration_Option(t *testing.T) { fn := tt.v.Option(tt.args.so) if err := fn(profile); err != nil { - t.Errorf("profileDefaultDuration.Option() error %v", err) + t.Errorf("profileDefaultDuration.Option() error = %v", err) } if !reflect.DeepEqual(cert, tt.want) { t.Errorf("profileDefaultDuration.Option() = %v, \nwant %v", cert, tt.want) diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index 3db0f95e..e447065b 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -2,6 +2,7 @@ package provisioner import ( "fmt" + "reflect" "testing" "time" @@ -299,7 +300,9 @@ func Test_sshCertificateValidityValidator(t *testing.T) { } func Test_sshValidityModifier(t *testing.T) { - n := now() + n, fn := mockNow() + defer fn() + p, err := generateX5C(nil) assert.FatalError(t, err) type test struct { @@ -352,6 +355,32 @@ func Test_sshValidityModifier(t *testing.T) { err: errors.New("provisioning credential expiration ("), } }, + "ok/no-limit": func() test { + va, vb := uint64(n.Unix()), uint64(n.Add(16*time.Hour).Unix()) + return test{ + svm: &sshLimitDuration{Claimer: p.claimer}, + cert: &ssh.Certificate{ + CertType: 1, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, vb) + }, + } + }, + "ok/defaults": func() test { + va, vb := uint64(n.Unix()), uint64(n.Add(16*time.Hour).Unix()) + return test{ + svm: &sshLimitDuration{Claimer: p.claimer}, + cert: &ssh.Certificate{ + CertType: 1, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, vb) + }, + } + }, "ok/valid-requested-validBefore": func() test { va, vb := uint64(n.Unix()), uint64(n.Add(2*time.Hour).Unix()) return test{ @@ -433,3 +462,98 @@ func Test_sshModifierFunc_Modify(t *testing.T) { }) } } + +func Test_sshDefaultDuration_Option(t *testing.T) { + tm, fn := mockNow() + defer fn() + + newClaimer := func(claims *Claims) *Claimer { + c, err := NewClaimer(claims, globalProvisionerClaims) + if err != nil { + t.Fatal(err) + } + return c + } + unix := func(d time.Duration) uint64 { + return uint64(tm.Add(d).Unix()) + } + + type fields struct { + Claimer *Claimer + } + type args struct { + o SSHOptions + cert *ssh.Certificate + } + tests := []struct { + name string + fields fields + args args + want *ssh.Certificate + wantErr bool + }{ + {"user", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}}, + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(0), ValidBefore: unix(16 * time.Hour)}, false}, + {"host", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}}, + &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(0), ValidBefore: unix(30 * 24 * time.Hour)}, false}, + {"user claim", fields{newClaimer(&Claims{DefaultUserSSHDur: &Duration{1 * time.Hour}})}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}}, + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(0), ValidBefore: unix(1 * time.Hour)}, false}, + {"host claim", fields{newClaimer(&Claims{DefaultHostSSHDur: &Duration{1 * time.Hour}})}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}}, + &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(0), ValidBefore: unix(1 * time.Hour)}, false}, + {"user backdate", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert}}, + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(16 * time.Hour)}, false}, + {"host backdate", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert}}, + &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(30 * 24 * time.Hour)}, false}, + {"user validAfter", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(1 * time.Hour)}}, + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Minute), ValidBefore: unix(17 * time.Hour)}, false}, + {"user validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidBefore: unix(1 * time.Hour)}}, + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(time.Hour)}, false}, + {"host validAfter validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}}, + &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}, false}, + {"fail zero", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{}}, &ssh.Certificate{}, true}, + {"fail type", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{CertType: 3}}, &ssh.Certificate{CertType: 3}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &sshDefaultDuration{ + Claimer: tt.fields.Claimer, + } + v := m.Option(tt.args.o) + if err := v.Modify(tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("sshDefaultDuration.Option() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(tt.args.cert, tt.want) { + t.Errorf("sshDefaultDuration.Option() = %v, want %v", tt.args.cert, tt.want) + } + }) + } +} + +func Test_sshLimitDuration_Option(t *testing.T) { + type fields struct { + Claimer *Claimer + NotAfter time.Time + } + type args struct { + o SSHOptions + } + tests := []struct { + name string + fields fields + args args + want SSHCertificateModifier + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &sshLimitDuration{ + Claimer: tt.fields.Claimer, + NotAfter: tt.fields.NotAfter, + } + if got := m.Option(tt.args.o); !reflect.DeepEqual(got, tt.want) { + t.Errorf("sshLimitDuration.Option() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.sum b/go.sum index c200eeb8..e914534f 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,7 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRi golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= From f46dc031119ab26cf5e3f7bf99286ffed5dc61a2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Jan 2020 14:34:59 -0800 Subject: [PATCH 124/163] Add tests of profileLimitDuration with backdate. --- authority/provisioner/sign_options_test.go | 32 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index 6c22625f..d462780e 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -276,7 +276,9 @@ func Test_validityValidator_Valid(t *testing.T) { } func Test_profileLimitDuration_Option(t *testing.T) { - n := now() + n, fn := mockNow() + defer fn() + type test struct { pld profileLimitDuration so Options @@ -310,7 +312,7 @@ func Test_profileLimitDuration_Option(t *testing.T) { assert.FatalError(t, err) return test{ pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, - so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d, Backdate: 1 * time.Minute}, cert: new(x509.Certificate), valid: func(cert *x509.Certificate) { assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) @@ -321,7 +323,7 @@ func Test_profileLimitDuration_Option(t *testing.T) { "ok/valid-notAfter-nil-limit-over-default": func() test { return test{ pld: profileLimitDuration{def: 1 * time.Hour, notAfter: n.Add(6 * time.Hour)}, - so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute}, cert: new(x509.Certificate), valid: func(cert *x509.Certificate) { assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) @@ -332,7 +334,7 @@ func Test_profileLimitDuration_Option(t *testing.T) { "ok/valid-notAfter-nil-limit-under-default": func() test { return test{ pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, - so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute}, cert: new(x509.Certificate), valid: func(cert *x509.Certificate) { assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) @@ -340,6 +342,28 @@ func Test_profileLimitDuration_Option(t *testing.T) { }, } }, + "ok/over-limit-with-backdate": func() test { + return test{ + pld: profileLimitDuration{def: 24 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{Backdate: 1 * time.Minute}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(-time.Minute)) + assert.Equals(t, cert.NotAfter, n.Add(6*time.Hour)) + }, + } + }, + "ok/under-limit-with-backdate": func() test { + return test{ + pld: profileLimitDuration{def: 24 * time.Hour, notAfter: n.Add(30 * time.Hour)}, + so: Options{Backdate: 1 * time.Minute}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(-time.Minute)) + assert.Equals(t, cert.NotAfter, n.Add(24*time.Hour)) + }, + } + }, } for name, run := range tests { t.Run(name, func(t *testing.T) { From 77af30bfa395af5a025bb045315359c6fad7977c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 8 Jan 2020 11:46:33 -0800 Subject: [PATCH 125/163] Remove debug statement. --- authority/provisioner/sign_ssh_options.go | 1 - 1 file changed, 1 deletion(-) diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index f5ad8662..643e0645 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -276,7 +276,6 @@ func (m *sshLimitDuration) Option(o SSHOptions) SSHCertificateModifier { certValidBefore := certValidAfter.Add(d) if m.NotAfter.Before(certValidBefore) { certValidBefore = m.NotAfter - println(2, certValidBefore.String()) } cert.ValidBefore = uint64(certValidBefore.Unix()) } else { From d13754166a16ce0d37a98f5f535933ab48a9b027 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 9 Jan 2020 18:41:13 -0800 Subject: [PATCH 126/163] Add support for cloudkms and softkms. --- kms/apiv1/options.go | 59 ++++++++++++++ kms/apiv1/requests.go | 136 +++++++++++++++++++++++++++++++ kms/cloudkms/cloudkms.go | 172 +++++++++++++++++++++++++++++++++++++++ kms/cloudkms/signer.go | 80 ++++++++++++++++++ kms/kms.go | 35 ++++++++ kms/softkms/softkms.go | 112 +++++++++++++++++++++++++ 6 files changed, 594 insertions(+) create mode 100644 kms/apiv1/options.go create mode 100644 kms/apiv1/requests.go create mode 100644 kms/cloudkms/cloudkms.go create mode 100644 kms/cloudkms/signer.go create mode 100644 kms/kms.go create mode 100644 kms/softkms/softkms.go diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go new file mode 100644 index 00000000..a6672087 --- /dev/null +++ b/kms/apiv1/options.go @@ -0,0 +1,59 @@ +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 = "softkms" + // CloudKMS is a KMS implementation using Google's Cloud KMS. + CloudKMS = "cloudkms" + // AmazonKMS is a KMS implementation using Amazon AWS KMS. + AmazonKMS = "awskms" + // PKCS11 is a KMS implementation using the PKCS11 standard. + PKCS11 = "pkcs11" +) + +type Options struct { + Type string `json:"type"` + CredentialsFile string `json:"credentialsFile"` +} + +// 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/requests.go b/kms/apiv1/requests.go new file mode 100644 index 00000000..d079f6c1 --- /dev/null +++ b/kms/apiv1/requests.go @@ -0,0 +1,136 @@ +package apiv1 + +import ( + "crypto" + "fmt" +) + +type KeyType int + +const ( + // nolint:camelcase + RSA_2048 KeyType = iota + RSA_3072 + RSA_4096 + EC_P256 + EC_P384 + EC_P512 +) + +// 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) + } +} + +type GetPublicKeyRequest struct { + Name string +} + +type GetPublicKeyResponse struct { + Name string + PublicKey crypto.PublicKey +} + +type CreateKeyRequest struct { + Parent string + Name string + Type KeyType + Bits int + SignatureAlgorithm SignatureAlgorithm + + // ProtectionLevel specifies how cryptographic operations are performed. + // Used by: cloudkms + ProtectionLevel ProtectionLevel +} + +type CreateKeyResponse struct { + Name string + PublicKey crypto.PublicKey + PrivateKey crypto.PrivateKey +} + +type CreateSignerRequest struct { + SigningKey string + SigningKeyPEM []byte + Password string +} diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go new file mode 100644 index 00000000..f122bdeb --- /dev/null +++ b/kms/cloudkms/cloudkms.go @@ -0,0 +1,172 @@ +package cloudkms + +import ( + "context" + "crypto" + "time" + + 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, +} + +type keyManagementClient interface { + 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) +} + +// CloudKMS implements a KMS using Google's Cloud apiv1. +type CloudKMS struct { + client keyManagementClient +} + +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 +} + +// 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) { + switch { + case req.Name == "": + return nil, errors.New("createKeyRequest 'name' cannot be empty") + case req.Parent == "": + return nil, errors.New("createKeyRequest 'parent' 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") + } + + ctx, cancel := defaultContext() + defer cancel() + + response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ + Parent: req.Parent, + CryptoKeyId: req.Name, + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ + ProtectionLevel: protectionLevel, + Algorithm: signatureAlgorithm, + }, + }, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS CreateCryptoKey failed") + } + + return &apiv1.CreateKeyResponse{ + Name: response.Name, + }, 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) (*apiv1.GetPublicKeyResponse, error) { + 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 &apiv1.GetPublicKeyResponse{ + Name: req.Name, + PublicKey: pk, + }, nil +} + +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} diff --git a/kms/cloudkms/signer.go b/kms/cloudkms/signer.go new file mode 100644 index 00000000..a28fd9d8 --- /dev/null +++ b/kms/cloudkms/signer.go @@ -0,0 +1,80 @@ +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 { + println(1, err.Error()) + return errors.Wrap(err, "cloudKMS GetPublicKey failed") + } + + pk, err := pemutil.ParseKey([]byte(response.Pem)) + if err != nil { + println(2, err.Error()) + 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/kms.go b/kms/kms.go new file mode 100644 index 00000000..5e54451d --- /dev/null +++ b/kms/kms.go @@ -0,0 +1,35 @@ +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) (*apiv1.GetPublicKeyResponse, error) + CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) + CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, 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/softkms/softkms.go b/kms/softkms/softkms.go new file mode 100644 index 00000000..adb8483e --- /dev/null +++ b/kms/softkms/softkms.go @@ -0,0 +1,112 @@ +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 +} + +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{ + apiv1.UnspecifiedSignAlgorithm: algorithmAttributes{"EC", "P-256"}, + apiv1.SHA256WithRSA: algorithmAttributes{"RSA", ""}, + apiv1.SHA384WithRSA: algorithmAttributes{"RSA", ""}, + apiv1.SHA512WithRSA: algorithmAttributes{"RSA", ""}, + apiv1.SHA256WithRSAPSS: algorithmAttributes{"RSA", ""}, + apiv1.SHA384WithRSAPSS: algorithmAttributes{"RSA", ""}, + apiv1.SHA512WithRSAPSS: algorithmAttributes{"RSA", ""}, + apiv1.ECDSAWithSHA256: algorithmAttributes{"EC", "P-256"}, + apiv1.ECDSAWithSHA384: algorithmAttributes{"EC", "P-384"}, + apiv1.ECDSAWithSHA512: algorithmAttributes{"EC", "P-521"}, + apiv1.PureEd25519: algorithmAttributes{"OKP", "Ed25519"}, +} + +// SoftKSM is a key manager that uses keys stored in disk. +type SoftKMS struct{} + +// New returns a new SoftKSM. +func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { + return &SoftKMS{}, 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 != "" { + opts = append(opts, pemutil.WithPassword([]byte(req.Password))) + } + + switch { + 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 := keys.GenerateKeyPair(v.Type, v.Curve, req.Bits) + if err != nil { + return nil, err + } + + return &apiv1.CreateKeyResponse{ + Name: req.Name, + PublicKey: pub, + PrivateKey: priv, + }, nil +} + +func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, error) { + v, err := pemutil.Read(req.Name) + if err != nil { + return nil, err + } + + switch v.(type) { + case *x509.Certificate: + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + default: + return nil, errors.Errorf("unsupported public key type %T", v) + } + + return &apiv1.GetPublicKeyResponse{ + Name: req.Name, + PublicKey: v, + }, nil +} From c62526b39f0ce9bfe3fc389be1c5e52a03073b83 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 9 Jan 2020 18:42:26 -0800 Subject: [PATCH 127/163] Add wip support for kms. --- authority/authority.go | 59 ++++++++++++++++++++++++++++++++++-------- authority/config.go | 7 +++++ authority/options.go | 33 +++++++++++++++++++++++ authority/tls.go | 29 ++++++++++----------- 4 files changed, 102 insertions(+), 26 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 25b40350..85bb436d 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -12,6 +12,8 @@ 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" @@ -28,6 +30,9 @@ type Authority struct { config *Config rootX509Certs []*x509.Certificate intermediateIdentity *x509util.Identity + keyManager kms.KeyManager + x509Signer crypto.Signer + x509Issuer *x509.Certificate sshCAUserCertSignKey ssh.Signer sshCAHostCertSignKey ssh.Signer sshCAUserCerts []ssh.PublicKey @@ -76,6 +81,14 @@ func (a *Authority) init() error { } var err error + + if a.keyManager == nil { + a.keyManager, err = kms.New(context.Background(), *a.config.KMS) + 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 { @@ -107,27 +120,47 @@ func (a *Authority) init() error { 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)), - ) + 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: a.config.Password, + }) if err != nil { return err } + a.x509Signer = signer + a.x509Issuer = 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)), + // ) + // if err != nil { + // return err + // } + // } else { + // a.intermediateIdentity, err = x509util.LoadIdentityFromDisk(a.config.IntermediateCert, a.config.IntermediateKey) + // if err != nil { + // return err + // } + // } } // 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: a.config.Password, + }) + // signer, err := parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) if err != nil { return err } @@ -140,7 +173,11 @@ 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: a.config.Password, + }) + // signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) if err != nil { return err } 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..9891e258 100644 --- a/authority/options.go +++ b/authority/options.go @@ -2,11 +2,14 @@ package authority import ( "context" + "crypto" "crypto/x509" "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. @@ -52,3 +55,33 @@ func WithSSHCheckHost(fn func(ctx context.Context, principal string, tok string, a.sshCheckHostFunc = fn } } + +// WithKeyManager defines the key manager used to get and create keys, and sign +// certificates. +func WithKeyManager(k kms.KeyManager) Option { + return func(a *Authority) { + a.keyManager = k + } +} + +// WithX509Signer defines the signer used to sign X509 certificates. +func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option { + return func(a *Authority) { + a.x509Issuer = crt + a.x509Signer = s + } +} + +// WithSSHUserSigner defines the signer used to sign SSH user certificates. +func WithSSHUserSigner(s ssh.Signer) Option { + return func(a *Authority) { + a.sshCAUserCertSignKey = s + } +} + +// WithSSHHostSigner defines the signer used to sign SSH host certificates. +func WithSSHHostSigner(s ssh.Signer) Option { + return func(a *Authority) { + a.sshCAHostCertSignKey = s + } +} diff --git a/authority/tls.go b/authority/tls.go index eb7cb86a..0eb8f280 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -63,7 +63,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti errContext = apiCtx{"csr": csr, "signOptions": signOpts} mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)} certValidators = []provisioner.CertificateValidator{} - issIdentity = a.intermediateIdentity + // issIdentity = a.intermediateIdentity ) // Set backdate with the configured value @@ -90,7 +90,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti http.StatusBadRequest, errContext} } - 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, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext} } @@ -113,11 +113,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti http.StatusInternalServerError, errContext} } - caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) - if err != nil { - return nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"), - http.StatusInternalServerError, errContext} - } + // caCert, err := x509.ParseCertificate(a.x509SignerCert.Raw) + // if err != nil { + // return nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"), + // http.StatusInternalServerError, errContext} + // } if err = a.db.StoreCertificate(serverCert); err != nil { if err != db.ErrNotImplemented { @@ -126,7 +126,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,7 +138,7 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error } // Issuer - issIdentity := a.intermediateIdentity + // issIdentity := a.intermediateIdentity // Durations backdate := a.config.AuthorityConfig.Backdate.Duration @@ -147,7 +147,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), @@ -187,8 +187,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, &apiError{err, http.StatusInternalServerError, apiCtx{}} } @@ -203,7 +202,7 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error return nil, &apiError{errors.Wrap(err, "error parsing new server certificate"), http.StatusInternalServerError, apiCtx{}} } - caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) + caCert, err := x509.ParseCertificate(a.x509Issuer.Raw) if err != nil { return nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"), http.StatusInternalServerError, apiCtx{}} @@ -327,7 +326,7 @@ func (a *Authority) Revoke(ctx context.Context, opts *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, + a.x509Issuer, a.x509Signer, x509util.WithHosts(strings.Join(a.config.DNSNames, ","))) if err != nil { return nil, err @@ -350,7 +349,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, err } From 995375013d3542103395cbce68af72528f5b9f89 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 9 Jan 2020 18:43:35 -0800 Subject: [PATCH 128/163] Update dependencies for kms support. --- go.mod | 9 ++- go.sum | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 407e6164..3e1d554e 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 @@ -14,7 +17,11 @@ require ( github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 - golang.org/x/net v0.0.0-20190620200207-3b0461eec859 + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + golang.org/x/sys v0.0.0-20200107162124-548cf772de50 // indirect + google.golang.org/api v0.15.0 + google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb gopkg.in/square/go-jose.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index e914534f..8e07b9c1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,21 @@ +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= +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/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= @@ -15,12 +31,14 @@ github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPM github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/corpix/uarand v0.0.0-20170903190822-2b8494104d86/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M= github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= @@ -31,30 +49,64 @@ github.com/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6St github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +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 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +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/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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +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/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -96,6 +148,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= @@ -160,45 +214,160 @@ github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln github.com/weppos/publicsuffix-go v0.10.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +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= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/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-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= +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-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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 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= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/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/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= 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-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-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50 h1:YvQ10rzcqWXLlJZ3XCUoO25savxmscf4+SC+ZqiCHhA= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/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-20190816200558-6889da9d5479/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-20191012152004-8de300cfc20a/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/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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +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-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-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= +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.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From 085ae821636e80ecb3017d4206a7e003baee071c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 10:58:49 -0800 Subject: [PATCH 129/163] Remove the use of custom x509 package. Upgrade cli dependency. --- authority/provisioner/sign_options_test.go | 7 +- go.mod | 8 +- go.sum | 462 +++++++++++++++++++++ 3 files changed, 469 insertions(+), 8 deletions(-) diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index d462780e..1076d3b5 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -13,7 +13,6 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/x509util" - stepx509 "github.com/smallstep/cli/pkg/x509" ) func Test_emailOnlyIdentity_Valid(t *testing.T) { @@ -64,9 +63,9 @@ func Test_defaultPublicKeyValidator_Valid(t *testing.T) { ecdsaCSR, ok := _ecdsa.(*x509.CertificateRequest) assert.Fatal(t, ok) - _ed25519, err := pemutil.Read("./testdata/ed25519.csr", pemutil.WithStepCrypto()) + _ed25519, err := pemutil.Read("./testdata/ed25519.csr") assert.FatalError(t, err) - ed25519CSR, ok := _ed25519.(*stepx509.CertificateRequest) + ed25519CSR, ok := _ed25519.(*x509.CertificateRequest) assert.Fatal(t, ok) v := defaultPublicKeyValidator{} @@ -97,7 +96,7 @@ func Test_defaultPublicKeyValidator_Valid(t *testing.T) { }, { "ok/ed25519", - x509util.ToX509CertificateRequest(ed25519CSR), + ed25519CSR, nil, }, } diff --git a/go.mod b/go.mod index 407e6164..c32d3fff 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,11 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 - github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838 + github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e github.com/smallstep/nosql v0.2.0 - github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a - golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 - golang.org/x/net v0.0.0-20190620200207-3b0461eec859 + 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 gopkg.in/square/go-jose.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index e914534f..1462bec3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ +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= +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= 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/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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= @@ -11,39 +17,175 @@ github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4 github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.18+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/corpix/uarand v0.0.0-20170903190822-2b8494104d86/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M= github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6Sts= github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.8.0/go.mod h1:3l45GVGkyrnYNl9HoIjnp2NnNWvh6hLAqD8yTfGjnw8= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +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/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g= +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/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +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-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= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.17.2-0.20190910081718-bad04bb7378f/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg= +github.com/golangci/golangci-lint v1.22.2/go.mod h1:2Bj42k6hPQFTRxkDb7S3TQ+EsnumZXOmIYNqlQrp0FI= +github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= +github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/certificate-transparency-go v1.1.0/go.mod h1:i+Q7XY+ArBveOUT36jiHGfuSK1fHICIg6sUkRxPAbCs= +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/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/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= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/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= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= @@ -51,29 +193,61 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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/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= +github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/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= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= +github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -81,29 +255,110 @@ github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW1 github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +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/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= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= +github.com/securego/gosec v0.0.0-20191002120514-e680875ea14d/go.mod h1:w5+eXa0mYznDkHaMCXA4XYffjlH+cy1oyKbfzJXa2Do= +github.com/securego/gosec v0.0.0-20200106085552-9cb83e10afad/go.mod h1:7fJLcv5NlMd4t9waQEDLgpZeE3nv4D5DMz5JuZZGufg= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -117,8 +372,10 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191126035953-e88034bea402/go. github.com/smallstep/certificates v0.14.0-rc.1.0.20191210005525-50152391a397/go.mod h1:8leACUXHFo0JVm9YcrcX09aar2H8hz1BAWxD1D/GpsU= github.com/smallstep/certificates v0.14.0-rc.1.0.20191213215656-d2100821138c/go.mod h1:HMXt9hWBm7M7ZrUy0uZ/T/077te2x9bnXZCxrdVsBf4= github.com/smallstep/certificates v0.14.0-rc.1.0.20191217235337-aa5894058226/go.mod h1:MTKifeJBe1B/dzH5NDoPFpIPaWD0MzRozzONVkF8egc= +github.com/smallstep/certificates v0.14.0-rc.1.0.20191218224459-1fa35491ea07/go.mod h1:eEtpedAL4inqaAx6ZqJnE4NOx1/GxDh6VQOmbs7CPf0= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= +github.com/smallstep/certinfo v1.0.0/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df h1:SSZWKGpaVmKQgTkfaQMnYLS/gYhRVVjvzdE1F9GiffU= github.com/smallstep/cli v0.12.1-0.20191016010425-15911d8625df/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= github.com/smallstep/cli v0.13.3 h1:S29UydCtDVy0QQBtGdatq064tnks1/0DYxxnEtNiQpc= @@ -135,6 +392,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45 h1:ff5cEYAUC github.com/smallstep/cli v0.14.0-rc.1.0.20191217223638-5ee30a55af45/go.mod h1:6pTiWJKfIQcUYtK7lVnI0pOXRiYAWuy0qrlFVnn9q8M= github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838 h1:UZG/5HqHZbMRZk1KcBkH8HPgH8HSDtQ2jBb9hLFwpT8= github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838/go.mod h1:JPG34JrC37Pw0HjoB+cAtXT1yFOXfab/5nrM7ZTSw8c= +github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e h1:1aN6fvv1pOLZaKwxVcoYuS8kkqkLFnLjhd0y1pmNsTw= +github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e/go.mod h1:MA99N6UETSrq7/Pk/iZcgHqqiIU3tDscFNx2pGcdLlU= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= @@ -145,60 +404,263 @@ github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45 github.com/smallstep/zcrypto v0.0.0-20191008000232-9fc4bea33f70/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zcrypto v0.0.0-20191122194514-76530dff70e7/go.mod h1:8LA6x9T22WADMj89Ksf6DnOVCOJF3zLKUdSRAcZmW4U= github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= +github.com/tommy-muehle/go-mnd v1.1.1/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/weppos/publicsuffix-go v0.10.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= +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.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= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-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= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/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-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/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +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= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-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= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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/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= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= 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-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-20191220142924-d4481acd189f/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= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +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-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= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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-20190621195816-6e04913cbbac/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-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-20190930201159-7c411dea38b0/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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +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= +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/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/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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +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= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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/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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= +mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= +mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2/go.mod h1:rCqoQrfAmpTX/h2APczwM7UymU/uvaOluiVPIYCSY/k= +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/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= From 3cbf30b5551e60db037317046aa20b613b177905 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 11:19:28 -0800 Subject: [PATCH 130/163] Upgrade golangci-lint to v1.22.2 --- .golangci.yml | 18 +++++++-------- Makefile | 2 +- go.sum | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2028654a..4615a34c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,13 @@ linters-settings: govet: check-shadowing: true - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + # settings: + # printf: + # funcs: + # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf golint: min-confidence: 0 gocyclo: @@ -45,7 +45,7 @@ linters: enable: - gofmt - golint - - vet + - govet - misspell - ineffassign - deadcode @@ -66,6 +66,6 @@ issues: # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration service: - golangci-lint-version: 1.18.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.22.x # use the fixed version to not introduce new linters unexpectedly prepare: - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/Makefile b/Makefile index 7e06d837..750dbc5a 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ all: build test lint ######################################### bootstra%: - $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.18.0 + $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.22.2 .PHONY: bootstra% diff --git a/go.sum b/go.sum index 1462bec3..945cddb4 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= 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/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= @@ -19,6 +20,7 @@ github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHS github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= @@ -35,6 +37,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bombsimon/wsl/v2 v2.0.0 h1:+Vjcn+/T5lSrO8Bjzhk4v14Un/2UyCA1E3V5j9nwTkQ= github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -81,7 +84,9 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.8.0 h1:5bzFgL+oy7JITMTxUPJ00n7VxmYd/PdMp5mHFX40/RY= github.com/fatih/color v1.8.0/go.mod h1:3l45GVGkyrnYNl9HoIjnp2NnNWvh6hLAqD8yTfGjnw8= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= @@ -89,9 +94,11 @@ github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 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-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= github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -99,23 +106,33 @@ github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dT github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw= github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 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/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -130,27 +147,42 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w= github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0= github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks= github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= github.com/golangci/golangci-lint v1.17.2-0.20190910081718-bad04bb7378f/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg= +github.com/golangci/golangci-lint v1.22.2 h1:iaihss3Tf6NvZVjun3lHimKSgofPV1+FqE/cbehoiRQ= github.com/golangci/golangci-lint v1.22.2/go.mod h1:2Bj42k6hPQFTRxkDb7S3TQ+EsnumZXOmIYNqlQrp0FI= github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI= github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM= github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -175,6 +207,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gostaticanalysis/analysisutil v0.0.3 h1:iwp+5/UAyzQSFgQ4uR2sni99sJ8Eo9DEacKWM5pekIg= github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= @@ -184,6 +217,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqC github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= @@ -193,6 +227,7 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -210,6 +245,7 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -239,9 +275,11 @@ github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+L github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= +github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb h1:RHba4YImhrUVQDHUCe2BNSOz4tVy2yGyXhvYDvxGgeE= github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -264,10 +302,12 @@ github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -281,6 +321,7 @@ github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1: github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= @@ -302,6 +343,7 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -344,6 +386,7 @@ github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WS github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= github.com/securego/gosec v0.0.0-20191002120514-e680875ea14d/go.mod h1:w5+eXa0mYznDkHaMCXA4XYffjlH+cy1oyKbfzJXa2Do= +github.com/securego/gosec v0.0.0-20200106085552-9cb83e10afad h1:RRKCvNjpat6DCr+QHF4lzZS1vNfqp13J2uGEQPZ4JNI= github.com/securego/gosec v0.0.0-20200106085552-9cb83e10afad/go.mod h1:7fJLcv5NlMd4t9waQEDLgpZeE3nv4D5DMz5JuZZGufg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= @@ -407,10 +450,12 @@ github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= @@ -418,40 +463,51 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q= github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= +github.com/tommy-muehle/go-mnd v1.1.1 h1:4D0wuPKjOTiK2garzuPGGvm4zZ/wLYDOH8TJSABC7KU= github.com/tommy-muehle/go-mnd v1.1.1/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbdo= github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg= github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs= github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= @@ -598,6 +654,7 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn 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-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= @@ -635,6 +692,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -654,13 +712,18 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= 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= 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= sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= From ab1807d6a0b408a4fb876f3b4dd9330827fe280d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 17:19:56 -0800 Subject: [PATCH 131/163] Use release v1.19.1 of golangci-lint See https://github.com/golangci/golangci-lint/issues/885 --- .golangci.yml | 16 ++++++++-------- Makefile | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4615a34c..f0c2eed0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,13 @@ linters-settings: govet: check-shadowing: true - # settings: - # printf: - # funcs: - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf golint: min-confidence: 0 gocyclo: @@ -66,6 +66,6 @@ issues: # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration service: - golangci-lint-version: 1.22.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.19.x # use the fixed version to not introduce new linters unexpectedly prepare: - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/Makefile b/Makefile index 750dbc5a..bd11222e 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,8 @@ all: build test lint ######################################### bootstra%: - $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.22.2 + # Using a released version of golangci-lint to take into account custom replacements in their go.mod + $Q curl -sSfL https://raw.githubusercontent.com/smallstep/cli/master/make/golangci-install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.19.1 .PHONY: bootstra% From 3ce267cdd6b7415774affaf4257ee098417593a2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 17:21:47 -0800 Subject: [PATCH 132/163] Upgrade smallste/cli --- go.mod | 2 +- go.sum | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c32d3fff..66894cde 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 - github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e + github.com/smallstep/cli v0.14.0-rc.1.0.20200111011727-83a91ec8e405 github.com/smallstep/nosql v0.2.0 github.com/urfave/cli v1.22.2 golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 diff --git a/go.sum b/go.sum index 945cddb4..86dce041 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,7 @@ github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQY github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -416,6 +417,7 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191210005525-50152391a397/go. github.com/smallstep/certificates v0.14.0-rc.1.0.20191213215656-d2100821138c/go.mod h1:HMXt9hWBm7M7ZrUy0uZ/T/077te2x9bnXZCxrdVsBf4= github.com/smallstep/certificates v0.14.0-rc.1.0.20191217235337-aa5894058226/go.mod h1:MTKifeJBe1B/dzH5NDoPFpIPaWD0MzRozzONVkF8egc= github.com/smallstep/certificates v0.14.0-rc.1.0.20191218224459-1fa35491ea07/go.mod h1:eEtpedAL4inqaAx6ZqJnE4NOx1/GxDh6VQOmbs7CPf0= +github.com/smallstep/certificates v0.14.0-rc.1.0.20200110185849-085ae821636e/go.mod h1:weY9Os8g0yPfyxd+Zy1CTAwCb7YMqg/u5XnEagBN5Rk= github.com/smallstep/certinfo v0.0.0-20191008000228-b0e530932339/go.mod h1:n4YHPL9hJIyB+N4F2rPBy3mpPxMxTGJP5Pdsyaoc2Ns= github.com/smallstep/certinfo v0.0.0-20191029235839-00563809d483/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= github.com/smallstep/certinfo v1.0.0/go.mod h1:xmx5n8+7jI0lrjTUwc8WMMqXeOHRyxYUW9U1wrvP3Vo= @@ -437,6 +439,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838 h1:UZG/5HqHZ github.com/smallstep/cli v0.14.0-rc.1.0.20191218000521-3e7348324838/go.mod h1:JPG34JrC37Pw0HjoB+cAtXT1yFOXfab/5nrM7ZTSw8c= github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e h1:1aN6fvv1pOLZaKwxVcoYuS8kkqkLFnLjhd0y1pmNsTw= github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e/go.mod h1:MA99N6UETSrq7/Pk/iZcgHqqiIU3tDscFNx2pGcdLlU= +github.com/smallstep/cli v0.14.0-rc.1.0.20200111011727-83a91ec8e405 h1:hvcnKc+fiBOUa15cb4SPJNFPrQax9nDQWVNPCLPYjAc= +github.com/smallstep/cli v0.14.0-rc.1.0.20200111011727-83a91ec8e405/go.mod h1:MCvJvfMNtWCi/VBfXxP1JONqLLfF9TcBj1/t5Rqme90= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= github.com/smallstep/nosql v0.1.1 h1:ijeE3CM00SddioodNl/LWRQINNNCK1dLUsjZDwpUbNg= From e98d7832b99541d1e1794793a89e22203c5664d2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 18:33:48 -0800 Subject: [PATCH 133/163] Add options to read the roots and federated roots from a bundle. --- authority/authority.go | 96 ++++++++++++++++++++++-------------------- authority/options.go | 79 +++++++++++++++++++++++++++++----- go.sum | 1 + 3 files changed, 120 insertions(+), 56 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 85bb436d..4af92f5e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -17,7 +17,6 @@ import ( "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" ) @@ -27,24 +26,30 @@ const ( // Authority implements the Certificate Authority internal interface. type Authority struct { - config *Config - rootX509Certs []*x509.Certificate - intermediateIdentity *x509util.Identity - keyManager kms.KeyManager - x509Signer crypto.Signer - x509Issuer *x509.Certificate + 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) @@ -64,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 } @@ -82,6 +94,7 @@ func (a *Authority) init() error { var err error + // Initialize key manager if it has not been set in the options. if a.keyManager == nil { a.keyManager, err = kms.New(context.Background(), *a.config.KMS) if err != nil { @@ -97,29 +110,39 @@ 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) } + // Read intermediate and create X509 signer. if a.x509Signer == nil { crt, err := pemutil.ReadCertificate(a.config.IntermediateCert) if err != nil { @@ -134,23 +157,6 @@ func (a *Authority) init() error { } a.x509Signer = signer a.x509Issuer = 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)), - // ) - // if err != nil { - // return err - // } - // } else { - // a.intermediateIdentity, err = x509util.LoadIdentityFromDisk(a.config.IntermediateCert, a.config.IntermediateKey) - // if err != nil { - // return err - // } - // } } // Decrypt and load SSH keys @@ -160,7 +166,6 @@ func (a *Authority) init() error { SigningKey: a.config.SSH.HostKey, Password: a.config.Password, }) - // signer, err := parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) if err != nil { return err } @@ -177,7 +182,6 @@ func (a *Authority) init() error { SigningKey: a.config.SSH.UserKey, Password: a.config.Password, }) - // signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) if err != nil { return err } diff --git a/authority/options.go b/authority/options.go index 9891e258..3c38d69a 100644 --- a/authority/options.go +++ b/authority/options.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/x509" + "encoding/pem" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" @@ -13,37 +14,41 @@ import ( ) // 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 } } @@ -51,37 +56,91 @@ 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) { + 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) { + 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 ssh.Signer) Option { - return func(a *Authority) { + return func(a *Authority) error { a.sshCAUserCertSignKey = s + return nil } } // WithSSHHostSigner defines the signer used to sign SSH host certificates. func WithSSHHostSigner(s ssh.Signer) Option { - return func(a *Authority) { + return func(a *Authority) error { a.sshCAHostCertSignKey = s + return nil } } + +// WithX509RootBundle is an option that allows to define the list of root +// certificates. +func WithX509RootBundle(pemCerts []byte) Option { + return func(a *Authority) error { + certs, err := readCertificateBundle(pemCerts) + if err != nil { + return err + } + x509.NewCertPool() + a.rootX509Certs = certs + return nil + } +} + +// WithX509FederatedBundle is an option that allows to define the list of +// federated certificates. +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/go.sum b/go.sum index 16183e09..fdc76924 100644 --- a/go.sum +++ b/go.sum @@ -576,6 +576,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0= 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= From 9641ab33b8f31e429294b6e2b3bda245dc9e8b8e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:38:29 -0800 Subject: [PATCH 134/163] Use crypto.Signer instead of ssh.Signer in SSH options. --- authority/options.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/authority/options.go b/authority/options.go index 3c38d69a..2eb62e16 100644 --- a/authority/options.go +++ b/authority/options.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/pem" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/kms" @@ -81,17 +82,33 @@ func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option { } // WithSSHUserSigner defines the signer used to sign SSH user certificates. -func WithSSHUserSigner(s ssh.Signer) Option { +func WithSSHUserSigner(s crypto.Signer) Option { return func(a *Authority) error { - a.sshCAUserCertSignKey = s + 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 ssh.Signer) Option { +func WithSSHHostSigner(s crypto.Signer) Option { return func(a *Authority) error { - a.sshCAHostCertSignKey = s + 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 } } From 3f8de17a407e791533d7786266326436c4e8fef5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:42:14 -0800 Subject: [PATCH 135/163] Cleanup types and add initial support for the options required for PKCS11. --- kms/apiv1/options.go | 19 ++++++++++++++----- kms/apiv1/requests.go | 29 ++++++++++------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index a6672087..a46db037 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -25,18 +25,27 @@ const ( // DefaultKMS is a KMS implementation using software. DefaultKMS Type = "" // SoftKMS is a KMS implementation using software. - SoftKMS = "softkms" + SoftKMS Type = "softkms" // CloudKMS is a KMS implementation using Google's Cloud KMS. - CloudKMS = "cloudkms" + CloudKMS Type = "cloudkms" // AmazonKMS is a KMS implementation using Amazon AWS KMS. - AmazonKMS = "awskms" + AmazonKMS Type = "awskms" // PKCS11 is a KMS implementation using the PKCS11 standard. - PKCS11 = "pkcs11" + PKCS11 Type = "pkcs11" ) type Options struct { - Type string `json:"type"` + // 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. diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go index d079f6c1..ddcbb108 100644 --- a/kms/apiv1/requests.go +++ b/kms/apiv1/requests.go @@ -5,18 +5,6 @@ import ( "fmt" ) -type KeyType int - -const ( - // nolint:camelcase - RSA_2048 KeyType = iota - RSA_3072 - RSA_4096 - EC_P256 - EC_P384 - EC_P512 -) - // ProtectionLevel specifies on some KMS how cryptographic operations are // performed. type ProtectionLevel int @@ -112,11 +100,9 @@ type GetPublicKeyResponse struct { } type CreateKeyRequest struct { - Parent string Name string - Type KeyType - Bits int SignatureAlgorithm SignatureAlgorithm + Bits int // ProtectionLevel specifies how cryptographic operations are performed. // Used by: cloudkms @@ -124,13 +110,18 @@ type CreateKeyRequest struct { } type CreateKeyResponse struct { - Name string - PublicKey crypto.PublicKey - PrivateKey crypto.PrivateKey + Name string + PublicKey crypto.PublicKey + PrivateKey crypto.PrivateKey + CreateSignerRequest CreateSignerRequest } type CreateSignerRequest struct { + Signer crypto.Signer SigningKey string SigningKeyPEM []byte - Password string + TokenLabel string + PublicKey string + PublicKeyPEM []byte + Password []byte } From e60beeb7fc50d76154ae13dfef47a44bdf6da1be Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:43:39 -0800 Subject: [PATCH 136/163] Make cloudkms more robust. * Automatically create key rings if needed. * User CryptoKeyVersions if needed. * Add support to close the client. * Add new pareters to CreateKey responses to make things easier. --- kms/cloudkms/cloudkms.go | 110 ++++++++++++++++++++++++++++++++++++--- kms/cloudkms/signer.go | 1 - 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index f122bdeb..f734a470 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -3,8 +3,12 @@ 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" @@ -53,9 +57,13 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ } 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. @@ -79,6 +87,14 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { }, nil } +// 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) { @@ -94,8 +110,6 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo switch { case req.Name == "": return nil, errors.New("createKeyRequest 'name' cannot be empty") - case req.Parent == "": - return nil, errors.New("createKeyRequest 'parent' cannot be empty") } protectionLevel, ok := protectionLevelMapping[req.ProtectionLevel] @@ -119,12 +133,22 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo 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: req.Parent, - CryptoKeyId: req.Name, + Parent: keyRing, + CryptoKeyId: keyId, CryptoKey: &kmspb.CryptoKey{ Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ @@ -134,14 +158,68 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo }, }) if err != nil { - return nil, errors.Wrap(err, "cloudKMS CreateCryptoKey failed") + 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 the 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: response.Name, + Name: crytoKeyName, + PublicKey: pk.PublicKey, + 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}) @@ -170,3 +248,23 @@ func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPubli 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/signer.go b/kms/cloudkms/signer.go index a28fd9d8..515e15dc 100644 --- a/kms/cloudkms/signer.go +++ b/kms/cloudkms/signer.go @@ -31,7 +31,6 @@ func (s *signer) Public() crypto.PublicKey { Name: s.signingKey, }) if err != nil { - println(1, err.Error()) return errors.Wrap(err, "cloudKMS GetPublicKey failed") } From a3128a26bb0574e35a8aa2de1f42b9f18c8da634 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:46:01 -0800 Subject: [PATCH 137/163] Add Close method to the key manager interface. --- kms/kms.go | 1 + 1 file changed, 1 insertion(+) diff --git a/kms/kms.go b/kms/kms.go index 5e54451d..524afae7 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -16,6 +16,7 @@ type KeyManager interface { GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, 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. From a9c2db8f985d87b9229985ad4fa3fe96f2c60488 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:46:18 -0800 Subject: [PATCH 138/163] Add close method and fix types in softkms. --- kms/softkms/softkms.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index adb8483e..ec79daeb 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -41,14 +41,21 @@ func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { return &SoftKMS{}, nil } +// Closes 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 != "" { - opts = append(opts, pemutil.WithPassword([]byte(req.Password))) + 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 { @@ -84,11 +91,18 @@ func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon 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 } From 9021951f1aa68ed6f3a8aff2d582df25ff799ca8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:47:05 -0800 Subject: [PATCH 139/163] Fix types. --- authority/authority.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 4af92f5e..1ea0936c 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -96,7 +96,11 @@ func (a *Authority) init() error { // Initialize key manager if it has not been set in the options. if a.keyManager == nil { - a.keyManager, err = kms.New(context.Background(), *a.config.KMS) + 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 } @@ -150,7 +154,7 @@ func (a *Authority) init() error { } signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.IntermediateKey, - Password: a.config.Password, + Password: []byte(a.config.Password), }) if err != nil { return err @@ -164,7 +168,7 @@ func (a *Authority) init() error { if a.config.SSH.HostKey != "" { signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.SSH.HostKey, - Password: a.config.Password, + Password: []byte(a.config.Password), }) if err != nil { return err @@ -180,7 +184,7 @@ func (a *Authority) init() error { if a.config.SSH.UserKey != "" { signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.SSH.UserKey, - Password: a.config.Password, + Password: []byte(a.config.Password), }) if err != nil { return err From ec2046bba8d2bd8203d6429c187be8f045d27470 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 14 Jan 2020 18:51:05 -0800 Subject: [PATCH 140/163] Add grpc dependency. --- go.mod | 1 + go.sum | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a9d507cd..16f3fb91 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( 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 fdc76924..eaf5b628 100644 --- a/go.sum +++ b/go.sum @@ -290,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= @@ -323,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= @@ -576,7 +578,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0= 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= From 927a3b3a86cfa87909b06ea7f13d597551452fa4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 15 Jan 2020 17:27:21 -0800 Subject: [PATCH 141/163] Return crypto.PublicKey on kms.GetPublicKey. --- kms/apiv1/requests.go | 5 ----- kms/cloudkms/cloudkms.go | 7 ++----- kms/kms.go | 2 +- kms/softkms/softkms.go | 24 ++++++++++++++++-------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go index ddcbb108..40187d4f 100644 --- a/kms/apiv1/requests.go +++ b/kms/apiv1/requests.go @@ -94,11 +94,6 @@ type GetPublicKeyRequest struct { Name string } -type GetPublicKeyResponse struct { - Name string - PublicKey crypto.PublicKey -} - type CreateKeyRequest struct { Name string SignatureAlgorithm SignatureAlgorithm diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index f734a470..78a734f0 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -223,7 +223,7 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { // 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) (*apiv1.GetPublicKeyResponse, error) { +func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { ctx, cancel := defaultContext() defer cancel() @@ -239,10 +239,7 @@ func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPubli return nil, err } - return &apiv1.GetPublicKeyResponse{ - Name: req.Name, - PublicKey: pk, - }, nil + return pk, nil } func defaultContext() (context.Context, context.CancelFunc) { diff --git a/kms/kms.go b/kms/kms.go index 524afae7..209783e5 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -13,7 +13,7 @@ import ( // KeyManager is the interface implemented by all the KMS. type KeyManager interface { - GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, error) + GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) Close() error diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index ec79daeb..b5ec6468 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -19,6 +19,9 @@ type algorithmAttributes struct { Curve string } +// DefaultRSAKeySize is the default size for RSA keys. +const DefaultRSAKeySize = 3072 + var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{ apiv1.UnspecifiedSignAlgorithm: algorithmAttributes{"EC", "P-256"}, apiv1.SHA256WithRSA: algorithmAttributes{"RSA", ""}, @@ -33,6 +36,14 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes apiv1.PureEd25519: algorithmAttributes{"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) +} + // SoftKSM is a key manager that uses keys stored in disk. type SoftKMS struct{} @@ -87,7 +98,7 @@ func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon return nil, errors.Errorf("softKMS does not support signature algorithm '%s'", req.SignatureAlgorithm) } - pub, priv, err := keys.GenerateKeyPair(v.Type, v.Curve, req.Bits) + pub, priv, err := generateKey(v.Type, v.Curve, req.Bits) if err != nil { return nil, err } @@ -106,21 +117,18 @@ func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon }, nil } -func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, error) { +func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { v, err := pemutil.Read(req.Name) if err != nil { return nil, err } - switch v.(type) { + 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) } - - return &apiv1.GetPublicKeyResponse{ - Name: req.Name, - PublicKey: v, - }, nil } From a773977a81633ebb689d95777d9e5182046b2212 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 15 Jan 2020 17:29:34 -0800 Subject: [PATCH 142/163] Fix interface change. --- kms/cloudkms/cloudkms.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index 78a734f0..5118d4ae 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -190,7 +190,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo return &apiv1.CreateKeyResponse{ Name: crytoKeyName, - PublicKey: pk.PublicKey, + PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{ SigningKey: crytoKeyName, }, From c250c6ad9107a3754f3092363150006636b908c7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 15 Jan 2020 17:30:17 -0800 Subject: [PATCH 143/163] Add unit tests for softkms. --- kms/softkms/softkms_test.go | 312 ++++++++++++++++++++++++++++++++++ kms/softkms/testdata/cert.crt | 11 ++ kms/softkms/testdata/cert.key | 5 + kms/softkms/testdata/priv.pem | 8 + kms/softkms/testdata/pub.pem | 4 + 5 files changed, 340 insertions(+) create mode 100644 kms/softkms/softkms_test.go create mode 100644 kms/softkms/testdata/cert.crt create mode 100644 kms/softkms/testdata/cert.key create mode 100644 kms/softkms/testdata/priv.pem create mode 100644 kms/softkms/testdata/pub.pem 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----- From 264179cda30b2be06d69cda9969c271233b44ca9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 15 Jan 2020 17:56:50 -0800 Subject: [PATCH 144/163] Add tests for kms and kms/apiv1 packages. --- kms/apiv1/options_test.go | 51 ++++++++++++++++++++++++++++++++++++++ kms/apiv1/requests.go | 4 +++ kms/apiv1/requests_test.go | 51 ++++++++++++++++++++++++++++++++++++++ kms/kms_test.go | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 kms/apiv1/options_test.go create mode 100644 kms/apiv1/requests_test.go create mode 100644 kms/kms_test.go 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 index 40187d4f..35c2fcae 100644 --- a/kms/apiv1/requests.go +++ b/kms/apiv1/requests.go @@ -90,10 +90,12 @@ func (s SignatureAlgorithm) String() string { } } +// 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 @@ -104,6 +106,7 @@ type CreateKeyRequest struct { ProtectionLevel ProtectionLevel } +// CreateKeyResponse is the response value of the kms.CreateKey method. type CreateKeyResponse struct { Name string PublicKey crypto.PublicKey @@ -111,6 +114,7 @@ type CreateKeyResponse struct { CreateSignerRequest CreateSignerRequest } +// CreateSignerRequest is the parameter used in the kms.CreateSigner method. type CreateSignerRequest struct { Signer crypto.Signer SigningKey string 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/kms_test.go b/kms/kms_test.go new file mode 100644 index 00000000..9b041e59 --- /dev/null +++ b/kms/kms_test.go @@ -0,0 +1,45 @@ +package kms + +import ( + "context" + "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 + args args + want KeyManager + wantErr bool + }{ + {"softkms", args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false}, + {"default", args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, false}, + {"cloudkms", args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, true}, // fails because not credentials + {"awskms", args{ctx, apiv1.Options{Type: "awskms"}}, nil, true}, // not yet supported + {"pkcs11", args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not yet supported + {"fail validation", args{ctx, apiv1.Options{Type: "foobar"}}, nil, true}, + } + 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.TypeOf(got) != reflect.TypeOf(tt.want) { + t.Errorf("New() = %T, want %T", got, tt.want) + } + }) + } +} From dff498f17f22efe020679740bc4969ffd1fb48ec Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 15 Jan 2020 19:32:26 -0800 Subject: [PATCH 145/163] Add tests for cloudkms. --- kms/cloudkms/cloudkms.go | 11 +- kms/cloudkms/cloudkms_test.go | 350 ++++++++++++++++++++++++++++++++++ kms/cloudkms/mock_test.go | 46 +++++ kms/cloudkms/signer.go | 1 - kms/cloudkms/signer_test.go | 148 ++++++++++++++ kms/cloudkms/testdata/pub.pem | 4 + 6 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 kms/cloudkms/cloudkms_test.go create mode 100644 kms/cloudkms/mock_test.go create mode 100644 kms/cloudkms/signer_test.go create mode 100644 kms/cloudkms/testdata/pub.pem diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index 5118d4ae..afd0f493 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -107,8 +107,7 @@ func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, // CreateKey creates in Google's Cloud KMS a new asymmetric key for signing. func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { - switch { - case req.Name == "": + if req.Name == "" { return nil, errors.New("createKeyRequest 'name' cannot be empty") } @@ -137,7 +136,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo // 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) + keyRing, keyID := Parent(req.Name) if err := k.createKeyRingIfNeeded(keyRing); err != nil { return nil, err } @@ -148,7 +147,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo // Create private key in CloudKMS. response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ Parent: keyRing, - CryptoKeyId: keyId, + CryptoKeyId: keyID, CryptoKey: &kmspb.CryptoKey{ Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ @@ -224,6 +223,10 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { // 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() diff --git a/kms/cloudkms/cloudkms_test.go b/kms/cloudkms/cloudkms_test.go new file mode 100644 index 00000000..00863b85 --- /dev/null +++ b/kms/cloudkms/cloudkms_test.go @@ -0,0 +1,350 @@ +package cloudkms + +import ( + "context" + "crypto" + "fmt" + "io/ioutil" + "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 + args args + want *CloudKMS + wantErr bool + }{ + {"fail authentication", args{context.Background(), apiv1.Options{}}, nil, true}, + {"fail credentials", args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true}, + } + 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 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 index 515e15dc..be1162ef 100644 --- a/kms/cloudkms/signer.go +++ b/kms/cloudkms/signer.go @@ -36,7 +36,6 @@ func (s *signer) Public() crypto.PublicKey { pk, err := pemutil.ParseKey([]byte(response.Pem)) if err != nil { - println(2, err.Error()) return err } diff --git a/kms/cloudkms/signer_test.go b/kms/cloudkms/signer_test.go new file mode 100644 index 00000000..c8c2fe3f --- /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----- From 5d5ee68d88557d6330762f25ba080fc0dfbeb7d1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 21 Jan 2020 12:50:21 -0800 Subject: [PATCH 146/163] Make GCP client public to facilitate extensibility. --- kms/cloudkms/cloudkms.go | 18 +++++++++--------- kms/cloudkms/cloudkms_test.go | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index afd0f493..5f17b3aa 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -68,7 +68,7 @@ type keyManagementClient interface { // CloudKMS implements a KMS using Google's Cloud apiv1. type CloudKMS struct { - client keyManagementClient + Client keyManagementClient } func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { @@ -83,13 +83,13 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { } return &CloudKMS{ - client: client, + Client: client, }, nil } // Close closes the connection of the Cloud KMS client. func (k *CloudKMS) Close() error { - if err := k.client.Close(); err != nil { + if err := k.Client.Close(); err != nil { return errors.Wrap(err, "cloudKMS Close failed") } return nil @@ -102,7 +102,7 @@ func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, return nil, errors.New("signing key cannot be empty") } - return newSigner(k.client, req.SigningKey), nil + return newSigner(k.Client, req.SigningKey), nil } // CreateKey creates in Google's Cloud KMS a new asymmetric key for signing. @@ -145,7 +145,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo defer cancel() // Create private key in CloudKMS. - response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ + response, err := k.Client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ Parent: keyRing, CryptoKeyId: keyID, CryptoKey: &kmspb.CryptoKey{ @@ -170,7 +170,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo State: kmspb.CryptoKeyVersion_ENABLED, }, } - response, err := k.client.CreateCryptoKeyVersion(ctx, req) + response, err := k.Client.CreateCryptoKeyVersion(ctx, req) if err != nil { return nil, errors.Wrap(err, "cloudKMS CreateCryptoKeyVersion failed") } @@ -200,7 +200,7 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { ctx, cancel := defaultContext() defer cancel() - _, err := k.client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{ + _, err := k.Client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{ Name: name, }) if err == nil { @@ -208,7 +208,7 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { } parent, child := Parent(name) - _, err = k.client.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{ + _, err = k.Client.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{ Parent: parent, KeyRingId: child, }) @@ -230,7 +230,7 @@ func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKe ctx, cancel := defaultContext() defer cancel() - response, err := k.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ + response, err := k.Client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ Name: req.Name, }) if err != nil { diff --git a/kms/cloudkms/cloudkms_test.go b/kms/cloudkms/cloudkms_test.go index 00863b85..85b1e520 100644 --- a/kms/cloudkms/cloudkms_test.go +++ b/kms/cloudkms/cloudkms_test.go @@ -91,7 +91,7 @@ func TestCloudKMS_Close(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - client: tt.fields.client, + Client: tt.fields.client, } if err := k.Close(); (err != nil) != tt.wantErr { t.Errorf("CloudKMS.Close() error = %v, wantErr %v", err, tt.wantErr) @@ -121,7 +121,7 @@ func TestCloudKMS_CreateSigner(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - client: tt.fields.client, + Client: tt.fields.client, } got, err := k.CreateSigner(tt.args.req) if (err != nil) != tt.wantErr { @@ -269,7 +269,7 @@ func TestCloudKMS_CreateKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - client: tt.fields.client, + Client: tt.fields.client, } got, err := k.CreateKey(tt.args.req) if (err != nil) != tt.wantErr { @@ -335,7 +335,7 @@ func TestCloudKMS_GetPublicKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - client: tt.fields.client, + Client: tt.fields.client, } got, err := k.GetPublicKey(tt.args.req) if (err != nil) != tt.wantErr { From fa8116497cf12454c29c607e873438ac98007935 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 21 Jan 2020 19:09:21 -0800 Subject: [PATCH 147/163] Make Signer public and add contructor NewCloudKMS. --- kms/cloudkms/cloudkms.go | 30 ++++++++++++++++++--------- kms/cloudkms/cloudkms_test.go | 38 ++++++++++++++++++++++++++--------- kms/cloudkms/signer.go | 14 ++++++------- kms/cloudkms/signer_test.go | 16 +++++++-------- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index 5f17b3aa..afa840a2 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -56,7 +56,9 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ apiv1.ECDSAWithSHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, } -type keyManagementClient interface { +// 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) @@ -68,9 +70,10 @@ type keyManagementClient interface { // CloudKMS implements a KMS using Google's Cloud apiv1. type CloudKMS struct { - Client keyManagementClient + 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 != "" { @@ -83,13 +86,20 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { } return &CloudKMS{ - Client: client, + 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 { + if err := k.client.Close(); err != nil { return errors.Wrap(err, "cloudKMS Close failed") } return nil @@ -102,7 +112,7 @@ func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, return nil, errors.New("signing key cannot be empty") } - return newSigner(k.Client, req.SigningKey), nil + return NewSigner(k.client, req.SigningKey), nil } // CreateKey creates in Google's Cloud KMS a new asymmetric key for signing. @@ -145,7 +155,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo defer cancel() // Create private key in CloudKMS. - response, err := k.Client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ + response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ Parent: keyRing, CryptoKeyId: keyID, CryptoKey: &kmspb.CryptoKey{ @@ -170,7 +180,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo State: kmspb.CryptoKeyVersion_ENABLED, }, } - response, err := k.Client.CreateCryptoKeyVersion(ctx, req) + response, err := k.client.CreateCryptoKeyVersion(ctx, req) if err != nil { return nil, errors.Wrap(err, "cloudKMS CreateCryptoKeyVersion failed") } @@ -200,7 +210,7 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { ctx, cancel := defaultContext() defer cancel() - _, err := k.Client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{ + _, err := k.client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{ Name: name, }) if err == nil { @@ -208,7 +218,7 @@ func (k *CloudKMS) createKeyRingIfNeeded(name string) error { } parent, child := Parent(name) - _, err = k.Client.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{ + _, err = k.client.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{ Parent: parent, KeyRingId: child, }) @@ -230,7 +240,7 @@ func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKe ctx, cancel := defaultContext() defer cancel() - response, err := k.Client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ + response, err := k.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ Name: req.Name, }) if err != nil { diff --git a/kms/cloudkms/cloudkms_test.go b/kms/cloudkms/cloudkms_test.go index 85b1e520..1776ee17 100644 --- a/kms/cloudkms/cloudkms_test.go +++ b/kms/cloudkms/cloudkms_test.go @@ -76,9 +76,29 @@ func TestNew(t *testing.T) { } } +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 + client KeyManagementClient } tests := []struct { name string @@ -91,7 +111,7 @@ func TestCloudKMS_Close(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - Client: tt.fields.client, + client: tt.fields.client, } if err := k.Close(); (err != nil) != tt.wantErr { t.Errorf("CloudKMS.Close() error = %v, wantErr %v", err, tt.wantErr) @@ -103,7 +123,7 @@ func TestCloudKMS_Close(t *testing.T) { func TestCloudKMS_CreateSigner(t *testing.T) { keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1" type fields struct { - client keyManagementClient + client KeyManagementClient } type args struct { req *apiv1.CreateSignerRequest @@ -115,13 +135,13 @@ func TestCloudKMS_CreateSigner(t *testing.T) { want crypto.Signer wantErr bool }{ - {"ok", fields{&MockClient{}}, args{&apiv1.CreateSignerRequest{SigningKey: keyName}}, &signer{client: &MockClient{}, signingKey: keyName}, false}, + {"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, + client: tt.fields.client, } got, err := k.CreateSigner(tt.args.req) if (err != nil) != tt.wantErr { @@ -150,7 +170,7 @@ func TestCloudKMS_CreateKey(t *testing.T) { } type fields struct { - client keyManagementClient + client KeyManagementClient } type args struct { req *apiv1.CreateKeyRequest @@ -269,7 +289,7 @@ func TestCloudKMS_CreateKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - Client: tt.fields.client, + client: tt.fields.client, } got, err := k.CreateKey(tt.args.req) if (err != nil) != tt.wantErr { @@ -297,7 +317,7 @@ func TestCloudKMS_GetPublicKey(t *testing.T) { } type fields struct { - client keyManagementClient + client KeyManagementClient } type args struct { req *apiv1.GetPublicKeyRequest @@ -335,7 +355,7 @@ func TestCloudKMS_GetPublicKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { k := &CloudKMS{ - Client: tt.fields.client, + client: tt.fields.client, } got, err := k.GetPublicKey(tt.args.req) if (err != nil) != tt.wantErr { diff --git a/kms/cloudkms/signer.go b/kms/cloudkms/signer.go index be1162ef..b9232ca4 100644 --- a/kms/cloudkms/signer.go +++ b/kms/cloudkms/signer.go @@ -9,21 +9,21 @@ import ( kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) -// signer implements a crypto.Signer using Google's Cloud KMS. -type signer struct { - client keyManagementClient +// 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{ +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 { +func (s *Signer) Public() crypto.PublicKey { ctx, cancel := defaultContext() defer cancel() @@ -43,7 +43,7 @@ func (s *signer) Public() crypto.PublicKey { } // 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) { +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { req := &kmspb.AsymmetricSignRequest{ Name: s.signingKey, Digest: &kmspb.Digest{}, diff --git a/kms/cloudkms/signer_test.go b/kms/cloudkms/signer_test.go index c8c2fe3f..9a05e131 100644 --- a/kms/cloudkms/signer_test.go +++ b/kms/cloudkms/signer_test.go @@ -17,19 +17,19 @@ import ( func Test_newSigner(t *testing.T) { type args struct { - c keyManagementClient + c KeyManagementClient signingKey string } tests := []struct { name string args args - want *signer + want *Signer }{ - {"ok", args{&MockClient{}, "signingKey"}, &signer{client: &MockClient{}, signingKey: "signingKey"}}, + {"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) { + if got := NewSigner(tt.args.c, tt.args.signingKey); !reflect.DeepEqual(got, tt.want) { t.Errorf("newSigner() = %v, want %v", got, tt.want) } }) @@ -50,7 +50,7 @@ func Test_signer_Public(t *testing.T) { } type fields struct { - client keyManagementClient + client KeyManagementClient signingKey string } tests := []struct { @@ -78,7 +78,7 @@ func Test_signer_Public(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &signer{ + s := &Signer{ client: tt.fields.client, signingKey: tt.fields.signingKey, } @@ -108,7 +108,7 @@ func Test_signer_Sign(t *testing.T) { } type fields struct { - client keyManagementClient + client KeyManagementClient signingKey string } type args struct { @@ -131,7 +131,7 @@ func Test_signer_Sign(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &signer{ + s := &Signer{ client: tt.fields.client, signingKey: tt.fields.signingKey, } From c387b21808434bdfb38066d2cded12fcb370daf8 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 20 Dec 2019 13:30:05 -0800 Subject: [PATCH 148/163] Introduce generalized statusCoder errors and loads of ssh unit tests. * StatusCoder api errors that have friendly user messages. * Unit tests for SSH sign/renew/rekey/revoke across all provisioners. --- api/api.go | 108 -- api/api_test.go | 31 +- api/renew.go | 36 + api/sign.go | 90 ++ api/ssh.go | 2 +- api/sshRekey.go | 4 +- api/sshRenew.go | 4 +- api/sshRevoke.go | 2 +- authority/authority_test.go | 23 +- authority/authorize.go | 210 ++-- authority/authorize_test.go | 985 ++++++++++++++---- authority/config_test.go | 37 +- authority/db_test.go | 96 -- authority/provisioner/acme.go | 3 +- authority/provisioner/acme_test.go | 128 ++- authority/provisioner/aws.go | 43 +- authority/provisioner/aws_test.go | 332 ++++-- authority/provisioner/azure.go | 35 +- authority/provisioner/azure_test.go | 310 ++++-- authority/provisioner/collection.go | 2 +- authority/provisioner/gcp.go | 41 +- authority/provisioner/gcp_test.go | 324 ++++-- authority/provisioner/jwk.go | 38 +- authority/provisioner/jwk_test.go | 147 ++- authority/provisioner/k8sSA.go | 33 +- authority/provisioner/k8sSA_test.go | 215 +++- authority/provisioner/method.go | 36 +- authority/provisioner/oidc.go | 47 +- authority/provisioner/oidc_test.go | 276 +++-- authority/provisioner/provisioner.go | 15 +- authority/provisioner/provisioner_test.go | 94 ++ authority/provisioner/sign_options.go | 29 +- authority/provisioner/sign_options_test.go | 250 +++-- authority/provisioner/sign_ssh_options.go | 26 +- .../provisioner/sign_ssh_options_test.go | 453 +++++++- authority/provisioner/sshpop.go | 65 +- authority/provisioner/sshpop_test.go | 684 ++++++++++++ .../provisioner/testdata/{ => certs}/bar.pub | 0 .../testdata/{ => certs}/ecdsa.csr | 0 .../testdata/{ => certs}/ed25519.csr | 0 .../provisioner/testdata/{ => certs}/foo.pub | 0 .../testdata/{ => certs}/root_ca.crt | 0 .../provisioner/testdata/{ => certs}/rsa.csr | 0 .../testdata/{ => certs}/short-rsa.csr | 0 .../testdata/certs/ssh_host_ca_key.pub | 1 + .../testdata/certs/ssh_user_ca_key.pub | 1 + .../testdata/{ => certs}/x5c-leaf.crt | 0 .../testdata/{ => secrets}/bar.priv | 0 .../testdata/secrets/bar_host_ssh_key | 5 + .../testdata/{ => secrets}/ecdsa.key | 0 .../testdata/{ => secrets}/ed25519.key | 0 .../testdata/{ => secrets}/foo.priv | 0 .../testdata/secrets/foo_user_ssh_key | 5 + .../testdata/{ => secrets}/rsa.key | 0 .../testdata/secrets/ssh_host_ca_key | 5 + .../testdata/secrets/ssh_user_ca_key | 5 + .../testdata/{ => secrets}/x5c-leaf.key | 0 authority/provisioner/utils_test.go | 88 +- authority/provisioner/x5c.go | 38 +- authority/provisioner/x5c_test.go | 471 +++++---- authority/ssh.go | 256 +---- authority/ssh_test.go | 274 ++++- authority/testdata/certs/ssh_host_ca_key.pub | 1 + authority/testdata/certs/ssh_user_ca_key.pub | 1 + authority/testdata/secrets/ssh_host_ca_key | 5 + authority/testdata/secrets/ssh_user_ca_key | 5 + authority/tls.go | 148 ++- authority/tls_test.go | 335 +++--- ca/ca_test.go | 17 +- ca/client.go | 30 +- ca/client_test.go | 174 ++-- ca/identity/identity_test.go | 1 + db/db.go | 99 ++ errs/error.go | 98 +- {api => errs}/errors_test.go | 14 +- 75 files changed, 5211 insertions(+), 2120 deletions(-) create mode 100644 api/renew.go create mode 100644 api/sign.go delete mode 100644 authority/db_test.go create mode 100644 authority/provisioner/sshpop_test.go rename authority/provisioner/testdata/{ => certs}/bar.pub (100%) rename authority/provisioner/testdata/{ => certs}/ecdsa.csr (100%) rename authority/provisioner/testdata/{ => certs}/ed25519.csr (100%) rename authority/provisioner/testdata/{ => certs}/foo.pub (100%) rename authority/provisioner/testdata/{ => certs}/root_ca.crt (100%) rename authority/provisioner/testdata/{ => certs}/rsa.csr (100%) rename authority/provisioner/testdata/{ => certs}/short-rsa.csr (100%) create mode 100644 authority/provisioner/testdata/certs/ssh_host_ca_key.pub create mode 100644 authority/provisioner/testdata/certs/ssh_user_ca_key.pub rename authority/provisioner/testdata/{ => certs}/x5c-leaf.crt (100%) rename authority/provisioner/testdata/{ => secrets}/bar.priv (100%) create mode 100644 authority/provisioner/testdata/secrets/bar_host_ssh_key rename authority/provisioner/testdata/{ => secrets}/ecdsa.key (100%) rename authority/provisioner/testdata/{ => secrets}/ed25519.key (100%) rename authority/provisioner/testdata/{ => secrets}/foo.priv (100%) create mode 100644 authority/provisioner/testdata/secrets/foo_user_ssh_key rename authority/provisioner/testdata/{ => secrets}/rsa.key (100%) create mode 100644 authority/provisioner/testdata/secrets/ssh_host_ca_key create mode 100644 authority/provisioner/testdata/secrets/ssh_user_ca_key rename authority/provisioner/testdata/{ => secrets}/x5c-leaf.key (100%) create mode 100644 authority/testdata/certs/ssh_host_ca_key.pub create mode 100644 authority/testdata/certs/ssh_user_ca_key.pub create mode 100644 authority/testdata/secrets/ssh_host_ca_key create mode 100644 authority/testdata/secrets/ssh_user_ca_key rename {api => errs}/errors_test.go (87%) diff --git a/api/api.go b/api/api.go index 33aa0f44..c4b307b3 100644 --- a/api/api.go +++ b/api/api.go @@ -5,7 +5,6 @@ import ( "crypto/dsa" "crypto/ecdsa" "crypto/rsa" - "crypto/tls" "crypto/x509" "encoding/asn1" "encoding/base64" @@ -209,14 +208,6 @@ type RootResponse struct { RootPEM Certificate `json:"ca"` } -// SignRequest is the request body for a certificate signature request. -type SignRequest struct { - CsrPEM CertificateRequest `json:"csr"` - OTT string `json:"ott"` - NotAfter TimeDuration `json:"notAfter"` - NotBefore TimeDuration `json:"notBefore"` -} - // ProvisionersResponse is the response object that returns the list of // provisioners. type ProvisionersResponse struct { @@ -230,31 +221,6 @@ type ProvisionerKeyResponse struct { Key string `json:"key"` } -// Validate checks the fields of the SignRequest and returns nil if they are ok -// or an error if something is wrong. -func (s *SignRequest) Validate() error { - if s.CsrPEM.CertificateRequest == nil { - return errs.BadRequest(errors.New("missing csr")) - } - if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil { - return errs.BadRequest(errors.Wrap(err, "invalid csr")) - } - if s.OTT == "" { - return errs.BadRequest(errors.New("missing ott")) - } - - return nil -} - -// SignResponse is the response object of the certificate signature request. -type SignResponse struct { - ServerPEM Certificate `json:"crt"` - CaPEM Certificate `json:"ca"` - CertChainPEM []Certificate `json:"certChain"` - TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"` - TLS *tls.ConnectionState `json:"-"` -} - // RootsResponse is the response object of the roots request. type RootsResponse struct { Certificates []Certificate `json:"crts"` @@ -344,80 +310,6 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate { return certChainPEM } -// Sign is an HTTP handler that reads a certificate request and an -// one-time-token (ott) from the body and creates a new certificate with the -// information in the certificate request. -func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { - var body SignRequest - if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) - return - } - - logOtt(w, body.OTT) - if err := body.Validate(); err != nil { - WriteError(w, err) - return - } - - opts := provisioner.Options{ - NotBefore: body.NotBefore, - NotAfter: body.NotAfter, - } - - signOpts, err := h.Authority.AuthorizeSign(body.OTT) - if err != nil { - WriteError(w, errs.Unauthorized(err)) - return - } - - certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) - if err != nil { - WriteError(w, errs.Forbidden(err)) - return - } - certChainPEM := certChainToPEM(certChain) - var caPEM Certificate - if len(certChainPEM) > 0 { - caPEM = certChainPEM[1] - } - logCertificate(w, certChain[0]) - JSONStatus(w, &SignResponse{ - ServerPEM: certChainPEM[0], - CaPEM: caPEM, - CertChainPEM: certChainPEM, - TLSOptions: h.Authority.GetTLSOptions(), - }, http.StatusCreated) -} - -// Renew uses the information of certificate in the TLS connection to create a -// new one. -func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { - if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { - WriteError(w, errs.BadRequest(errors.New("missing peer certificate"))) - return - } - - certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) - if err != nil { - WriteError(w, errs.Forbidden(err)) - return - } - certChainPEM := certChainToPEM(certChain) - var caPEM Certificate - if len(certChainPEM) > 0 { - caPEM = certChainPEM[1] - } - - logCertificate(w, certChain[0]) - JSONStatus(w, &SignResponse{ - ServerPEM: certChainPEM[0], - CaPEM: caPEM, - CertChainPEM: certChainPEM, - TLSOptions: h.Authority.GetTLSOptions(), - }, http.StatusCreated) -} - // Provisioners returns the list of provisioners configured in the authority. func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := parseCursor(r) diff --git a/api/api_test.go b/api/api_test.go index 70ba6a89..9f40a8e0 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -28,6 +28,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" @@ -914,7 +915,7 @@ func Test_caHandler_Renew(t *testing.T) { {"ok", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated}, {"no tls", nil, nil, nil, nil, http.StatusBadRequest}, {"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest}, - {"renew error", cs, nil, nil, fmt.Errorf("an error"), http.StatusForbidden}, + {"renew error", cs, nil, nil, errs.Forbidden(fmt.Errorf("an error")), http.StatusForbidden}, } expected := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) @@ -934,13 +935,13 @@ func Test_caHandler_Renew(t *testing.T) { res := w.Result() if res.StatusCode != tt.statusCode { - t.Errorf("caHandler.Root StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + t.Errorf("caHandler.Renew StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) } body, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - t.Errorf("caHandler.Root unexpected error = %v", err) + t.Errorf("caHandler.Renew unexpected error = %v", err) } if tt.statusCode < http.StatusBadRequest { if !bytes.Equal(bytes.TrimSpace(body), expected) { @@ -1009,8 +1010,12 @@ func Test_caHandler_Provisioners(t *testing.T) { t.Fatal(err) } - expectedError400 := []byte(`{"status":400,"message":"Bad Request"}`) - expectedError500 := []byte(`{"status":500,"message":"Internal Server Error"}`) + expectedError400 := errs.BadRequest(errors.New("force")) + expectedError400Bytes, err := json.Marshal(expectedError400) + assert.FatalError(t, err) + expectedError500 := errs.InternalServerError(errors.New("force")) + expectedError500Bytes, err := json.Marshal(expectedError500) + assert.FatalError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := &caHandler{ @@ -1035,12 +1040,12 @@ func Test_caHandler_Provisioners(t *testing.T) { } else { switch tt.statusCode { case 400: - if !bytes.Equal(bytes.TrimSpace(body), expectedError400) { - t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError400) + if !bytes.Equal(bytes.TrimSpace(body), expectedError400Bytes) { + t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError400Bytes) } case 500: - if !bytes.Equal(bytes.TrimSpace(body), expectedError500) { - t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError500) + if !bytes.Equal(bytes.TrimSpace(body), expectedError500Bytes) { + t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError500Bytes) } default: t.Errorf("caHandler.Provisioner unexpected status code = %d", tt.statusCode) @@ -1077,7 +1082,9 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { } expected := []byte(`{"key":"` + privKey + `"}`) - expectedError := []byte(`{"status":404,"message":"Not Found"}`) + expectedError404 := errs.NotFound(errors.New("force")) + expectedError404Bytes, err := json.Marshal(expectedError404) + assert.FatalError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1101,8 +1108,8 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expected) } } else { - if !bytes.Equal(bytes.TrimSpace(body), expectedError) { - t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError) + if !bytes.Equal(bytes.TrimSpace(body), expectedError404Bytes) { + t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError404Bytes) } } }) diff --git a/api/renew.go b/api/renew.go new file mode 100644 index 00000000..bc42ec24 --- /dev/null +++ b/api/renew.go @@ -0,0 +1,36 @@ +package api + +import ( + "net/http" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" +) + +// Renew uses the information of certificate in the TLS connection to create a +// new one. +func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + WriteError(w, errs.BadRequest(errors.New("missing peer certificate"))) + return + } + + certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) + if err != nil { + WriteError(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew")) + return + } + certChainPEM := certChainToPEM(certChain) + var caPEM Certificate + if len(certChainPEM) > 0 { + caPEM = certChainPEM[1] + } + + logCertificate(w, certChain[0]) + JSONStatus(w, &SignResponse{ + ServerPEM: certChainPEM[0], + CaPEM: caPEM, + CertChainPEM: certChainPEM, + TLSOptions: h.Authority.GetTLSOptions(), + }, http.StatusCreated) +} diff --git a/api/sign.go b/api/sign.go new file mode 100644 index 00000000..e76f6256 --- /dev/null +++ b/api/sign.go @@ -0,0 +1,90 @@ +package api + +import ( + "crypto/tls" + "net/http" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" + "github.com/smallstep/cli/crypto/tlsutil" +) + +// SignRequest is the request body for a certificate signature request. +type SignRequest struct { + CsrPEM CertificateRequest `json:"csr"` + OTT string `json:"ott"` + NotAfter TimeDuration `json:"notAfter"` + NotBefore TimeDuration `json:"notBefore"` +} + +// Validate checks the fields of the SignRequest and returns nil if they are ok +// or an error if something is wrong. +func (s *SignRequest) Validate() error { + if s.CsrPEM.CertificateRequest == nil { + return errs.BadRequest(errors.New("missing csr")) + } + if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil { + return errs.BadRequest(errors.Wrap(err, "invalid csr")) + } + if s.OTT == "" { + return errs.BadRequest(errors.New("missing ott")) + } + + return nil +} + +// SignResponse is the response object of the certificate signature request. +type SignResponse struct { + ServerPEM Certificate `json:"crt"` + CaPEM Certificate `json:"ca"` + CertChainPEM []Certificate `json:"certChain"` + TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"` + TLS *tls.ConnectionState `json:"-"` +} + +// Sign is an HTTP handler that reads a certificate request and an +// one-time-token (ott) from the body and creates a new certificate with the +// information in the certificate request. +func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { + var body SignRequest + if err := ReadJSON(r.Body, &body); err != nil { + WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + return + } + + logOtt(w, body.OTT) + if err := body.Validate(); err != nil { + WriteError(w, err) + return + } + + opts := provisioner.Options{ + NotBefore: body.NotBefore, + NotAfter: body.NotAfter, + } + + signOpts, err := h.Authority.AuthorizeSign(body.OTT) + if err != nil { + WriteError(w, errs.Unauthorized(err)) + return + } + + certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) + if err != nil { + WriteError(w, errs.Forbidden(err)) + return + } + certChainPEM := certChainToPEM(certChain) + var caPEM Certificate + if len(certChainPEM) > 0 { + caPEM = certChainPEM[1] + } + logCertificate(w, certChain[0]) + JSONStatus(w, &SignResponse{ + ServerPEM: certChainPEM[0], + CaPEM: caPEM, + CertChainPEM: certChainPEM, + TLSOptions: h.Authority.GetTLSOptions(), + }, http.StatusCreated) +} diff --git a/api/ssh.go b/api/ssh.go index f125a95a..2206973b 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -282,7 +282,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { ValidAfter: body.ValidAfter, } - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignSSHMethod) + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { WriteError(w, errs.Unauthorized(err)) diff --git a/api/sshRekey.go b/api/sshRekey.go index aa70cf4f..efeee141 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -56,13 +56,13 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { return } - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RekeySSHMethod) + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { WriteError(w, errs.Unauthorized(err)) return } - oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT) + oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT) if err != nil { WriteError(w, errs.InternalServerError(err)) } diff --git a/api/sshRenew.go b/api/sshRenew.go index 5165bf33..fd4ff1ee 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -46,13 +46,13 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { return } - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RenewSSHMethod) + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod) _, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { WriteError(w, errs.Unauthorized(err)) return } - oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT) + oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT) if err != nil { WriteError(w, errs.InternalServerError(err)) } diff --git a/api/sshRevoke.go b/api/sshRevoke.go index 93e0e450..cd4a3a3e 100644 --- a/api/sshRevoke.go +++ b/api/sshRevoke.go @@ -66,7 +66,7 @@ func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { PassiveOnly: body.Passive, } - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeSSHMethod) + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod) // A token indicates that we are using the api via a provisioner token, // otherwise it is assumed that the certificate is revoking itself over mTLS. logOtt(w, body.OTT) diff --git a/authority/authority_test.go b/authority/authority_test.go index ee517517..e6a65453 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -13,12 +13,13 @@ import ( stepJOSE "github.com/smallstep/cli/jose" ) -func testAuthority(t *testing.T) *Authority { +func testAuthority(t *testing.T, opts ...Option) *Authority { maxjwk, err := stepJOSE.ParseKey("testdata/secrets/max_pub.jwk") assert.FatalError(t, err) clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk") assert.FatalError(t, err) disableRenewal := true + enableSSHCA := true p := provisioner.List{ &provisioner.JWK{ Name: "Max", @@ -29,6 +30,9 @@ func testAuthority(t *testing.T) *Authority { Name: "step-cli", Type: "JWK", Key: clijwk, + Claims: &provisioner.Claims{ + EnableSSHCA: &enableSSHCA, + }, }, &provisioner.JWK{ Name: "dev", @@ -46,19 +50,30 @@ func testAuthority(t *testing.T) *Authority { DisableRenewal: &disableRenewal, }, }, + &provisioner.SSHPOP{ + Name: "sshpop", + Type: "SSHPOP", + Claims: &provisioner.Claims{ + EnableSSHCA: &enableSSHCA, + }, + }, } c := &Config{ Address: "127.0.0.1:443", Root: []string{"testdata/certs/root_ca.crt"}, IntermediateCert: "testdata/certs/intermediate_ca.crt", IntermediateKey: "testdata/secrets/intermediate_ca_key", - DNSNames: []string{"test.ca.smallstep.com"}, - Password: "pass", + SSH: &SSHConfig{ + HostKey: "testdata/secrets/ssh_host_ca_key", + UserKey: "testdata/secrets/ssh_user_ca_key", + }, + DNSNames: []string{"example.com"}, + Password: "pass", AuthorityConfig: &AuthConfig{ Provisioners: p, }, } - a, err := New(c) + a, err := New(c, opts...) assert.FatalError(t, err) return a } diff --git a/authority/authorize.go b/authority/authorize.go index 3353c6b1..cdca026d 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -8,7 +8,9 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" + "golang.org/x/crypto/ssh" ) // Claims extends jose.Claims with step attributes. @@ -36,22 +38,19 @@ func SkipTokenReuseFromContext(ctx context.Context) bool { // authorizeToken parses the token and returns the provisioner used to generate // the token. This method enforces the One-Time use policy (tokens can only be // used once). -func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner.Interface, error) { - var errContext = map[string]interface{}{"ott": ott} - +func (a *Authority) authorizeToken(ctx context.Context, token string) (provisioner.Interface, error) { // Validate payload - token, err := jose.ParseSigned(ott) + tok, err := jose.ParseSigned(token) if err != nil { - return nil, &apiError{errors.Wrapf(err, "authorizeToken: error parsing token"), - http.StatusUnauthorized, errContext} + return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken: error parsing token") } // Get claims w/out verification. We need to look up the provisioner // key in order to verify the claims and we need the issuer from the claims // before we can look up the provisioner. var claims Claims - if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeToken"), http.StatusUnauthorized, errContext} + if err = tok.UnsafeClaimsWithoutVerification(&claims); err != nil { + return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken") } // TODO: use new persistence layer abstraction. @@ -59,29 +58,27 @@ func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner // This check is meant as a stopgap solution to the current lack of a persistence layer. if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck { if claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) { - return nil, &apiError{errors.New("authorizeToken: token issued before the bootstrap of certificate authority"), - http.StatusUnauthorized, errContext} + return nil, errs.Unauthorized(errors.New("authority.authorizeToken: token issued before the bootstrap of certificate authority")) } } // This method will also validate the audiences for JWK provisioners. - p, ok := a.provisioners.LoadByToken(token, &claims.Claims) + p, ok := a.provisioners.LoadByToken(tok, &claims.Claims) if !ok { - return nil, &apiError{ - errors.Errorf("authorizeToken: provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")), - http.StatusUnauthorized, errContext} + return nil, errs.Unauthorized(errors.Errorf("authority.authorizeToken: provisioner "+ + "not found or invalid audience (%s)", strings.Join(claims.Audience, ", "))) } // Store the token to protect against reuse unless it's skipped. if !SkipTokenReuseFromContext(ctx) { - if reuseKey, err := p.GetTokenID(ott); err == nil { - ok, err := a.db.UseToken(reuseKey, ott) + if reuseKey, err := p.GetTokenID(token); err == nil { + ok, err := a.db.UseToken(reuseKey, token) if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when attempting to store token"), - http.StatusInternalServerError, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.authorizeToken: failed when attempting to store token") } if !ok { - return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext} + return nil, errs.Unauthorized(errors.Errorf("authority.authorizeToken: token already used")) } } } @@ -89,125 +86,158 @@ func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner return p, nil } -// Authorize grabs the method from the context and authorizes a signature -// request by validating the one-time-token. -func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - var errContext = apiCtx{"ott": ott} +// Authorize grabs the method from the context and authorizes the request by +// validating the one-time-token. +func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner.SignOption, error) { + var opts = []errs.Option{errs.WithKeyVal("token", token)} + switch m := provisioner.MethodFromContext(ctx); m { case provisioner.SignMethod: - return a.authorizeSign(ctx, ott) + signOpts, err := a.authorizeSign(ctx, token) + return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) case provisioner.RevokeMethod: - return nil, a.authorizeRevoke(ctx, ott) - case provisioner.SignSSHMethod: + return nil, errs.Wrap(http.StatusInternalServerError, a.authorizeRevoke(ctx, token), "authority.Authorize", opts...) + case provisioner.SSHSignMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { - return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} + return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) } - return a.authorizeSSHSign(ctx, ott) - case provisioner.RenewSSHMethod: + _, err := a.authorizeSSHSign(ctx, token) + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) + case provisioner.SSHRenewMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { - return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} - } - if _, err := a.authorizeSSHRenew(ctx, ott); err != nil { - return nil, err + return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) } - return nil, nil - case provisioner.RevokeSSHMethod: - return nil, a.authorizeSSHRevoke(ctx, ott) - case provisioner.RekeySSHMethod: + _, err := a.authorizeSSHRenew(ctx, token) + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) + case provisioner.SSHRevokeMethod: + return nil, errs.Wrap(http.StatusInternalServerError, a.authorizeSSHRevoke(ctx, token), "authority.Authorize", opts...) + case provisioner.SSHRekeyMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { - return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} + return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) } - _, opts, err := a.authorizeSSHRekey(ctx, ott) - if err != nil { - return nil, err - } - return opts, nil + _, signOpts, err := a.authorizeSSHRekey(ctx, token) + return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) default: - return nil, &apiError{errors.Errorf("authorize: method %d is not supported", m), http.StatusInternalServerError, errContext} + return nil, errs.InternalServerError(errors.Errorf("authority.Authorize; method %d is not supported", m), opts...) } } -// authorizeSign loads the provisioner from the token, checks that it has not -// been used again and calls the provisioner AuthorizeSign method. Returns a -// list of methods to apply to the signing flow. -func (a *Authority) authorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - var errContext = apiCtx{"ott": ott} - p, err := a.authorizeToken(ctx, ott) +// authorizeSign loads the provisioner from the token and calls the provisioner +// AuthorizeSign method. Returns a list of methods to apply to the signing flow. +func (a *Authority) authorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) { + p, err := a.authorizeToken(ctx, token) if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSign") } - opts, err := p.AuthorizeSign(ctx, ott) + signOpts, err := p.AuthorizeSign(ctx, token) if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSign") } - return opts, nil + return signOpts, nil } // AuthorizeSign authorizes a signature request by validating and authenticating -// a OTT that must be sent w/ the request. +// a token that must be sent w/ the request. // // NOTE: This method is deprecated and should not be used. We make it available // in the short term os as not to break existing clients. -func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { +func (a *Authority) AuthorizeSign(token string) ([]provisioner.SignOption, error) { ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) - return a.Authorize(ctx, ott) + return a.Authorize(ctx, token) } -// authorizeRevoke authorizes a revocation request by validating and authenticating -// the RevokeOptions POSTed with the request. -// Returns a tuple of the provisioner ID and error, if one occurred. +// authorizeRevoke locates the provisioner used to generate the authenticating +// token and then performs the token validation flow. func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { - errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(ctx, token) if err != nil { - return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRevoke") } if err = p.AuthorizeRevoke(ctx, token); err != nil { - return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRevoke") } return nil } -// authorizeRenewl tries to locate the step provisioner extension, and checks +// authorizeRenew locates the provisioner (using the provisioner extension in the cert), and checks // if for the configured provisioner, the renewal is enabled or not. If the // extra extension cannot be found, authorize the renewal by default. // // TODO(mariano): should we authorize by default? -func (a *Authority) authorizeRenew(crt *x509.Certificate) error { - errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()} +func (a *Authority) authorizeRenew(cert *x509.Certificate) error { + var opts = []errs.Option{errs.WithKeyVal("serialNumber", cert.SerialNumber.String())} // Check the passive revocation table. - isRevoked, err := a.db.IsRevoked(crt.SerialNumber.String()) + isRevoked, err := a.db.IsRevoked(cert.SerialNumber.String()) if err != nil { - return &apiError{ - err: errors.Wrap(err, "renew"), - code: http.StatusInternalServerError, - context: errContext, - } + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) } if isRevoked { - return &apiError{ - err: errors.New("renew: certificate has been revoked"), - code: http.StatusUnauthorized, - context: errContext, - } + return errs.Unauthorized(errors.New("authority.authorizeRenew: certificate has been revoked"), opts...) } - p, ok := a.provisioners.LoadByCertificate(crt) + p, ok := a.provisioners.LoadByCertificate(cert) if !ok { - return &apiError{ - err: errors.New("renew: provisioner not found"), - code: http.StatusUnauthorized, - context: errContext, - } + return errs.Unauthorized(errors.New("authority.authorizeRenew: provisioner not found"), opts...) } - if err := p.AuthorizeRenew(context.Background(), crt); err != nil { - return &apiError{ - err: errors.Wrap(err, "renew"), - code: http.StatusUnauthorized, - context: errContext, - } + if err := p.AuthorizeRenew(context.Background(), cert); err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) + } + return nil +} + +// authorizeSSHSign loads the provisioner from the token, checks that it has not +// been used again and calls the provisioner AuthorizeSSHSign method. Returns a +// list of methods to apply to the signing flow. +func (a *Authority) authorizeSSHSign(ctx context.Context, token string) ([]provisioner.SignOption, error) { + p, err := a.authorizeToken(ctx, token) + if err != nil { + return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeSSHSign") + } + signOpts, err := p.AuthorizeSSHSign(ctx, token) + if err != nil { + return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeSSHSign") + } + return signOpts, nil +} + +// authorizeSSHRenew authorizes an SSH certificate renewal request, by +// validating the contents of an SSHPOP token. +func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { + p, err := a.authorizeToken(ctx, token) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRenew") + } + cert, err := p.AuthorizeSSHRenew(ctx, token) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRenew") + } + return cert, nil +} + +// authorizeSSHRekey authorizes an SSH certificate rekey request, by +// validating the contents of an SSHPOP token. +func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) { + p, err := a.authorizeToken(ctx, token) + if err != nil { + return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRekey") + } + cert, signOpts, err := p.AuthorizeSSHRekey(ctx, token) + if err != nil { + return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRekey") + } + return cert, signOpts, nil +} + +// authorizeSSHRevoke authorizes an SSH certificate revoke request, by +// validating the contents of an SSHPOP token. +func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error { + p, err := a.authorizeToken(ctx, token) + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRevoke") + } + if err = p.AuthorizeSSHRevoke(ctx, token); err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRevoke") } return nil } diff --git a/authority/authorize_test.go b/authority/authorize_test.go index 5e112e95..6f7bf940 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -2,25 +2,58 @@ package authority import ( "context" + "crypto" + "crypto/rand" "crypto/x509" + "encoding/base64" + "fmt" "net/http" + "strconv" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/jose" + "golang.org/x/crypto/ssh" "gopkg.in/square/go-jose.v2/jwt" ) -func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose.JSONWebKey) (string, error) { - sig, err := jose.NewSigner( - jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, - new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), - ) +var testAudiences = provisioner.Audiences{ + Sign: []string{"https://example.com/1.0/sign", "https://example.com/sign"}, + Revoke: []string{"https://example.com/1.0/revoke", "https://example.com/revoke"}, + SSHSign: []string{"https://example.com/1.0/ssh/sign"}, + SSHRevoke: []string{"https://example.com/1.0/ssh/revoke"}, + SSHRenew: []string{"https://example.com/1.0/ssh/renew"}, + SSHRekey: []string{"https://example.com/1.0/ssh/rekey"}, +} + +type tokOption func(*jose.SignerOptions) error + +func withSSHPOPFile(cert *ssh.Certificate) tokOption { + return func(so *jose.SignerOptions) error { + so.WithHeader("sshpop", base64.StdEncoding.EncodeToString(cert.Marshal())) + return nil + } +} + +func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { + so := new(jose.SignerOptions) + so.WithType("JWT") + so.WithHeader("kid", jwk.KeyID) + + for _, o := range tokOpts { + if err := o(so); err != nil { + return "", err + } + } + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so) if err != nil { return "", err } @@ -61,20 +94,21 @@ func TestAuthority_authorizeToken(t *testing.T) { now := time.Now().UTC() validIssuer := "step-cli" - validAudience := []string{"https://test.ca.smallstep.com/revoke"} + validAudience := []string{"https://example.com/revoke"} type authorizeTest struct { - auth *Authority - ott string - err *apiError + auth *Authority + token string + err error + code int } tests := map[string]func(t *testing.T) *authorizeTest{ - "fail/invalid-ott": func(t *testing.T) *authorizeTest { + "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ - auth: a, - ott: "foo", - err: &apiError{errors.New("authorizeToken: error parsing token"), - http.StatusUnauthorized, apiCtx{"ott": "foo"}}, + auth: a, + token: "foo", + err: errors.New("authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, } }, "fail/prehistoric-token": func(t *testing.T) *authorizeTest { @@ -90,10 +124,10 @@ func TestAuthority_authorizeToken(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, - err: &apiError{errors.New("authorizeToken: token issued before the bootstrap of certificate authority"), - http.StatusUnauthorized, apiCtx{"ott": raw}}, + auth: a, + token: raw, + err: errors.New("authority.authorizeToken: token issued before the bootstrap of certificate authority"), + code: http.StatusUnauthorized, } }, "fail/provisioner-not-found": func(t *testing.T) *authorizeTest { @@ -112,10 +146,10 @@ func TestAuthority_authorizeToken(t *testing.T) { raw, err := jwt.Signed(_sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, - err: &apiError{errors.New("authorizeToken: provisioner not found or invalid audience (https://test.ca.smallstep.com/revoke)"), - http.StatusUnauthorized, apiCtx{"ott": raw}}, + auth: a, + token: raw, + err: errors.New("authority.authorizeToken: provisioner not found or invalid audience (https://example.com/revoke)"), + code: http.StatusUnauthorized, } }, "ok/simpledb": func(t *testing.T) *authorizeTest { @@ -130,8 +164,8 @@ func TestAuthority_authorizeToken(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, + auth: a, + token: raw, } }, "fail/simpledb/token-already-used": func(t *testing.T) *authorizeTest { @@ -149,16 +183,16 @@ func TestAuthority_authorizeToken(t *testing.T) { _, err = _a.authorizeToken(context.TODO(), raw) assert.FatalError(t, err) return &authorizeTest{ - auth: _a, - ott: raw, - err: &apiError{errors.New("authorizeToken: token already used"), - http.StatusUnauthorized, apiCtx{"ott": raw}}, + auth: _a, + token: raw, + err: errors.New("authority.authorizeToken: token already used"), + code: http.StatusUnauthorized, } }, "ok/mockNoSQLDB": func(t *testing.T) *authorizeTest { _a := testAuthority(t) - _a.db = &MockAuthDB{ - useToken: func(id, tok string) (bool, error) { + _a.db = &db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { return true, nil }, } @@ -174,14 +208,14 @@ func TestAuthority_authorizeToken(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: _a, - ott: raw, + auth: _a, + token: raw, } }, "fail/mockNoSQLDB/error": func(t *testing.T) *authorizeTest { _a := testAuthority(t) - _a.db = &MockAuthDB{ - useToken: func(id, tok string) (bool, error) { + _a.db = &db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { return false, errors.New("force") }, } @@ -197,16 +231,16 @@ func TestAuthority_authorizeToken(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: _a, - ott: raw, - err: &apiError{errors.New("authorizeToken: failed when checking if token already used: force"), - http.StatusInternalServerError, apiCtx{"ott": raw}}, + auth: _a, + token: raw, + err: errors.New("authority.authorizeToken: failed when attempting to store token: force"), + code: http.StatusInternalServerError, } }, "fail/mockNoSQLDB/token-already-used": func(t *testing.T) *authorizeTest { _a := testAuthority(t) - _a.db = &MockAuthDB{ - useToken: func(id, tok string) (bool, error) { + _a.db = &db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { return false, nil }, } @@ -222,10 +256,10 @@ func TestAuthority_authorizeToken(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: _a, - ott: raw, - err: &apiError{errors.New("authorizeToken: token already used"), - http.StatusUnauthorized, apiCtx{"ott": raw}}, + auth: _a, + token: raw, + err: errors.New("authority.authorizeToken: token already used"), + code: http.StatusUnauthorized, } }, } @@ -234,17 +268,13 @@ func TestAuthority_authorizeToken(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - p, err := tc.auth.authorizeToken(context.TODO(), tc.ott) + p, err := tc.auth.authorizeToken(context.TODO(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { @@ -268,20 +298,21 @@ func TestAuthority_authorizeRevoke(t *testing.T) { now := time.Now().UTC() validIssuer := "step-cli" - validAudience := []string{"https://test.ca.smallstep.com/revoke"} + validAudience := []string{"https://example.com/revoke"} type authorizeTest struct { auth *Authority token string - opts *RevokeOptions err error + code int } tests := map[string]func(t *testing.T) *authorizeTest{ - "fail/token/invalid-ott": func(t *testing.T) *authorizeTest { + "fail/token/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ - auth: a, - opts: &RevokeOptions{OTT: "foo"}, - err: errors.New("authorizeRevoke: authorizeToken: error parsing token"), + auth: a, + token: "foo", + err: errors.New("authority.authorizeRevoke: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, } }, "fail/token/invalid-subject": func(t *testing.T) *authorizeTest { @@ -296,9 +327,10 @@ func TestAuthority_authorizeRevoke(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - opts: &RevokeOptions{OTT: raw}, - err: errors.New("authorizeRevoke: token subject cannot be empty"), + auth: a, + token: raw, + err: errors.New("authority.authorizeRevoke: jwk.AuthorizeRevoke: jwk.authorizeToken; jwk token subject cannot be empty"), + code: http.StatusUnauthorized, } }, "ok/token": func(t *testing.T) *authorizeTest { @@ -313,34 +345,8 @@ func TestAuthority_authorizeRevoke(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - opts: &RevokeOptions{OTT: raw}, - } - }, - "fail/mTLS/invalid-serial": func(t *testing.T) *authorizeTest { - crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") - assert.FatalError(t, err) - return &authorizeTest{ - auth: a, - opts: &RevokeOptions{MTLS: true, Crt: crt, Serial: "foo"}, - err: errors.New("authorizeRevoke: serial number in certificate different than body"), - } - }, - "fail/mTLS/load-provisioner": func(t *testing.T) *authorizeTest { - crt, err := pemutil.ReadCertificate("./testdata/certs/provisioner-not-found.crt") - assert.FatalError(t, err) - return &authorizeTest{ - auth: a, - opts: &RevokeOptions{MTLS: true, Crt: crt, Serial: "41633491264736369593451462439668497527"}, - err: errors.New("authorizeRevoke: provisioner not found"), - } - }, - "ok/mTLS": func(t *testing.T) *authorizeTest { - crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") - assert.FatalError(t, err) - return &authorizeTest{ - auth: a, - opts: &RevokeOptions{MTLS: true, Crt: crt, Serial: "102012593071130646873265215610956555026"}, + auth: a, + token: raw, } }, } @@ -351,6 +357,9 @@ func TestAuthority_authorizeRevoke(t *testing.T) { if err := tc.auth.authorizeRevoke(context.TODO(), tc.token); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -360,7 +369,7 @@ func TestAuthority_authorizeRevoke(t *testing.T) { } } -func TestAuthority_AuthorizeSign(t *testing.T) { +func TestAuthority_authorizeSign(t *testing.T) { a := testAuthority(t) jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) @@ -373,20 +382,21 @@ func TestAuthority_AuthorizeSign(t *testing.T) { now := time.Now().UTC() validIssuer := "step-cli" - validAudience := []string{"https://test.ca.smallstep.com/sign"} + validAudience := []string{"https://example.com/sign"} type authorizeTest struct { - auth *Authority - ott string - err *apiError + auth *Authority + token string + err error + code int } tests := map[string]func(t *testing.T) *authorizeTest{ - "fail/invalid-ott": func(t *testing.T) *authorizeTest { + "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ - auth: a, - ott: "foo", - err: &apiError{errors.New("authorizeSign: authorizeToken: error parsing token"), - http.StatusUnauthorized, apiCtx{"ott": "foo"}}, + auth: a, + token: "foo", + err: errors.New("authority.authorizeSign: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, } }, "fail/invalid-subject": func(t *testing.T) *authorizeTest { @@ -401,10 +411,10 @@ func TestAuthority_AuthorizeSign(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, - err: &apiError{errors.New("authorizeSign: token subject cannot be empty"), - http.StatusUnauthorized, apiCtx{"ott": raw}}, + auth: a, + token: raw, + err: errors.New("authority.authorizeSign: jwk.AuthorizeSign: jwk.authorizeToken; jwk token subject cannot be empty"), + code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { @@ -419,8 +429,8 @@ func TestAuthority_AuthorizeSign(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, + auth: a, + token: raw, } }, } @@ -429,18 +439,13 @@ func TestAuthority_AuthorizeSign(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - got, err := tc.auth.AuthorizeSign(tc.ott) + got, err := tc.auth.authorizeSign(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { - assert.Nil(t, got) - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { @@ -451,7 +456,6 @@ func TestAuthority_AuthorizeSign(t *testing.T) { } } -// TODO: remove once Authorize deprecated. func TestAuthority_Authorize(t *testing.T) { a := testAuthority(t) @@ -463,56 +467,229 @@ func TestAuthority_Authorize(t *testing.T) { assert.FatalError(t, err) now := time.Now().UTC() - validIssuer := "step-cli" - validAudience := []string{"https://test.ca.smallstep.com/sign"} type authorizeTest struct { - auth *Authority - ott string - err *apiError + auth *Authority + token string + ctx context.Context + err error + code int } tests := map[string]func(t *testing.T) *authorizeTest{ - "fail/invalid-ott": func(t *testing.T) *authorizeTest { + "default-to-signMethod": func(t *testing.T) *authorizeTest { return &authorizeTest{ - auth: a, - ott: "foo", - err: &apiError{errors.New("authorizeSign: authorizeToken: error parsing token"), - http.StatusUnauthorized, apiCtx{"ott": "foo"}}, + auth: a, + token: "foo", + ctx: context.Background(), + err: errors.New("authority.Authorize: authority.authorizeSign: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, } }, - "fail/invalid-subject": func(t *testing.T) *authorizeTest { + "fail/sign/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod), + err: errors.New("authority.Authorize: authority.authorizeSign: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "ok/sign": func(t *testing.T) *authorizeTest { cl := jwt.Claims{ - Subject: "", + Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validAudience, - ID: "43", + Audience: testAudiences.Sign, + ID: "1", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + token, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, - err: &apiError{errors.New("authorizeSign: token subject cannot be empty"), - http.StatusUnauthorized, apiCtx{"ott": raw}}, + auth: a, + token: token, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod), } }, - "ok": func(t *testing.T) *authorizeTest { + "fail/revoke/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod), + err: errors.New("authority.Authorize: authority.authorizeRevoke: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "ok/revoke": func(t *testing.T) *authorizeTest { cl := jwt.Claims{ Subject: "test.smallstep.com", Issuer: validIssuer, NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Minute)), - Audience: validAudience, - ID: "44", + Audience: testAudiences.Revoke, + ID: "2", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + token, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, + auth: a, + token: token, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod), + } + }, + "fail/sshSign/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), + err: errors.New("authority.Authorize: authority.authorizeSSHSign: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/sshSign/disabled": func(t *testing.T) *authorizeTest { + _a := testAuthority(t) + _a.sshCAHostCertSignKey = nil + _a.sshCAUserCertSignKey = nil + return &authorizeTest{ + auth: _a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), + err: errors.New("authority.Authorize; ssh certificate flows are not enabled"), + code: http.StatusNotImplemented, + } + }, + "ok/sshSign": func(t *testing.T) *authorizeTest { + raw, err := generateSimpleSSHUserToken(validIssuer, testAudiences.SSHSign[0], jwk) + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: raw, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod), + } + }, + "fail/sshRenew/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), + err: errors.New("authority.Authorize: authority.authorizeSSHRenew: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/sshRenew/disabled": func(t *testing.T) *authorizeTest { + _a := testAuthority(t) + _a.sshCAHostCertSignKey = nil + _a.sshCAUserCertSignKey = nil + return &authorizeTest{ + auth: _a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), + err: errors.New("authority.Authorize; ssh certificate flows are not enabled"), + code: http.StatusNotImplemented, + } + }, + "ok/sshRenew": func(t *testing.T) *authorizeTest { + key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) + assert.FatalError(t, err) + + p, ok := a.provisioners.Load("sshpop/sshpop") + assert.Fatal(t, ok, "sshpop provisioner not found in test authority") + + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0]+"#sshpop/sshpop", + []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: tok, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod), + } + }, + "fail/sshRevoke/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), + err: errors.New("authority.Authorize: authority.authorizeSSHRevoke: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "ok/sshRevoke": func(t *testing.T) *authorizeTest { + cl := jwt.Claims{ + Subject: "test.smallstep.com", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: testAudiences.SSHRevoke, + ID: "3", + } + token, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: token, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod), + } + }, + "fail/sshRekey/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), + err: errors.New("authority.Authorize: authority.authorizeSSHRekey: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/sshRekey/disabled": func(t *testing.T) *authorizeTest { + _a := testAuthority(t) + _a.sshCAHostCertSignKey = nil + _a.sshCAUserCertSignKey = nil + return &authorizeTest{ + auth: _a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), + err: errors.New("authority.Authorize; ssh certificate flows are not enabled"), + code: http.StatusNotImplemented, + } + }, + "ok/sshRekey": func(t *testing.T) *authorizeTest { + key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) + assert.FatalError(t, err) + + p, ok := a.provisioners.Load("sshpop/sshpop") + assert.Fatal(t, ok, "sshpop provisioner not found in test authority") + + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0]+"#sshpop/sshpop", + []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + + return &authorizeTest{ + auth: a, + token: tok, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod), + } + }, + "fail/unexpected-method": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: provisioner.NewContextWithMethod(context.Background(), 15), + err: errors.New("authority.Authorize; method 15 is not supported"), + code: http.StatusInternalServerError, } }, } @@ -520,30 +697,27 @@ func TestAuthority_Authorize(t *testing.T) { for name, genTestCase := range tests { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) - got, err := tc.auth.Authorize(ctx, tc.ott) + got, err := tc.auth.Authorize(tc.ctx, tc.token) if err != nil { - if assert.NotNil(t, tc.err) { + if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { assert.Nil(t, got) - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + + ctxErr, ok := err.(*errs.Error) + assert.Fatal(t, ok, "error is not of type *errs.Error") + assert.Equals(t, ctxErr.Details["token"], tc.token) } } else { - if assert.Nil(t, tc.err) { - assert.Len(t, 8, got) - } + assert.Nil(t, tc.err) } }) } } -func TestAuthority_authorizeRenewal(t *testing.T) { +func TestAuthority_authorizeRenew(t *testing.T) { fooCrt, err := pemutil.ReadCertificate("testdata/certs/foo.crt") assert.FatalError(t, err) @@ -555,78 +729,79 @@ func TestAuthority_authorizeRenewal(t *testing.T) { type authorizeTest struct { auth *Authority - crt *x509.Certificate - err *apiError + cert *x509.Certificate + err error + code int } tests := map[string]func(t *testing.T) *authorizeTest{ "fail/db.IsRevoked-error": func(t *testing.T) *authorizeTest { a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { return false, errors.New("force") }, } return &authorizeTest{ auth: a, - crt: fooCrt, - err: &apiError{errors.New("renew: force"), - http.StatusInternalServerError, apiCtx{"serialNumber": "102012593071130646873265215610956555026"}}, + cert: fooCrt, + err: errors.New("authority.authorizeRenew: force"), + code: http.StatusInternalServerError, } }, "fail/revoked": func(t *testing.T) *authorizeTest { a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { return true, nil }, } return &authorizeTest{ auth: a, - crt: fooCrt, - err: &apiError{errors.New("renew: certificate has been revoked"), - http.StatusUnauthorized, apiCtx{"serialNumber": "102012593071130646873265215610956555026"}}, + cert: fooCrt, + err: errors.New("authority.authorizeRenew: certificate has been revoked"), + code: http.StatusUnauthorized, } }, "fail/load-provisioner": func(t *testing.T) *authorizeTest { a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { return false, nil }, } return &authorizeTest{ auth: a, - crt: otherCrt, - err: &apiError{errors.New("renew: provisioner not found"), - http.StatusUnauthorized, apiCtx{"serialNumber": "41633491264736369593451462439668497527"}}, + cert: otherCrt, + err: errors.New("authority.authorizeRenew: provisioner not found"), + code: http.StatusUnauthorized, } }, "fail/provisioner-authorize-renewal-fail": func(t *testing.T) *authorizeTest { a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { return false, nil }, } return &authorizeTest{ auth: a, - crt: renewDisabledCrt, - err: &apiError{errors.New("renew: renew is disabled for provisioner renew_disabled:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"), - http.StatusUnauthorized, apiCtx{"serialNumber": "119772236532068856521070735128919532568"}}, + cert: renewDisabledCrt, + err: errors.New("authority.authorizeRenew: jwk.AuthorizeRenew; renew is disabled for jwk provisioner renew_disabled:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"), + code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { return false, nil }, } return &authorizeTest{ auth: a, - crt: fooCrt, + cert: fooCrt, } }, } @@ -635,17 +810,17 @@ func TestAuthority_authorizeRenewal(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - err := tc.auth.authorizeRenew(tc.crt) + err := tc.auth.authorizeRenew(tc.cert) if err != nil { if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + + ctxErr, ok := err.(*errs.Error) + assert.Fatal(t, ok, "error is not of type *errs.Error") + assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String()) } } else { assert.Nil(t, tc.err) @@ -653,3 +828,431 @@ func TestAuthority_authorizeRenewal(t *testing.T) { }) } } + +func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) { + return generateSSHToken("subject@localhost", iss, aud, time.Now(), &provisioner.SSHOptions{ + CertType: "user", + Principals: []string{"name"}, + }, jwk) +} + +type stepPayload struct { + SSH *provisioner.SSHOptions `json:"ssh,omitempty"` +} + +func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *provisioner.SSHOptions, jwk *jose.JSONWebKey) (string, error) { + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), + ) + if err != nil { + return "", err + } + + id, err := randutil.ASCII(64) + if err != nil { + return "", err + } + + claims := struct { + jose.Claims + Step *stepPayload `json:"step,omitempty"` + }{ + Claims: jose.Claims{ + ID: id, + Subject: sub, + Issuer: iss, + IssuedAt: jose.NewNumericDate(iat), + NotBefore: jose.NewNumericDate(iat), + Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)), + Audience: []string{aud}, + }, + Step: &stepPayload{ + SSH: sshOpts, + }, + } + return jose.Signed(sig).Claims(claims).CompactSerialize() +} + +func createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, *jose.JSONWebKey, error) { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "foo", 0) + if err != nil { + return nil, nil, err + } + cert.Key, err = ssh.NewPublicKey(jwk.Public().Key) + if err != nil { + return nil, nil, err + } + if err = cert.SignCert(rand.Reader, signer); err != nil { + return nil, nil, err + } + return cert, jwk, nil +} + +func TestAuthority_authorizeSSHSign(t *testing.T) { + a := testAuthority(t) + + jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) + assert.FatalError(t, err) + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) + assert.FatalError(t, err) + + now := time.Now().UTC() + + validIssuer := "step-cli" + validAudience := []string{"https://example.com/ssh/sign"} + + type authorizeTest struct { + auth *Authority + token string + err error + code int + } + tests := map[string]func(t *testing.T) *authorizeTest{ + "fail/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + err: errors.New("authority.authorizeSSHSign: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/invalid-subject": func(t *testing.T) *authorizeTest { + cl := jwt.Claims{ + Subject: "", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: validAudience, + ID: "43", + } + raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: raw, + err: errors.New("authority.authorizeSSHSign: jwk.AuthorizeSSHSign: jwk.authorizeToken; jwk token subject cannot be empty"), + code: http.StatusUnauthorized, + } + }, + "ok": func(t *testing.T) *authorizeTest { + raw, err := generateSimpleSSHUserToken(validIssuer, validAudience[0], jwk) + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: raw, + } + }, + } + + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token) + if err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Len(t, 11, got) + } + } + }) + } +} + +func TestAuthority_authorizeSSHRenew(t *testing.T) { + a := testAuthority(t) + + jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) + assert.FatalError(t, err) + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) + assert.FatalError(t, err) + + now := time.Now().UTC() + + validIssuer := "step-cli" + + type authorizeTest struct { + auth *Authority + token string + cert *ssh.Certificate + err error + code int + } + tests := map[string]func(t *testing.T) *authorizeTest{ + "fail/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + err: errors.New("authority.authorizeSSHRenew: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/sshRenew-unimplemented-jwk-provisioner": func(t *testing.T) *authorizeTest { + cl := jwt.Claims{ + Subject: "", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: testAudiences.SSHRenew, + ID: "43", + } + raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: raw, + err: errors.New("authority.authorizeSSHRenew: provisioner.AuthorizeSSHRenew not implemented"), + code: http.StatusUnauthorized, + } + }, + "ok": func(t *testing.T) *authorizeTest { + key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) + assert.FatalError(t, err) + + p, ok := a.provisioners.Load("sshpop/sshpop") + assert.Fatal(t, ok, "sshpop provisioner not found in test authority") + + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0]+"#sshpop/sshpop", + []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + + return &authorizeTest{ + auth: a, + token: tok, + cert: cert, + } + }, + } + + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + got, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token) + if err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, tc.cert.Serial, got.Serial) + } + } + }) + } +} + +func TestAuthority_authorizeSSHRevoke(t *testing.T) { + a := testAuthority(t, []Option{WithDatabase(&db.MockAuthDB{ + MIsSSHRevoked: func(serial string) (bool, error) { + return false, nil + }, + MUseToken: func(id, tok string) (bool, error) { + return true, nil + }, + })}...) + + jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) + assert.FatalError(t, err) + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) + assert.FatalError(t, err) + + now := time.Now().UTC() + validIssuer := "step-cli" + + type authorizeTest struct { + auth *Authority + token string + cert *ssh.Certificate + err error + code int + } + tests := map[string]func(t *testing.T) *authorizeTest{ + "fail/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + err: errors.New("authority.authorizeSSHRevoke: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/invalid-subject": func(t *testing.T) *authorizeTest { + cl := jwt.Claims{ + Subject: "", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: testAudiences.SSHRevoke, + ID: "43", + } + raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: raw, + err: errors.New("authority.authorizeSSHRevoke: jwk.AuthorizeSSHRevoke: jwk.authorizeToken; jwk token subject cannot be empty"), + code: http.StatusUnauthorized, + } + }, + "ok": func(t *testing.T) *authorizeTest { + key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) + assert.FatalError(t, err) + + p, ok := a.provisioners.Load("sshpop/sshpop") + assert.Fatal(t, ok, "sshpop provisioner not found in test authority") + + tok, err := generateToken(strconv.FormatUint(cert.Serial, 10), p.GetName(), testAudiences.SSHRevoke[0]+"#sshpop/sshpop", + []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + + return &authorizeTest{ + auth: a, + token: tok, + cert: cert, + } + }, + } + + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestAuthority_authorizeSSHRekey(t *testing.T) { + a := testAuthority(t) + + jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) + assert.FatalError(t, err) + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) + assert.FatalError(t, err) + + now := time.Now().UTC() + + validIssuer := "step-cli" + + type authorizeTest struct { + auth *Authority + token string + cert *ssh.Certificate + err error + code int + } + tests := map[string]func(t *testing.T) *authorizeTest{ + "fail/invalid-token": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + err: errors.New("authority.authorizeSSHRekey: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "fail/sshRekey-unimplemented-jwk-provisioner": func(t *testing.T) *authorizeTest { + cl := jwt.Claims{ + Subject: "", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: testAudiences.SSHRekey, + ID: "43", + } + raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: raw, + err: errors.New("authority.authorizeSSHRekey: provisioner.AuthorizeSSHRekey not implemented"), + code: http.StatusUnauthorized, + } + }, + "ok": func(t *testing.T) *authorizeTest { + key, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + cert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) + assert.FatalError(t, err) + + p, ok := a.provisioners.Load("sshpop/sshpop") + assert.Fatal(t, ok, "sshpop provisioner not found in test authority") + + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0]+"#sshpop/sshpop", + []string{"foo.smallstep.com"}, now, _jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + + return &authorizeTest{ + auth: a, + token: tok, + cert: cert, + } + }, + } + + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token) + if err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, tc.cert.Serial, cert.Serial) + assert.Len(t, 3, signOpts) + } + } + }) + } +} diff --git a/authority/config_test.go b/authority/config_test.go index 40ae639b..c8767dd1 100644 --- a/authority/config_test.go +++ b/authority/config_test.go @@ -1,6 +1,7 @@ package authority import ( + "fmt" "testing" "github.com/pkg/errors" @@ -9,7 +10,6 @@ import ( "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" stepJOSE "github.com/smallstep/cli/jose" - jose "gopkg.in/square/go-jose.v2" ) func TestConfigValidate(t *testing.T) { @@ -255,28 +255,19 @@ func TestAuthConfigValidate(t *testing.T) { err: errors.New("authority cannot be undefined"), } }, - "fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest { - return AuthConfigValidateTest{ - ac: &AuthConfig{ - Provisioners: provisioner.List{ - &provisioner.JWK{Name: "foo", Type: "bar", Key: &jose.JSONWebKey{}}, - &provisioner.JWK{Name: "foo", Key: &jose.JSONWebKey{}}, - }, - }, - err: errors.New("provisioner type cannot be empty"), - } - }, - "fail-invalid-claims": func(t *testing.T) AuthConfigValidateTest { - return AuthConfigValidateTest{ - ac: &AuthConfig{ - Provisioners: p, - Claims: &provisioner.Claims{ - MinTLSDur: &provisioner.Duration{Duration: -1}, + /* + "fail-invalid-claims": func(t *testing.T) AuthConfigValidateTest { + return AuthConfigValidateTest{ + ac: &AuthConfig{ + Provisioners: p, + Claims: &provisioner.Claims{ + MinTLSDur: &provisioner.Duration{Duration: -1}, + }, }, - }, - err: errors.New("claims: MinTLSCertDuration must be greater than 0"), - } - }, + err: errors.New("claims: MinTLSCertDuration must be greater than 0"), + } + }, + */ "ok-empty-provisioners": func(t *testing.T) AuthConfigValidateTest { return AuthConfigValidateTest{ ac: &AuthConfig{}, @@ -311,7 +302,7 @@ func TestAuthConfigValidate(t *testing.T) { assert.Equals(t, tc.err.Error(), err.Error()) } } else { - if assert.Nil(t, tc.err) { + if assert.Nil(t, tc.err, fmt.Sprintf("expected error: %s, but got ", tc.err)) { assert.Equals(t, *tc.ac.Template, tc.asn1dn) } } diff --git a/authority/db_test.go b/authority/db_test.go deleted file mode 100644 index 72684c63..00000000 --- a/authority/db_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package authority - -import ( - "crypto/x509" - - "github.com/smallstep/certificates/db" - "golang.org/x/crypto/ssh" -) - -type MockAuthDB struct { - err error - ret1 interface{} - isRevoked func(string) (bool, error) - isSSHRevoked func(string) (bool, error) - revoke func(rci *db.RevokedCertificateInfo) error - revokeSSH func(rci *db.RevokedCertificateInfo) error - storeCertificate func(crt *x509.Certificate) error - useToken func(id, tok string) (bool, error) - isSSHHost func(principal string) (bool, error) - storeSSHCertificate func(crt *ssh.Certificate) error - getSSHHostPrincipals func() ([]string, error) - shutdown func() error -} - -func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { - if m.isRevoked != nil { - return m.isRevoked(sn) - } - return m.ret1.(bool), m.err -} - -func (m *MockAuthDB) IsSSHRevoked(sn string) (bool, error) { - if m.isSSHRevoked != nil { - return m.isSSHRevoked(sn) - } - return m.ret1.(bool), m.err -} - -func (m *MockAuthDB) UseToken(id, tok string) (bool, error) { - if m.useToken != nil { - return m.useToken(id, tok) - } - if m.ret1 == nil { - return false, m.err - } - return m.ret1.(bool), m.err -} - -func (m *MockAuthDB) Revoke(rci *db.RevokedCertificateInfo) error { - if m.revoke != nil { - return m.revoke(rci) - } - return m.err -} - -func (m *MockAuthDB) RevokeSSH(rci *db.RevokedCertificateInfo) error { - if m.revokeSSH != nil { - return m.revokeSSH(rci) - } - return m.err -} - -func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error { - if m.storeCertificate != nil { - return m.storeCertificate(crt) - } - return m.err -} - -func (m *MockAuthDB) IsSSHHost(principal string) (bool, error) { - if m.isSSHHost != nil { - return m.isSSHHost(principal) - } - return m.ret1.(bool), m.err -} - -func (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error { - if m.storeSSHCertificate != nil { - return m.storeSSHCertificate(crt) - } - return m.err -} - -func (m *MockAuthDB) GetSSHHostPrincipals() ([]string, error) { - if m.getSSHHostPrincipals != nil { - return m.getSSHHostPrincipals() - } - return m.ret1.([]string), m.err -} - -func (m *MockAuthDB) Shutdown() error { - if m.shutdown != nil { - return m.shutdown() - } - return m.err -} diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index adba8fd3..7adeb311 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" ) // ACME is the acme provisioner type, an entity that can authorize the ACME @@ -79,7 +80,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // certificate was configured to allow renewals. func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("acme.AuthorizeRenew; renew is disabled for acme provisioner %s", p.GetID())) } return nil } diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index 2ffdd195..581f20ed 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -3,11 +3,13 @@ package provisioner import ( "context" "crypto/x509" + "net/http" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" ) func TestACME_Getters(t *testing.T) { @@ -88,86 +90,98 @@ func TestACME_Init(t *testing.T) { } } -func TestACME_AuthorizeRevoke(t *testing.T) { - p, err := generateACME() - assert.FatalError(t, err) - assert.Nil(t, p.AuthorizeRevoke(context.TODO(), "")) -} - func TestACME_AuthorizeRenew(t *testing.T) { - p1, err := generateACME() - assert.FatalError(t, err) - p2, err := generateACME() - assert.FatalError(t, err) - - // disable renewal - disable := true - p2.Claims = &Claims{DisableRenewal: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) - assert.FatalError(t, err) - - type args struct { + type test struct { + p *ACME cert *x509.Certificate - } - tests := []struct { - name string - prov *ACME - args args err error - }{ - {"ok", p1, args{nil}, nil}, - {"fail", p2, args{nil}, errors.Errorf("renew is disabled for provisioner %s", p2.GetID())}, + code int + } + tests := map[string]func(*testing.T) test{ + "fail/renew-disabled": func(t *testing.T) test { + p, err := generateACME() + assert.FatalError(t, err) + // disable renewal + disable := true + p.Claims = &Claims{DisableRenewal: &disable} + p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + return test{ + p: p, + cert: &x509.Certificate{}, + code: http.StatusUnauthorized, + err: errors.Errorf("acme.AuthorizeRenew; renew is disabled for acme provisioner %s", p.GetID()), + } + }, + "ok": func(t *testing.T) test { + p, err := generateACME() + assert.FatalError(t, err) + return test{ + p: p, + cert: &x509.Certificate{}, + } + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); err != nil { - if assert.NotNil(t, tt.err) { - assert.HasPrefix(t, err.Error(), tt.err.Error()) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { - assert.Nil(t, tt.err) + assert.Nil(t, tc.err) } }) } } func TestACME_AuthorizeSign(t *testing.T) { - p1, err := generateACME() - assert.FatalError(t, err) - - tests := []struct { - name string - prov *ACME - method Method - err error - }{ - {"fail/method", p1, SignSSHMethod, errors.New("unexpected method type 1 in context")}, - {"ok", p1, SignMethod, nil}, + type test struct { + p *ACME + token string + code int + err error } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), tt.method) - if got, err := tt.prov.AuthorizeSign(ctx, ""); err != nil { - if assert.NotNil(t, tt.err) { - assert.HasPrefix(t, err.Error(), tt.err.Error()) + tests := map[string]func(*testing.T) test{ + "ok": func(t *testing.T) test { + p, err := generateACME() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { - if assert.NotNil(t, got) { - assert.Len(t, 4, got) - - for _, o := range got { + if assert.Nil(t, tc.err) && assert.NotNil(t, opts) { + assert.Len(t, 4, opts) + for _, o := range opts { switch v := o.(type) { case *provisionerExtensionOption: assert.Equals(t, v.Type, int(TypeACME)) - assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.Name, tc.p.GetName()) assert.Equals(t, v.CredentialID, "") assert.Len(t, 0, v.KeyValuePairs) case profileDefaultDuration: - assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + assert.Equals(t, time.Duration(v), tc.p.claimer.DefaultTLSCertDuration()) case defaultPublicKeyValidator: case *validityValidator: - assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) - assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration()) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 74fa3a1f..39769118 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -16,6 +16,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -271,7 +272,7 @@ func (p *AWS) Init(config Config) (err error) { func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { payload, err := p.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign") } doc := payload.document @@ -305,7 +306,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // certificate was configured to allow renewals. func (p *AWS) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("aws.AuthorizeRenew; renew is disabled for aws provisioner %s", p.GetID())) } return nil } @@ -349,41 +350,41 @@ func (p *AWS) readURL(url string) ([]byte, error) { func (p *AWS) authorizeToken(token string) (*awsPayload, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errs.Wrapf(http.StatusUnauthorized, err, "aws.authorizeToken; error parsing aws token") } if len(jwt.Headers) == 0 { - return nil, errors.New("error parsing token: header is missing") + return nil, errs.InternalServerError(errors.New("aws.authorizeToken; error parsing token, header is missing")) } var unsafeClaims awsPayload if err := jwt.UnsafeClaimsWithoutVerification(&unsafeClaims); err != nil { - return nil, errors.Wrap(err, "error unmarshaling claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; error unmarshaling claims") } var payload awsPayload if err := jwt.Claims(unsafeClaims.Amazon.Signature, &payload); err != nil { - return nil, errors.Wrap(err, "error verifying claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; error verifying claims") } // Validate identity document signature if err := p.checkSignature(payload.Amazon.Document, payload.Amazon.Signature); err != nil { - return nil, err + return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; invalid aws token signature") } var doc awsInstanceIdentityDocument if err := json.Unmarshal(payload.Amazon.Document, &doc); err != nil { - return nil, errors.Wrap(err, "error unmarshaling identity document") + return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; error unmarshaling aws identity document") } switch { case doc.AccountID == "": - return nil, errors.New("identity document accountId cannot be empty") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document accountId cannot be empty")) case doc.InstanceID == "": - return nil, errors.New("identity document instanceId cannot be empty") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document instanceId cannot be empty")) case doc.PrivateIP == "": - return nil, errors.New("identity document privateIp cannot be empty") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document privateIp cannot be empty")) case doc.Region == "": - return nil, errors.New("identity document region cannot be empty") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document region cannot be empty")) } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -393,12 +394,12 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { Issuer: awsIssuer, Time: now, }, time.Minute); err != nil { - return nil, errors.Wrapf(err, "invalid token") + return nil, errs.Wrapf(http.StatusUnauthorized, err, "aws.authorizeToken; invalid aws token") } // validate audiences with the defaults if !matchesAudience(payload.Audience, p.audiences.Sign) { - return nil, errors.New("invalid token: invalid audience claim (aud)") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid token - invalid audience claim (aud)")) } // Validate subject, it has to be known if disableCustomSANs is enabled @@ -406,7 +407,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { if payload.Subject != doc.InstanceID && payload.Subject != doc.PrivateIP && payload.Subject != fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) { - return nil, errors.New("invalid token: invalid subject claim (sub)") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid token - invalid subject claim (sub)")) } } @@ -420,14 +421,14 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { } } if !found { - return nil, errors.New("invalid identity document: accountId is not valid") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid aws identity document - accountId is not valid")) } } // validate instance age if d := p.InstanceAge.Value(); d > 0 { if now.Sub(doc.PendingTime) > d { - return nil, errors.New("identity document pendingTime is too old") + return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document pendingTime is too old")) } } @@ -438,18 +439,18 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + return nil, errs.Unauthorized(errors.Errorf("aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner %s", p.GetID())) } claims, err := p.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSSHSign") } doc := claims.document signOptions := []SignOption{ // set the key id to the token subject - sshCertificateKeyIDModifier(claims.Subject), + sshCertKeyIDModifier(claims.Subject), } // Default to host + known IPs/hostnames @@ -463,7 +464,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validate user options signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) // Set defaults if not given as user options - signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) + signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) return append(signOptions, // Set the default extensions. diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index e855bf9f..8c59bebe 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -10,12 +10,15 @@ import ( "encoding/hex" "encoding/pem" "fmt" + "net/http" "net/url" "strings" "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -229,6 +232,213 @@ func TestAWS_Init(t *testing.T) { } } +func TestAWS_authorizeToken(t *testing.T) { + block, _ := pem.Decode([]byte(awsTestKey)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + t.Fatal("error decoding AWS key") + } + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + assert.FatalError(t, err) + badKey, err := rsa.GenerateKey(rand.Reader, 1024) + assert.FatalError(t, err) + + type test struct { + p *AWS + token string + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; error parsing aws token"), + } + }, + "fail/cannot-validate-sig": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id", + "127.0.0.1", "us-west-1", time.Now(), badKey) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; invalid aws token signature"), + } + }, + "fail/empty-account-id": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), "", "instance-id", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; aws identity document accountId cannot be empty"), + } + }, + "fail/empty-instance-id": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), p.Accounts[0], "", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; aws identity document instanceId cannot be empty"), + } + }, + "fail/empty-private-ip": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id", + "", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; aws identity document privateIp cannot be empty"), + } + }, + "fail/empty-region": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id", + "127.0.0.1", "", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; aws identity document region cannot be empty"), + } + }, + "fail/invalid-token-issuer": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", "bad-issuer", p.GetID(), p.Accounts[0], "instance-id", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; invalid aws token"), + } + }, + "fail/invalid-audience": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, "bad-audience", p.Accounts[0], "instance-id", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; invalid token - invalid audience claim (aud)"), + } + }, + "fail/invalid-subject-disabled-custom-SANs": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + p.DisableCustomSANs = true + tok, err := generateAWSToken( + "foo", awsIssuer, p.GetID(), p.Accounts[0], "instance-id", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; invalid token - invalid subject claim (sub)"), + } + }, + "fail/invalid-account-id": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), "foo", "instance-id", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; invalid aws identity document - accountId is not valid"), + } + }, + "fail/instance-age": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + p.InstanceAge = Duration{1 * time.Minute} + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id", + "127.0.0.1", "us-west-1", time.Now().Add(-1*time.Minute), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("aws.authorizeToken; aws identity document pendingTime is too old"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateAWS() + assert.FatalError(t, err) + tok, err := generateAWSToken( + "instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id", + "127.0.0.1", "us-west-1", time.Now(), key) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if claims, err := tc.p.authorizeToken(tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) && assert.NotNil(t, claims) { + assert.Equals(t, claims.Subject, "instance-id") + assert.Equals(t, claims.Issuer, awsIssuer) + assert.NotNil(t, claims.Amazon) + + aud, err := generateSignAudience("https://ca.smallstep.com", tc.p.GetID()) + assert.FatalError(t, err) + assert.Equals(t, claims.Audience[0], aud) + } + } + }) + } +} + func TestAWS_AuthorizeSign(t *testing.T) { p1, srv, err := generateAWSWithServer() assert.FatalError(t, err) @@ -326,26 +536,27 @@ func TestAWS_AuthorizeSign(t *testing.T) { aws *AWS args args wantLen int + code int wantErr bool }{ - {"ok", p1, args{t1}, 5, false}, - {"ok", p2, args{t2}, 7, false}, - {"ok", p2, args{t2Hostname}, 7, false}, - {"ok", p2, args{t2PrivateIP}, 7, false}, - {"ok", p1, args{t4}, 5, false}, - {"fail account", p3, args{t3}, 0, true}, - {"fail token", p1, args{"token"}, 0, true}, - {"fail subject", p1, args{failSubject}, 0, true}, - {"fail issuer", p1, args{failIssuer}, 0, true}, - {"fail audience", p1, args{failAudience}, 0, true}, - {"fail account", p1, args{failAccount}, 0, true}, - {"fail instanceID", p1, args{failInstanceID}, 0, true}, - {"fail privateIP", p1, args{failPrivateIP}, 0, true}, - {"fail region", p1, args{failRegion}, 0, true}, - {"fail exp", p1, args{failExp}, 0, true}, - {"fail nbf", p1, args{failNbf}, 0, true}, - {"fail key", p1, args{failKey}, 0, true}, - {"fail instance age", p2, args{failInstanceAge}, 0, true}, + {"ok", p1, args{t1}, 5, http.StatusOK, false}, + {"ok", p2, args{t2}, 7, http.StatusOK, false}, + {"ok", p2, args{t2Hostname}, 7, http.StatusOK, false}, + {"ok", p2, args{t2PrivateIP}, 7, http.StatusOK, false}, + {"ok", p1, args{t4}, 5, http.StatusOK, false}, + {"fail account", p3, args{t3}, 0, http.StatusUnauthorized, true}, + {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, + {"fail subject", p1, args{failSubject}, 0, http.StatusUnauthorized, true}, + {"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true}, + {"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true}, + {"fail account", p1, args{failAccount}, 0, http.StatusUnauthorized, true}, + {"fail instanceID", p1, args{failInstanceID}, 0, http.StatusUnauthorized, true}, + {"fail privateIP", p1, args{failPrivateIP}, 0, http.StatusUnauthorized, true}, + {"fail region", p1, args{failRegion}, 0, http.StatusUnauthorized, true}, + {"fail exp", p1, args{failExp}, 0, http.StatusUnauthorized, true}, + {"fail nbf", p1, args{failNbf}, 0, http.StatusUnauthorized, true}, + {"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true}, + {"fail instance age", p2, args{failInstanceAge}, 0, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -354,8 +565,13 @@ func TestAWS_AuthorizeSign(t *testing.T) { if (err != nil) != tt.wantErr { t.Errorf("AWS.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) + } else { + assert.Len(t, tt.wantLen, got) } - assert.Len(t, tt.wantLen, got) }) } } @@ -368,6 +584,14 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { assert.FatalError(t, err) defer srv.Close() + p2, err := generateAWS() + assert.FatalError(t, err) + // disable sshCA + disable := false + p2.Claims = &Claims{EnableSSHCA: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com") assert.FatalError(t, err) @@ -407,30 +631,35 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { aws *AWS args args expected *SSHOptions + code int wantErr bool wantSignErr bool }{ - {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false}, - {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false}, - {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false}, - {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, false, false}, - {"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, false, false}, - {"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, false, false}, - {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, false, false}, - {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true}, - {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true}, - {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true}, - {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, false, true}, + {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, http.StatusOK, false, false}, + {"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, http.StatusOK, false, false}, + {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true}, + {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true}, + {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, + {"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.aws.AuthorizeSSHSign(ctx, tt.args.token) + got, err := tt.aws.AuthorizeSSHSign(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("AWS.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) @@ -447,6 +676,7 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) { }) } } + func TestAWS_AuthorizeRenew(t *testing.T) { p1, err := generateAWS() assert.FatalError(t, err) @@ -466,44 +696,20 @@ func TestAWS_AuthorizeRenew(t *testing.T) { name string aws *AWS args args + code int wantErr bool }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, + {"ok", p1, args{nil}, http.StatusOK, false}, + {"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.aws.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("AWS.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestAWS_AuthorizeRevoke(t *testing.T) { - p1, srv, err := generateAWSWithServer() - assert.FatalError(t, err) - defer srv.Close() - - t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com") - assert.FatalError(t, err) - - type args struct { - token string - } - tests := []struct { - name string - aws *AWS - args args - wantErr bool - }{ - {"ok", p1, args{t1}, true}, // revoke is disabled - {"fail", p1, args{"token"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.aws.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr { - t.Errorf("AWS.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) } }) } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 998ef6e1..86eb516f 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -13,6 +13,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -209,14 +210,14 @@ func (p *Azure) Init(config Config) (err error) { return nil } -// parseToken returns the claims, name, group, error. -func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) { +// authorizeToken returs the claims, name, group, error. +func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, "", "", errors.Wrapf(err, "error parsing token") + return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token") } if len(jwt.Headers) == 0 { - return nil, "", "", errors.New("error parsing token: header is missing") + return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; azure token missing header")) } var found bool @@ -229,7 +230,7 @@ func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) } } if !found { - return nil, "", "", errors.New("cannot validate token") + return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; cannot validate azure token")) } if err := claims.ValidateWithLeeway(jose.Expected{ @@ -237,17 +238,17 @@ func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) Issuer: p.oidcConfig.Issuer, Time: time.Now(), }, 1*time.Minute); err != nil { - return nil, "", "", errors.Wrap(err, "failed to validate payload") + return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; failed to validate azure token payload") } // Validate TenantID if claims.TenantID != p.TenantID { - return nil, "", "", errors.New("validation failed: invalid tenant id claim (tid)") + return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)")) } re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID) if len(re) != 4 { - return nil, "", "", errors.Errorf("error parsing xms_mirid claim: %s", claims.XMSMirID) + return nil, "", "", errs.Unauthorized(errors.Errorf("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID)) } group, name := re[2], re[3] return &claims, name, group, nil @@ -256,9 +257,9 @@ func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) // AuthorizeSign validates the given token and returns the sign options that // will be used on certificate creation. func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - _, name, group, err := p.parseToken(token) + _, name, group, err := p.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSign") } // Filter by resource group @@ -271,7 +272,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } } if !found { - return nil, errors.New("validation failed: invalid resource group") + return nil, errs.Unauthorized(errors.New("azure.AuthorizeSign; azure token validation failed - invalid resource group")) } } @@ -301,7 +302,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // certificate was configured to allow renewals. func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("azure.AuthorizeRenew; renew is disabled for azure provisioner %s", p.GetID())) } return nil } @@ -309,16 +310,16 @@ func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) erro // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + return nil, errs.Unauthorized(errors.Errorf("azure.AuthorizeSSHSign; sshCA is disabled for provisioner %s", p.GetID())) } - _, name, _, err := p.parseToken(token) + _, name, _, err := p.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign") } signOptions := []SignOption{ // set the key id to the token subject - sshCertificateKeyIDModifier(name), + sshCertKeyIDModifier(name), } // Default to host + known hostnames @@ -329,7 +330,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Validate user options signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) // Set defaults if not given as user options - signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) + signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) return append(signOptions, // Set the default extensions. diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 1760ed5c..13e6ac8e 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -15,7 +15,10 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" + "github.com/smallstep/cli/jose" ) func TestAzure_Getters(t *testing.T) { @@ -209,6 +212,148 @@ func TestAzure_Init(t *testing.T) { } } +func TestAzure_authorizeToken(t *testing.T) { + type test struct { + p *Azure + token string + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateAzure() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("azure.authorizeToken; error parsing azure token"), + } + }, + "fail/cannot-validate-sig": func(t *testing.T) test { + p, srv, err := generateAzureWithServer() + assert.FatalError(t, err) + defer srv.Close() + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience, + p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + time.Now(), jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("azure.authorizeToken; cannot validate azure token"), + } + }, + "fail/invalid-token-issuer": func(t *testing.T) test { + p, srv, err := generateAzureWithServer() + assert.FatalError(t, err) + defer srv.Close() + tok, err := generateAzureToken("subject", "bad-issuer", azureDefaultAudience, + p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("azure.authorizeToken; failed to validate azure token payload"), + } + }, + "fail/invalid-tenant-id": func(t *testing.T) test { + p, srv, err := generateAzureWithServer() + assert.FatalError(t, err) + defer srv.Close() + tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience, + "foo", "subscriptionID", "resourceGroup", "virtualMachine", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)"), + } + }, + "fail/invalid-xms-mir-id": func(t *testing.T) test { + p, srv, err := generateAzureWithServer() + assert.FatalError(t, err) + defer srv.Close() + jwk := &p.keyStore.keySet.Keys[0] + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), + ) + assert.FatalError(t, err) + + now := time.Now() + claims := azurePayload{ + Claims: jose.Claims{ + Subject: "subject", + Issuer: p.oidcConfig.Issuer, + IssuedAt: jose.NewNumericDate(now), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), + Audience: []string{azureDefaultAudience}, + ID: "the-jti", + }, + AppID: "the-appid", + AppIDAcr: "the-appidacr", + IdentityProvider: "the-idp", + ObjectID: "the-oid", + TenantID: p.TenantID, + Version: "the-version", + XMSMirID: "foo", + } + tok, err := jose.Signed(sig).Claims(claims).CompactSerialize() + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("azure.authorizeToken; error parsing xms_mirid claim - foo"), + } + }, + "ok": func(t *testing.T) test { + p, srv, err := generateAzureWithServer() + assert.FatalError(t, err) + defer srv.Close() + tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience, + p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if claims, name, group, err := tc.p.authorizeToken(tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, claims.Subject, "subject") + assert.Equals(t, claims.Issuer, tc.p.oidcConfig.Issuer) + assert.Equals(t, claims.Audience[0], azureDefaultAudience) + + assert.Equals(t, name, "virtualMachine") + assert.Equals(t, group, "resourceGroup") + } + } + }) + } +} + func TestAzure_AuthorizeSign(t *testing.T) { p1, srv, err := generateAzureWithServer() assert.FatalError(t, err) @@ -283,19 +428,20 @@ func TestAzure_AuthorizeSign(t *testing.T) { azure *Azure args args wantLen int + code int wantErr bool }{ - {"ok", p1, args{t1}, 4, false}, - {"ok", p2, args{t2}, 6, false}, - {"ok", p1, args{t11}, 4, false}, - {"fail tenant", p3, args{t3}, 0, true}, - {"fail resource group", p4, args{t4}, 0, true}, - {"fail token", p1, args{"token"}, 0, true}, - {"fail issuer", p1, args{failIssuer}, 0, true}, - {"fail audience", p1, args{failAudience}, 0, true}, - {"fail exp", p1, args{failExp}, 0, true}, - {"fail nbf", p1, args{failNbf}, 0, true}, - {"fail key", p1, args{failKey}, 0, true}, + {"ok", p1, args{t1}, 4, http.StatusOK, false}, + {"ok", p2, args{t2}, 6, http.StatusOK, false}, + {"ok", p1, args{t11}, 4, http.StatusOK, false}, + {"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true}, + {"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true}, + {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, + {"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true}, + {"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true}, + {"fail exp", p1, args{failExp}, 0, http.StatusUnauthorized, true}, + {"fail nbf", p1, args{failNbf}, 0, http.StatusUnauthorized, true}, + {"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -304,8 +450,51 @@ func TestAzure_AuthorizeSign(t *testing.T) { if (err != nil) != tt.wantErr { t.Errorf("Azure.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) + } else { + assert.Len(t, tt.wantLen, got) + } + }) + } +} + +func TestAzure_AuthorizeRenew(t *testing.T) { + p1, err := generateAzure() + assert.FatalError(t, err) + p2, err := generateAzure() + assert.FatalError(t, err) + + // disable renewal + disable := true + p2.Claims = &Claims{DisableRenewal: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + azure *Azure + args args + code int + wantErr bool + }{ + {"ok", p1, args{nil}, http.StatusOK, false}, + {"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.azure.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) } - assert.Len(t, tt.wantLen, got) }) } } @@ -318,6 +507,14 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) { assert.FatalError(t, err) defer srv.Close() + p2, err := generateAzure() + assert.FatalError(t, err) + // disable sshCA + disable := false + p2.Claims = &Claims{EnableSSHCA: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + t1, err := p1.GetIdentityToken("subject", "caURL") assert.FatalError(t, err) @@ -349,28 +546,33 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) { azure *Azure args args expected *SSHOptions + code int wantErr bool wantSignErr bool }{ - {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false}, - {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false}, - {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false}, - {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, false, false}, - {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, false, false}, - {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true}, - {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true}, - {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true}, - {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, false, true}, + {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true}, + {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true}, + {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, + {"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.azure.AuthorizeSSHSign(ctx, tt.args.token) + got, err := tt.azure.AuthorizeSSHSign(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("Azure.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) @@ -388,68 +590,6 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) { } } -func TestAzure_AuthorizeRenew(t *testing.T) { - p1, err := generateAzure() - assert.FatalError(t, err) - p2, err := generateAzure() - assert.FatalError(t, err) - - // disable renewal - disable := true - p2.Claims = &Claims{DisableRenewal: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) - assert.FatalError(t, err) - - type args struct { - cert *x509.Certificate - } - tests := []struct { - name string - azure *Azure - args args - wantErr bool - }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.azure.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestAzure_AuthorizeRevoke(t *testing.T) { - az, srv, err := generateAzureWithServer() - assert.FatalError(t, err) - defer srv.Close() - - token, err := az.GetIdentityToken("subject", "caURL") - assert.FatalError(t, err) - - type args struct { - token string - } - tests := []struct { - name string - azure *Azure - args args - wantErr bool - }{ - {"ok token", az, args{token}, true}, // revoke is disabled - {"bad token", az, args{"bad token"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.azure.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr { - t.Errorf("Azure.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestAzure_assertConfig(t *testing.T) { p1, err := generateAzure() assert.FatalError(t, err) diff --git a/authority/provisioner/collection.go b/authority/provisioner/collection.go index bf189ee5..a1d11740 100644 --- a/authority/provisioner/collection.go +++ b/authority/provisioner/collection.go @@ -78,7 +78,7 @@ func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims) // match with server audiences if matchesAudience(claims.Audience, audiences) { - // Use fragment to get provisioner name (GCP, AWS) + // Use fragment to get provisioner name (GCP, AWS, SSHPOP) if fragment != "" { return c.Load(fragment) } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index bc531e92..69a3006a 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -14,6 +14,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -210,7 +211,7 @@ func (p *GCP) Init(config Config) error { func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { claims, err := p.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSign") } ce := claims.Google.ComputeEngine @@ -239,10 +240,10 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er ), nil } -// AuthorizeRenewal returns an error if the renewal is disabled. -func (p *GCP) AuthorizeRenewal(ctx context.Context, cert *x509.Certificate) error { +// AuthorizeRenew returns an error if the renewal is disabled. +func (p *GCP) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("gcp.AuthorizeRenew; renew is disabled for gcp provisioner %s", p.GetID())) } return nil } @@ -260,10 +261,10 @@ func (p *GCP) assertConfig() { func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errs.Wrap(http.StatusUnauthorized, err, "gcp.authorizeToken; error parsing gcp token") } if len(jwt.Headers) == 0 { - return nil, errors.New("error parsing token: header is missing") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; error parsing gcp token - header is missing")) } var found bool @@ -277,7 +278,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } if !found { - return nil, errors.Errorf("failed to validate payload: cannot find key for kid %s", kid) + return nil, errs.Unauthorized(errors.Errorf("gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid %s", kid)) } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -287,12 +288,12 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { Issuer: "https://accounts.google.com", Time: now, }, time.Minute); err != nil { - return nil, errors.Wrapf(err, "invalid token") + return nil, errs.Wrap(http.StatusUnauthorized, err, "gcp.authorizeToken; invalid gcp token payload") } // validate audiences with the defaults if !matchesAudience(claims.Audience, p.audiences.Sign) { - return nil, errors.New("invalid token: invalid audience claim (aud)") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid audience claim (aud)")) } // validate subject (service account) @@ -305,7 +306,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } if !found { - return nil, errors.New("invalid token: invalid subject claim") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid subject claim")) } } @@ -319,26 +320,26 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } if !found { - return nil, errors.New("invalid token: invalid project id") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid project id")) } } // validate instance age if d := p.InstanceAge.Value(); d > 0 { if now.Sub(claims.Google.ComputeEngine.InstanceCreationTimestamp.Time()) > d { - return nil, errors.New("token google.compute_engine.instance_creation_timestamp is too old") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old")) } } switch { case claims.Google.ComputeEngine.InstanceID == "": - return nil, errors.New("token google.compute_engine.instance_id cannot be empty") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty")) case claims.Google.ComputeEngine.InstanceName == "": - return nil, errors.New("token google.compute_engine.instance_name cannot be empty") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty")) case claims.Google.ComputeEngine.ProjectID == "": - return nil, errors.New("token google.compute_engine.project_id cannot be empty") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty")) case claims.Google.ComputeEngine.Zone == "": - return nil, errors.New("token google.compute_engine.zone cannot be empty") + return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty")) } return &claims, nil @@ -347,18 +348,18 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + return nil, errs.Unauthorized(errors.Errorf("gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner %s", p.GetID())) } claims, err := p.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSSHSign") } ce := claims.Google.ComputeEngine signOptions := []SignOption{ // set the key id to the token subject - sshCertificateKeyIDModifier(ce.InstanceName), + sshCertKeyIDModifier(ce.InstanceName), } // Default to host + known hostnames @@ -372,7 +373,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validate user options signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) // Set defaults if not given as user options - signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) + signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) return append(signOptions, // Set the default extensions diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 4764dfc7..bdda8fd9 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -16,7 +16,10 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" + "github.com/smallstep/cli/jose" ) func TestGCP_Getters(t *testing.T) { @@ -211,6 +214,202 @@ func TestGCP_Init(t *testing.T) { } } +func TestGCP_authorizeToken(t *testing.T) { + type test struct { + p *GCP + token string + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; error parsing gcp token"), + } + }, + "fail/cannot-validate-sig": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid "), + } + }, + "fail/invalid-issuer": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://foo.bar.zap", p.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; invalid gcp token payload"), + } + }, + "fail/invalid-serviceAccount": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken("foo", + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; invalid gcp token - invalid subject claim"), + } + }, + "fail/invalid-projectID": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + p.ProjectIDs = []string{"foo", "bar"} + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; invalid gcp token - invalid project id"), + } + }, + "fail/instance-age": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + p.InstanceAge = Duration{1 * time.Minute} + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now().Add(-1*time.Minute), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old"), + } + }, + "fail/empty-instance-id": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "", "instance-name", "project-id", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty"), + } + }, + "fail/empty-instance-name": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "", "project-id", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty"), + } + }, + "fail/empty-project-id": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty"), + } + }, + "fail/empty-zone": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "project-id", "", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateGCP() + assert.FatalError(t, err) + tok, err := generateGCPToken(p.ServiceAccounts[0], + "https://accounts.google.com", p.GetID(), + "instance-id", "instance-name", "project-id", "zone", + time.Now(), &p.keyStore.keySet.Keys[0]) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if claims, err := tc.p.authorizeToken(tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) && assert.NotNil(t, claims) { + assert.Equals(t, claims.Subject, tc.p.ServiceAccounts[0]) + assert.Equals(t, claims.Issuer, "https://accounts.google.com") + assert.NotNil(t, claims.Google) + + aud, err := generateSignAudience("https://ca.smallstep.com", tc.p.GetID()) + assert.FatalError(t, err) + assert.Equals(t, claims.Audience[0], aud) + } + } + }) + } +} + func TestGCP_AuthorizeSign(t *testing.T) { p1, err := generateGCP() assert.FatalError(t, err) @@ -313,24 +512,25 @@ func TestGCP_AuthorizeSign(t *testing.T) { gcp *GCP args args wantLen int + code int wantErr bool }{ - {"ok", p1, args{t1}, 4, false}, - {"ok", p2, args{t2}, 6, false}, - {"ok", p3, args{t3}, 4, false}, - {"fail token", p1, args{"token"}, 0, true}, - {"fail key", p1, args{failKey}, 0, true}, - {"fail iss", p1, args{failIss}, 0, true}, - {"fail aud", p1, args{failAud}, 0, true}, - {"fail exp", p1, args{failExp}, 0, true}, - {"fail nbf", p1, args{failNbf}, 0, true}, - {"fail service account", p1, args{failServiceAccount}, 0, true}, - {"fail invalid project id", p3, args{failInvalidProjectID}, 0, true}, - {"fail invalid instance age", p3, args{failInvalidInstanceAge}, 0, true}, - {"fail instance id", p1, args{failInstanceID}, 0, true}, - {"fail instance name", p1, args{failInstanceName}, 0, true}, - {"fail project id", p1, args{failProjectID}, 0, true}, - {"fail zone", p1, args{failZone}, 0, true}, + {"ok", p1, args{t1}, 4, http.StatusOK, false}, + {"ok", p2, args{t2}, 6, http.StatusOK, false}, + {"ok", p3, args{t3}, 4, http.StatusOK, false}, + {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, + {"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true}, + {"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true}, + {"fail aud", p1, args{failAud}, 0, http.StatusUnauthorized, true}, + {"fail exp", p1, args{failExp}, 0, http.StatusUnauthorized, true}, + {"fail nbf", p1, args{failNbf}, 0, http.StatusUnauthorized, true}, + {"fail service account", p1, args{failServiceAccount}, 0, http.StatusUnauthorized, true}, + {"fail invalid project id", p3, args{failInvalidProjectID}, 0, http.StatusUnauthorized, true}, + {"fail invalid instance age", p3, args{failInvalidInstanceAge}, 0, http.StatusUnauthorized, true}, + {"fail instance id", p1, args{failInstanceID}, 0, http.StatusUnauthorized, true}, + {"fail instance name", p1, args{failInstanceName}, 0, http.StatusUnauthorized, true}, + {"fail project id", p1, args{failProjectID}, 0, http.StatusUnauthorized, true}, + {"fail zone", p1, args{failZone}, 0, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -339,8 +539,13 @@ func TestGCP_AuthorizeSign(t *testing.T) { if (err != nil) != tt.wantErr { t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) + } else { + assert.Len(t, tt.wantLen, got) } - assert.Len(t, tt.wantLen, got) }) } } @@ -352,6 +557,14 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { p1, err := generateGCP() assert.FatalError(t, err) + p2, err := generateGCP() + assert.FatalError(t, err) + // disable sshCA + disable := false + p2.Claims = &Claims{EnableSSHCA: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + t1, err := generateGCPToken(p1.ServiceAccounts[0], "https://accounts.google.com", p1.GetID(), "instance-id", "instance-name", "project-id", "zone", @@ -394,30 +607,35 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { gcp *GCP args args expected *SSHOptions + code int wantErr bool wantSignErr bool }{ - {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false}, - {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false}, - {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false}, - {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, false, false}, - {"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, false, false}, - {"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, false, false}, - {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, false, false}, - {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true}, - {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true}, - {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true}, - {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, false, true}, + {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false}, + {"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false}, + {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true}, + {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true}, + {"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, + {"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.gcp.AuthorizeSSHSign(ctx, tt.args.token) + got, err := tt.gcp.AuthorizeSSHSign(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("GCP.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) @@ -435,7 +653,7 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) { } } -func TestGCP_AuthorizeRenewal(t *testing.T) { +func TestGCP_AuthorizeRenew(t *testing.T) { p1, err := generateGCP() assert.FatalError(t, err) p2, err := generateGCP() @@ -454,46 +672,20 @@ func TestGCP_AuthorizeRenewal(t *testing.T) { name string prov *GCP args args + code int wantErr bool }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenewal(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("GCP.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestGCP_AuthorizeRevoke(t *testing.T) { - p1, err := generateGCP() - assert.FatalError(t, err) - - t1, err := generateGCPToken(p1.ServiceAccounts[0], - "https://accounts.google.com", p1.GetID(), - "instance-id", "instance-name", "project-id", "zone", - time.Now(), &p1.keyStore.keySet.Keys[0]) - assert.FatalError(t, err) - - type args struct { - token string - } - tests := []struct { - name string - gcp *GCP - args args - wantErr bool - }{ - {"ok", p1, args{t1}, true}, // revoke is disabled - {"fail", p1, args{"token"}, true}, + {"ok", p1, args{nil}, http.StatusOK, false}, + {"fail/renewal-disabled", p2, args{nil}, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.gcp.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr { - t.Errorf("GCP.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr) + if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("GCP.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) } }) } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index b5add3f4..1c613de6 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -3,9 +3,11 @@ package provisioner import ( "context" "crypto/x509" + "net/http" "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/jose" ) @@ -99,12 +101,12 @@ func (p *JWK) Init(config Config) (err error) { func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errs.Wrap(http.StatusUnauthorized, err, "jwk.authorizeToken; error parsing jwk token") } var claims jwtPayload if err = jwt.Claims(p.Key, &claims); err != nil { - return nil, errors.Wrap(err, "error parsing claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, "jwk.authorizeToken; error parsing jwk claims") } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -113,17 +115,17 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err Issuer: p.Name, Time: time.Now().UTC(), }, time.Minute); err != nil { - return nil, errors.Wrapf(err, "invalid token") + return nil, errs.Wrapf(http.StatusUnauthorized, err, "jwk.authorizeToken; invalid jwk claims") } // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errors.Errorf("invalid token: invalid audience claim (aud); want %s, but got %s", - audiences, claims.Audience) + return nil, errs.Unauthorized(errors.Errorf("jwk.authorizeToken; invalid jwk token audience claim (aud); want %s, but got %s", + audiences, claims.Audience)) } if claims.Subject == "" { - return nil, errors.New("token subject cannot be empty") + return nil, errs.Unauthorized(errors.New("jwk.authorizeToken; jwk token subject cannot be empty")) } return &claims, nil @@ -133,14 +135,14 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err // revoke the certificate with serial number in the `sub` property. func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) - return err + return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeRevoke") } // AuthorizeSign validates the given token. func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { claims, err := p.authorizeToken(token, p.audiences.Sign) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign") } // NOTE: This is for backwards compatibility with older versions of cli @@ -171,7 +173,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // certificate was configured to allow renewals. func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("jwk.AuthorizeRenew; renew is disabled for jwk provisioner %s", p.GetID())) } return nil } @@ -179,14 +181,14 @@ func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + return nil, errs.Unauthorized(errors.Errorf("jwk.AuthorizeSSHSign; sshCA is disabled for jwk provisioner %s", p.GetID())) } claims, err := p.authorizeToken(token, p.audiences.SSHSign) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHSign") } if claims.Step == nil || claims.Step.SSH == nil { - return nil, errors.New("authorization token must be an SSH provisioning token") + return nil, errs.Unauthorized(errors.New("jwk.AuthorizeSSHSign; jwk token must be an SSH provisioning token")) } opts := claims.Step.SSH @@ -205,19 +207,19 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, signOptions = append(signOptions, sshCertPrincipalsModifier(opts.Principals)) } if !opts.ValidAfter.IsZero() { - signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) + signOptions = append(signOptions, sshCertValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) } if !opts.ValidBefore.IsZero() { - signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) + signOptions = append(signOptions, sshCertValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) } if opts.KeyID != "" { - signOptions = append(signOptions, sshCertificateKeyIDModifier(opts.KeyID)) + signOptions = append(signOptions, sshCertKeyIDModifier(opts.KeyID)) } else { - signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject)) + signOptions = append(signOptions, sshCertKeyIDModifier(claims.Subject)) } // Default to a user certificate with no principals if not set - signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) + signOptions = append(signOptions, sshCertDefaultsModifier{CertType: SSHUserCert}) return append(signOptions, // Set the default extensions. @@ -238,5 +240,5 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // AuthorizeSSHRevoke returns nil if the token is valid, false otherwise. func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.SSHRevoke) - return err + return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHRevoke") } diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index 47a6e7cc..a0c48ee9 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -7,12 +7,14 @@ import ( "crypto/rsa" "crypto/x509" "net" + "net/http" "strings" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -162,25 +164,29 @@ func TestJWK_authorizeToken(t *testing.T) { name string prov *JWK args args + code int err error }{ - {"fail-token", p1, args{failTok}, errors.New("error parsing token")}, - {"fail-key", p1, args{failKey}, errors.New("error parsing claims")}, - {"fail-claims", p1, args{failClaims}, errors.New("error parsing claims")}, - {"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, - {"fail-issuer", p1, args{failIss}, errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)")}, - {"fail-expired", p1, args{failExp}, errors.New("invalid token: square/go-jose/jwt: validation failed, token is expired (exp)")}, - {"fail-not-before", p1, args{failNbf}, errors.New("invalid token: square/go-jose/jwt: validation failed, token not valid yet (nbf)")}, - {"fail-audience", p1, args{failAud}, errors.New("invalid token: invalid audience claim (aud)")}, - {"fail-subject", p1, args{failSub}, errors.New("token subject cannot be empty")}, - {"ok", p1, args{t1}, nil}, - {"ok-no-encrypted-key", p2, args{t2}, nil}, - {"ok-no-sans", p1, args{t3}, nil}, + {"fail-token", p1, args{failTok}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk token")}, + {"fail-key", p1, args{failKey}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims")}, + {"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims")}, + {"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")}, + {"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)")}, + {"fail-expired", p1, args{failExp}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, token is expired (exp)")}, + {"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, token not valid yet (nbf)")}, + {"fail-audience", p1, args{failAud}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk token audience claim (aud)")}, + {"fail-subject", p1, args{failSub}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; jwk token subject cannot be empty")}, + {"ok", p1, args{t1}, http.StatusOK, nil}, + {"ok-no-encrypted-key", p2, args{t2}, http.StatusOK, nil}, + {"ok-no-sans", p1, args{t3}, http.StatusOK, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got, err := tt.prov.authorizeToken(tt.args.token, testAudiences.Sign); err != nil { if assert.NotNil(t, tt.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.HasPrefix(t, err.Error(), tt.err.Error()) } } else { @@ -208,15 +214,19 @@ func TestJWK_AuthorizeRevoke(t *testing.T) { name string prov *JWK args args + code int err error }{ - {"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, - {"ok", p1, args{t1}, nil}, + {"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.AuthorizeRevoke: jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")}, + {"ok", p1, args{t1}, http.StatusOK, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token); err != nil { if assert.NotNil(t, tt.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.HasPrefix(t, err.Error(), tt.err.Error()) } } @@ -246,20 +256,24 @@ func TestJWK_AuthorizeSign(t *testing.T) { name string prov *JWK args args + code int err error dns []string emails []string ips []net.IP }{ - {name: "fail-signature", prov: p1, args: args{failSig}, err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, - {"ok-sans", p1, args{t1}, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}}, - {"ok-no-sans", p1, args{t2}, nil, []string{"subject"}, []string{}, []net.IP{}}, + {name: "fail-signature", prov: p1, args: args{failSig}, code: http.StatusUnauthorized, err: errors.New("jwk.AuthorizeSign: jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")}, + {"ok-sans", p1, args{t1}, http.StatusOK, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}}, + {"ok-no-sans", p1, args{t2}, http.StatusOK, nil, []string{"subject"}, []string{}, []net.IP{}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignMethod) if got, err := tt.prov.AuthorizeSign(ctx, tt.args.token); err != nil { if assert.NotNil(t, tt.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.HasPrefix(t, err.Error(), tt.err.Error()) } } else { @@ -315,15 +329,20 @@ func TestJWK_AuthorizeRenew(t *testing.T) { name string prov *JWK args args + code int wantErr bool }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, + {"ok", p1, args{nil}, http.StatusOK, false}, + {"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("JWK.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) } }) } @@ -335,6 +354,14 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) { p1, err := generateJWK() assert.FatalError(t, err) + p2, err := generateJWK() + assert.FatalError(t, err) + // disable sshCA + disable := false + p2.Claims = &Claims{EnableSSHCA: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + jwk, err := decryptJSONWebKey(p1.EncryptedKey) assert.FatalError(t, err) @@ -382,30 +409,34 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) { prov *JWK args args expected *SSHOptions + code int wantErr bool wantSignErr bool }{ - {"user", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false}, - {"user-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false}, - {"user-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false}, - {"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, false, false}, - {"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false}, - {"host", p1, args{t2, SSHOptions{}, pub}, expectedHostOptions, false, false}, - {"host-type", p1, args{t2, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false}, - {"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false}, - {"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false}, - {"fail-signature", p1, args{failSig, SSHOptions{}, pub}, nil, true, false}, - {"rail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true}, + {"user", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false}, + {"user-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false}, + {"user-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false}, + {"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false}, + {"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false}, + {"host", p1, args{t2, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"host-type", p1, args{t2, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false}, + {"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedUserOptions, http.StatusUnauthorized, true, false}, + {"fail-signature", p1, args{failSig, SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false}, + {"rail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.prov.AuthorizeSSHSign(ctx, tt.args.token) + got, err := tt.prov.AuthorizeSSHSign(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) @@ -511,10 +542,9 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignSSHMethod) token, err := generateSSHToken(tt.args.sub, tt.args.iss, tt.args.aud, tt.args.iat, tt.args.tokSSHOpts, tt.args.jwk) assert.FatalError(t, err) - if got, err := tt.prov.AuthorizeSSHSign(ctx, token); (err != nil) != tt.wantErr { + if got, err := tt.prov.AuthorizeSSHSign(context.Background(), token); (err != nil) != tt.wantErr { t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) } else if !tt.wantErr && assert.NotNil(t, got) { var opts SSHOptions @@ -535,3 +565,52 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) { }) } } + +func TestJWK_AuthorizeSSHRevoke(t *testing.T) { + type test struct { + p *JWK + token string + code int + err error + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateJWK() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("jwk.AuthorizeSSHRevoke: jwk.authorizeToken; error parsing jwk token"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateJWK() + assert.FatalError(t, err) + jwk, err := decryptJSONWebKey(p.EncryptedKey) + assert.FatalError(t, err) + + tok, err := generateToken("subject", p.Name, testAudiences.SSHRevoke[0], "name@smallstep.com", []string{"127.0.0.1", "max@smallstep.com", "foo"}, time.Now(), jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index e7d45236..0826028e 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -6,8 +6,10 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" + "net/http" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/jose" "golang.org/x/crypto/ed25519" @@ -138,7 +140,8 @@ func (p *K8sSA) Init(config Config) (err error) { func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errs.Wrap(http.StatusUnauthorized, err, + "k8ssa.authorizeToken; error parsing k8sSA token") } var ( @@ -146,7 +149,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, claims k8sSAPayload ) if p.pubKeys == nil { - return nil, errors.New("TokenReview API integration not implemented") + return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented")) /* NOTE: We plan to support the TokenReview API in a future release. Below is some code that should be useful when we prioritize this integration. @@ -174,7 +177,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, } } if !valid { - return nil, errors.New("error validating token and extracting claims") + return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; error validating k8sSA token and extracting claims")) } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -182,11 +185,11 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, if err = claims.Validate(jose.Expected{ Issuer: k8sSAIssuer, }); err != nil { - return nil, errors.Wrapf(err, "invalid token claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, "k8ssa.authorizeToken; invalid k8sSA token claims") } if claims.Subject == "" { - return nil, errors.New("token subject cannot be empty") + return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; k8sSA token subject cannot be empty")) } return &claims, nil @@ -196,14 +199,13 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, // revoke the certificate with serial number in the `sub` property. func (p *K8sSA) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) - return err + return errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeRevoke") } // AuthorizeSign validates the given token. func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - _, err := p.authorizeToken(token, p.audiences.Sign) - if err != nil { - return nil, err + if _, err := p.authorizeToken(token, p.audiences.Sign); err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSign") } return []SignOption{ @@ -219,7 +221,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // AuthorizeRenew returns an error if the renewal is disabled. func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("k8ssa.AuthorizeRenew; renew is disabled for k8sSA provisioner %s", p.GetID())) } return nil } @@ -227,17 +229,14 @@ func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) erro // AuthorizeSSHSign validates an request for an SSH certificate. func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("authorizeSSHSign: ssh ca is disabled for provisioner %s", p.GetID()) + return nil, errs.Unauthorized(errors.Errorf("k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner %s", p.GetID())) } - _, err := p.authorizeToken(token, p.audiences.SSHSign) - if err != nil { - return nil, errors.Wrap(err, "authorizeSSHSign") + if _, err := p.authorizeToken(token, p.audiences.SSHSign); err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSSHSign") } // Default to a user certificate with no principals if not set - signOptions := []SignOption{ - sshCertificateDefaultsModifier{CertType: SSHUserCert}, - } + signOptions := []SignOption{sshCertDefaultsModifier{CertType: SSHUserCert}} return append(signOptions, // Set the default extensions. diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 692e7bab..09a856c5 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -3,11 +3,13 @@ package provisioner import ( "context" "crypto/x509" + "net/http" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -36,6 +38,7 @@ func TestK8sSA_authorizeToken(t *testing.T) { p *K8sSA token string err error + code int } tests := map[string]func(*testing.T) test{ "fail/bad-token": func(t *testing.T) test { @@ -44,7 +47,24 @@ func TestK8sSA_authorizeToken(t *testing.T) { return test{ p: p, token: "foo", - err: errors.New("error parsing token"), + code: http.StatusUnauthorized, + err: errors.New("k8ssa.authorizeToken; error parsing k8sSA token"), + } + }, + "fail/not-implemented": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateK8sSA(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk) + p.pubKeys = nil + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented"), + code: http.StatusUnauthorized, } }, "fail/error-validating-token": func(t *testing.T) test { @@ -58,7 +78,8 @@ func TestK8sSA_authorizeToken(t *testing.T) { return test{ p: p, token: tok, - err: errors.New("error validating token and extracting claims"), + err: errors.New("k8ssa.authorizeToken; error validating k8sSA token and extracting claims"), + code: http.StatusUnauthorized, } }, "fail/invalid-issuer": func(t *testing.T) test { @@ -73,7 +94,8 @@ func TestK8sSA_authorizeToken(t *testing.T) { return test{ p: p, token: tok, - err: errors.New("invalid token claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"), + code: http.StatusUnauthorized, + err: errors.New("k8ssa.authorizeToken; invalid k8sSA token claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"), } }, "ok": func(t *testing.T) test { @@ -94,6 +116,9 @@ func TestK8sSA_authorizeToken(t *testing.T) { tc := tt(t) if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -105,12 +130,12 @@ func TestK8sSA_authorizeToken(t *testing.T) { } } -func TestK8sSA_AuthorizeSign(t *testing.T) { +func TestK8sSA_AuthorizeRevoke(t *testing.T) { type test struct { p *K8sSA token string - ctx context.Context err error + code int } tests := map[string]func(*testing.T) test{ "fail/invalid-token": func(t *testing.T) test { @@ -119,10 +144,11 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { return test{ p: p, token: "foo", - err: errors.New("error parsing token"), + code: http.StatusUnauthorized, + err: errors.New("k8ssa.AuthorizeRevoke: k8ssa.authorizeToken; error parsing k8sSA token"), } }, - "fail/ssh-unimplemented": func(t *testing.T) test { + "ok": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) p, err := generateK8sSA(jwk.Public().Key) @@ -131,9 +157,92 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) return test{ p: p, - ctx: NewContextWithMethod(context.Background(), SignSSHMethod), token: tok, - err: errors.Errorf("ssh certificates not enabled for k8s ServiceAccount provisioners"), + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestK8sSA_AuthorizeRenew(t *testing.T) { + type test struct { + p *K8sSA + cert *x509.Certificate + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/renew-disabled": func(t *testing.T) test { + p, err := generateK8sSA(nil) + assert.FatalError(t, err) + // disable renewal + disable := true + p.Claims = &Claims{DisableRenewal: &disable} + p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + return test{ + p: p, + cert: &x509.Certificate{}, + code: http.StatusUnauthorized, + err: errors.Errorf("k8ssa.AuthorizeRenew; renew is disabled for k8sSA provisioner %s", p.GetID()), + } + }, + "ok": func(t *testing.T) test { + p, err := generateK8sSA(nil) + assert.FatalError(t, err) + return test{ + p: p, + cert: &x509.Certificate{}, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestK8sSA_AuthorizeSign(t *testing.T) { + type test struct { + p *K8sSA + token string + code int + err error + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateK8sSA(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("k8ssa.AuthorizeSign: k8ssa.authorizeToken; error parsing k8sSA token"), } }, "ok": func(t *testing.T) test { @@ -145,7 +254,6 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) return test{ p: p, - ctx: NewContextWithMethod(context.Background(), SignMethod), token: tok, } }, @@ -153,8 +261,11 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil { + if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -187,20 +298,37 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { } } -func TestK8sSA_AuthorizeRevoke(t *testing.T) { +func TestK8sSA_AuthorizeSSHSign(t *testing.T) { type test struct { p *K8sSA token string + code int err error } tests := map[string]func(*testing.T) test{ + "fail/sshCA-disabled": func(t *testing.T) test { + p, err := generateK8sSA(nil) + assert.FatalError(t, err) + // disable sshCA + disable := false + p.Claims = &Claims{EnableSSHCA: &disable} + p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.Errorf("k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner %s", p.GetID()), + } + }, "fail/invalid-token": func(t *testing.T) test { p, err := generateK8sSA(nil) assert.FatalError(t, err) return test{ p: p, token: "foo", - err: errors.New("error parsing token"), + code: http.StatusUnauthorized, + err: errors.New("k8ssa.AuthorizeSSHSign: k8ssa.authorizeToken; error parsing k8sSA token"), } }, "ok": func(t *testing.T) test { @@ -219,45 +347,36 @@ func TestK8sSA_AuthorizeRevoke(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil { + if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { - assert.Nil(t, tc.err) - } - }) - } -} - -func TestK8sSA_AuthorizeRenew(t *testing.T) { - p1, err := generateK8sSA(nil) - assert.FatalError(t, err) - p2, err := generateK8sSA(nil) - assert.FatalError(t, err) - - // disable renewal - disable := true - p2.Claims = &Claims{DisableRenewal: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) - assert.FatalError(t, err) - - type args struct { - cert *x509.Certificate - } - tests := []struct { - name string - prov *K8sSA - args args - wantErr bool - }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("X5C.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) + if assert.Nil(t, tc.err) { + if assert.NotNil(t, opts) { + tot := 0 + for _, o := range opts { + switch v := o.(type) { + case sshCertDefaultsModifier: + assert.Equals(t, v.CertType, SSHUserCert) + case *sshDefaultExtensionModifier: + case *sshCertificateValidityValidator: + assert.Equals(t, v.Claimer, tc.p.claimer) + case *sshDefaultPublicKeyValidator: + case *sshCertificateDefaultValidator: + case *sshDefaultDuration: + assert.Equals(t, v.Claimer, tc.p.claimer) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + tot++ + } + assert.Equals(t, tot, 6) + } + } } }) } diff --git a/authority/provisioner/method.go b/authority/provisioner/method.go index 4e5f32a7..775ed96f 100644 --- a/authority/provisioner/method.go +++ b/authority/provisioner/method.go @@ -16,14 +16,16 @@ const ( SignMethod Method = iota // RevokeMethod is the method used to revoke X.509 certificates. RevokeMethod - // SignSSHMethod is the method used to sign SSH certificates. - SignSSHMethod - // RenewSSHMethod is the method used to renew SSH certificates. - RenewSSHMethod - // RevokeSSHMethod is the method used to revoke SSH certificates. - RevokeSSHMethod - // RekeySSHMethod is the method used to rekey SSH certificates. - RekeySSHMethod + // RenewMethod is the method used to renew X.509 certificates. + RenewMethod + // SSHSignMethod is the method used to sign SSH certificates. + SSHSignMethod + // SSHRenewMethod is the method used to renew SSH certificates. + SSHRenewMethod + // SSHRevokeMethod is the method used to revoke SSH certificates. + SSHRevokeMethod + // SSHRekeyMethod is the method used to rekey SSH certificates. + SSHRekeyMethod ) // String returns a string representation of the context method. @@ -33,14 +35,16 @@ func (m Method) String() string { return "sign-method" case RevokeMethod: return "revoke-method" - case SignSSHMethod: - return "sign-ssh-method" - case RenewSSHMethod: - return "renew-ssh-method" - case RevokeSSHMethod: - return "revoke-ssh-method" - case RekeySSHMethod: - return "rekey-ssh-method" + case RenewMethod: + return "renew-method" + case SSHSignMethod: + return "ssh-sign-method" + case SSHRenewMethod: + return "ssh-renew-method" + case SSHRevokeMethod: + return "ssh-revoke-method" + case SSHRekeyMethod: + return "ssh-rekey-method" default: return "unknown" } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 4c4b68d2..87710ebb 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -189,17 +190,17 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { Audience: jose.Audience{o.ClientID}, Time: time.Now().UTC(), }, time.Minute); err != nil { - return errors.Wrap(err, "failed to validate payload") + return errs.Wrap(http.StatusUnauthorized, err, "validatePayload: failed to validate oidc token payload") } // Validate azp if present if p.AuthorizedParty != "" && p.AuthorizedParty != o.ClientID { - return errors.New("failed to validate payload: invalid azp") + return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: invalid azp")) } // Enforce an email claim if p.Email == "" { - return errors.New("failed to validate payload: email not found") + return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: email not found")) } // Validate domains (case-insensitive) @@ -213,7 +214,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { } } if !found { - return errors.New("failed to validate payload: email is not allowed") + return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: email is not allowed")) } } @@ -229,7 +230,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { } } if !found { - return errors.New("validation failed: invalid group") + return errs.Unauthorized(errors.New("validatePayload: oidc token payload validation failed: invalid group")) } } @@ -241,13 +242,15 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errs.Wrap(http.StatusUnauthorized, err, + "oidc.AuthorizeToken; error parsing oidc token") } // Parse claims to get the kid var claims openIDPayload if err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil { - return nil, errors.Wrap(err, "error parsing claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, + "oidc.AuthorizeToken; error parsing oidc token claims") } found := false @@ -260,11 +263,11 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) { } } if !found { - return nil, errors.New("cannot validate token") + return nil, errs.Unauthorized(errors.New("oidc.AuthorizeToken; cannot validate oidc token")) } if err := o.ValidatePayload(claims); err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeToken") } return &claims, nil @@ -276,21 +279,21 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) { func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error { claims, err := o.authorizeToken(token) if err != nil { - return err + return errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeRevoke") } // Only admins can revoke certificates. if o.IsAdmin(claims.Email) { return nil } - return errors.New("cannot revoke with non-admin token") + return errs.Unauthorized(errors.New("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token")) } // AuthorizeSign validates the given token. func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { claims, err := o.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSign") } so := []SignOption{ @@ -315,7 +318,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // certificate was configured to allow renewals. func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if o.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", o.GetID()) + return errs.Unauthorized(errors.Errorf("oidc.AuthorizeRenew; renew is disabled for oidc provisioner %s", o.GetID())) } return nil } @@ -323,22 +326,22 @@ func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !o.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID()) + return nil, errs.Unauthorized(errors.Errorf("oidc.AuthorizeSSHSign; sshCA is disabled for oidc provisioner %s", o.GetID())) } claims, err := o.authorizeToken(token) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign") } signOptions := []SignOption{ // set the key id to the token email - sshCertificateKeyIDModifier(claims.Email), + sshCertKeyIDModifier(claims.Email), } // Get the identity using either the default identityFunc or one injected // externally. iden, err := o.getIdentityFunc(o, claims.Email) if err != nil { - return nil, errors.Wrap(err, "authorizeSSHSign") + return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign") } defaults := SSHOptions{ CertType: SSHUserCert, @@ -354,7 +357,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Default to a user certificate with usernames as principals if those options // are not set. - signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) + signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) return append(signOptions, // Set the default extensions @@ -374,14 +377,14 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error { claims, err := o.authorizeToken(token) if err != nil { - return err + return errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHRevoke") } // Only admins can revoke certificates. - if o.IsAdmin(claims.Email) { - return nil + if !o.IsAdmin(claims.Email) { + return errs.Unauthorized(errors.New("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")) } - return errors.New("cannot revoke with non-admin token") + return nil } func getAndDecode(uri string, v interface{}) error { diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index cbb7b2a2..d0782c1e 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -7,12 +7,14 @@ import ( "crypto/rsa" "crypto/x509" "fmt" + "net/http" "strings" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" ) @@ -206,20 +208,21 @@ func TestOIDC_authorizeToken(t *testing.T) { name string prov *OIDC args args + code int wantErr bool }{ - {"ok1", p1, args{t1}, false}, - {"ok2", p2, args{t2}, false}, - {"fail-email", p3, args{failEmail}, true}, - {"fail-domain", p3, args{failDomain}, true}, - {"fail-key", p1, args{failKey}, true}, - {"fail-token", p1, args{failTok}, true}, - {"fail-claims", p1, args{failClaims}, true}, - {"fail-issuer", p1, args{failIss}, true}, - {"fail-audience", p1, args{failAud}, true}, - {"fail-signature", p1, args{failSig}, true}, - {"fail-expired", p1, args{failExp}, true}, - {"fail-not-before", p1, args{failNbf}, true}, + {"ok1", p1, args{t1}, http.StatusOK, false}, + {"ok2", p2, args{t2}, http.StatusOK, false}, + {"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true}, + {"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, true}, + {"fail-key", p1, args{failKey}, http.StatusUnauthorized, true}, + {"fail-token", p1, args{failTok}, http.StatusUnauthorized, true}, + {"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, true}, + {"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, true}, + {"fail-audience", p1, args{failAud}, http.StatusUnauthorized, true}, + {"fail-signature", p1, args{failSig}, http.StatusUnauthorized, true}, + {"fail-expired", p1, args{failExp}, http.StatusUnauthorized, true}, + {"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -230,6 +233,9 @@ func TestOIDC_authorizeToken(t *testing.T) { return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else { assert.NotNil(t, got) @@ -282,21 +288,24 @@ func TestOIDC_AuthorizeSign(t *testing.T) { name string prov *OIDC args args + code int wantErr bool }{ - {"ok1", p1, args{t1}, false}, - {"admin", p3, args{okAdmin}, false}, - {"fail-email", p3, args{failEmail}, true}, + {"ok1", p1, args{t1}, http.StatusOK, false}, + {"admin", p3, args{okAdmin}, http.StatusOK, false}, + {"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignMethod) - got, err := tt.prov.AuthorizeSign(ctx, tt.args.token) + got, err := tt.prov.AuthorizeSign(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else { if assert.NotNil(t, got) { @@ -330,6 +339,107 @@ func TestOIDC_AuthorizeSign(t *testing.T) { } } +func TestOIDC_AuthorizeRevoke(t *testing.T) { + srv := generateJWKServer(2) + defer srv.Close() + + var keys jose.JSONWebKeySet + assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys)) + + // Create test provisioners + p1, err := generateOIDC() + assert.FatalError(t, err) + p3, err := generateOIDC() + assert.FatalError(t, err) + // Admin + Domains + p3.Admins = []string{"name@smallstep.com", "root@example.com"} + p3.Domains = []string{"smallstep.com"} + + // Update configuration endpoints and initialize + config := Config{Claims: globalProvisionerClaims} + p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" + p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" + assert.FatalError(t, p1.Init(config)) + assert.FatalError(t, p3.Init(config)) + + t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0]) + assert.FatalError(t, err) + // Admin email not in domains + okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0]) + assert.FatalError(t, err) + // Invalid email + failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0]) + assert.FatalError(t, err) + + type args struct { + token string + } + tests := []struct { + name string + prov *OIDC + args args + code int + wantErr bool + }{ + {"ok1", p1, args{t1}, http.StatusUnauthorized, true}, + {"admin", p3, args{okAdmin}, http.StatusOK, false}, + {"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token) + if (err != nil) != tt.wantErr { + fmt.Println(tt) + t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr) + return + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) + } + }) + } +} + +func TestOIDC_AuthorizeRenew(t *testing.T) { + p1, err := generateOIDC() + assert.FatalError(t, err) + p2, err := generateOIDC() + assert.FatalError(t, err) + + // disable renewal + disable := true + p2.Claims = &Claims{DisableRenewal: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + prov *OIDC + args args + code int + wantErr bool + }{ + {"ok", p1, args{nil}, http.StatusOK, false}, + {"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert) + if (err != nil) != tt.wantErr { + t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) + } + }) + } +} + func TestOIDC_AuthorizeSSHSign(t *testing.T) { tm, fn := mockNow() defer fn() @@ -351,9 +461,16 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { assert.FatalError(t, err) p5, err := generateOIDC() assert.FatalError(t, err) + p6, err := generateOIDC() + assert.FatalError(t, err) // Admin + Domains p3.Admins = []string{"name@smallstep.com", "root@example.com"} p3.Domains = []string{"smallstep.com"} + // disable sshCA + disable := false + p6.Claims = &Claims{EnableSSHCA: &disable} + p6.claimer, err = NewClaimer(p6.Claims, globalProvisionerClaims) + assert.FatalError(t, err) // Update configuration endpoints and initialize config := Config{Claims: globalProvisionerClaims} @@ -425,48 +542,53 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { prov *OIDC args args expected *SSHOptions + code int wantErr bool wantSignErr bool }{ - {"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false}, - {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false}, - {"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false}, + {"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false}, + {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false}, + {"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false}, {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, &SSHOptions{CertType: "user", Principals: []string{"name"}, - ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false}, {"ok-principals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{Principals: []string{"mariano"}}, pub}, &SSHOptions{CertType: "user", Principals: []string{"mariano"}, - ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false}, {"ok-emptyPrincipals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{}, pub}, &SSHOptions{CertType: "user", Principals: []string{"max", "mariano"}, - ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false}, {"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, &SSHOptions{CertType: "user", Principals: []string{"name"}, - ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, - {"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, false, false}, - {"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, false, false}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false}, + {"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, http.StatusOK, false, false}, + {"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, http.StatusOK, false, false}, {"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}, pub}, &SSHOptions{CertType: "user", Principals: []string{"root"}, - ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false}, {"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, &SSHOptions{CertType: "user", Principals: []string{"name"}, - ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false}, - {"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false}, - {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true}, - {"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, false, true}, - {"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, false, true}, - {"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, true, false}, - {"fail-getIdentity", p5, args{failGetIdentityToken, SSHOptions{}, pub}, nil, true, false}, + ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false}, + {"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, + expectedHostOptions, http.StatusOK, false, false}, + {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true}, + {"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, http.StatusOK, false, true}, + {"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, http.StatusOK, false, true}, + {"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false}, + {"fail-getIdentity", p5, args{failGetIdentityToken, SSHOptions{}, pub}, nil, http.StatusInternalServerError, true, false}, + {"fail-sshCA-disabled", p6, args{"foo", SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := NewContextWithMethod(context.Background(), SignSSHMethod) - got, err := tt.prov.AuthorizeSSHSign(ctx, tt.args.token) + got, err := tt.prov.AuthorizeSSHSign(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { t.Errorf("OIDC.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr) return } if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) @@ -484,36 +606,32 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) { } } -func TestOIDC_AuthorizeRevoke(t *testing.T) { +func TestOIDC_AuthorizeSSHRevoke(t *testing.T) { + p1, err := generateOIDC() + assert.FatalError(t, err) + p2, err := generateOIDC() + assert.FatalError(t, err) + p2.Admins = []string{"root@example.com"} + srv := generateJWKServer(2) defer srv.Close() - var keys jose.JSONWebKeySet assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys)) - // Create test provisioners - p1, err := generateOIDC() - assert.FatalError(t, err) - p3, err := generateOIDC() - assert.FatalError(t, err) - // Admin + Domains - p3.Admins = []string{"name@smallstep.com", "root@example.com"} - p3.Domains = []string{"smallstep.com"} - - // Update configuration endpoints and initialize config := Config{Claims: globalProvisionerClaims} p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" - p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" + p2.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" assert.FatalError(t, p1.Init(config)) - assert.FatalError(t, p3.Init(config)) + assert.FatalError(t, p2.Init(config)) - t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0]) + // Invalid email + failEmail, err := generateToken("subject", "the-issuer", p1.ClientID, "", []string{}, time.Now(), &keys.Keys[0]) assert.FatalError(t, err) // Admin email not in domains - okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0]) + noAdmin, err := generateToken("subject", "the-issuer", p1.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0]) assert.FatalError(t, err) - // Invalid email - failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0]) + // Admin email in domains + okAdmin, err := generateToken("subject", "the-issuer", p2.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0]) assert.FatalError(t, err) type args struct { @@ -523,52 +641,22 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) { name string prov *OIDC args args + code int wantErr bool }{ - {"ok1", p1, args{t1}, true}, - {"admin", p3, args{okAdmin}, false}, - {"fail-email", p3, args{failEmail}, true}, + {"ok", p2, args{okAdmin}, http.StatusOK, false}, + {"fail/invalid-token", p1, args{failEmail}, http.StatusUnauthorized, true}, + {"fail/not-admin", p1, args{noAdmin}, http.StatusUnauthorized, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token) + err := tt.prov.AuthorizeSSHRevoke(context.Background(), tt.args.token) if (err != nil) != tt.wantErr { - fmt.Println(tt) - t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - -func TestOIDC_AuthorizeRenew(t *testing.T) { - p1, err := generateOIDC() - assert.FatalError(t, err) - p2, err := generateOIDC() - assert.FatalError(t, err) - - // disable renewal - disable := true - p2.Claims = &Claims{DisableRenewal: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) - assert.FatalError(t, err) - - type args struct { - cert *x509.Certificate - } - tests := []struct { - name string - prov *OIDC - args args - wantErr bool - }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("OIDC.AuthorizeSSHRevoke() error = %v, wantErr %v", err, tt.wantErr) + } else if err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.code) } }) } diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 4b4200f5..40e1e309 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" "golang.org/x/crypto/ssh" ) @@ -283,43 +284,43 @@ type base struct{} // AuthorizeSign returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for signing x509 Certificates. func (b *base) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - return nil, errors.New("not implemented; provisioner does not implement AuthorizeSign") + return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSign not implemented")) } // AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for revoking x509 Certificates. func (b *base) AuthorizeRevoke(ctx context.Context, token string) error { - return errors.New("not implemented; provisioner does not implement AuthorizeRevoke") + return errs.Unauthorized(errors.New("provisioner.AuthorizeRevoke not implemented")) } // AuthorizeRenew returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for renewing x509 Certificates. func (b *base) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { - return errors.New("not implemented; provisioner does not implement AuthorizeRenew") + return errs.Unauthorized(errors.New("provisioner.AuthorizeRenew not implemented")) } // AuthorizeSSHSign returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for signing SSH Certificates. func (b *base) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { - return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHSign") + return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHSign not implemented")) } // AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for revoking SSH Certificates. func (b *base) AuthorizeSSHRevoke(ctx context.Context, token string) error { - return errors.New("not implemented; provisioner does not implement AuthorizeSSHRevoke") + return errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRevoke not implemented")) } // AuthorizeSSHRenew returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for renewing SSH Certificates. func (b *base) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { - return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRenew") + return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRenew not implemented")) } // AuthorizeSSHRekey returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for rekeying SSH Certificates. func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { - return nil, nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRekey") + return nil, nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRekey not implemented")) } // Identity is the type representing an externally supplied identity that is used diff --git a/authority/provisioner/provisioner_test.go b/authority/provisioner/provisioner_test.go index 14e62769..2577c62f 100644 --- a/authority/provisioner/provisioner_test.go +++ b/authority/provisioner/provisioner_test.go @@ -1,10 +1,14 @@ package provisioner import ( + "context" + "net/http" "testing" "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" + "golang.org/x/crypto/ssh" ) func TestType_String(t *testing.T) { @@ -101,3 +105,93 @@ func TestDefaultIdentityFunc(t *testing.T) { }) } } + +func TestUnimplementedMethods(t *testing.T) { + tests := []struct { + name string + p Interface + method Method + }{ + {"jwk/sshRekey", &JWK{}, SSHRekeyMethod}, + {"jwk/sshRenew", &JWK{}, SSHRenewMethod}, + {"aws/revoke", &AWS{}, RevokeMethod}, + {"aws/sshRenew", &AWS{}, SSHRenewMethod}, + {"aws/rekey", &AWS{}, SSHRekeyMethod}, + {"aws/sshRevoke", &AWS{}, SSHRevokeMethod}, + {"azure/revoke", &Azure{}, RevokeMethod}, + {"azure/sshRenew", &Azure{}, SSHRenewMethod}, + {"azure/sshRekey", &Azure{}, SSHRekeyMethod}, + {"azure/sshRevoke", &Azure{}, SSHRevokeMethod}, + {"gcp/revoke", &GCP{}, RevokeMethod}, + {"gcp/sshRenew", &GCP{}, SSHRenewMethod}, + {"gcp/sshRekey", &GCP{}, SSHRekeyMethod}, + {"gcp/sshRevoke", &GCP{}, SSHRevokeMethod}, + {"oidc/sshRenew", &OIDC{}, SSHRenewMethod}, + {"oidc/sshRekey", &OIDC{}, SSHRekeyMethod}, + {"x5c/sshRenew", &X5C{}, SSHRenewMethod}, + {"x5c/sshRekey", &X5C{}, SSHRekeyMethod}, + {"x5c/sshRevoke", &X5C{}, SSHRekeyMethod}, + {"acme/revoke", &ACME{}, RevokeMethod}, + {"acme/sshSign", &ACME{}, SSHSignMethod}, + {"acme/sshRekey", &ACME{}, SSHRekeyMethod}, + {"acme/sshRenew", &ACME{}, SSHRenewMethod}, + {"acme/sshRevoke", &ACME{}, SSHRevokeMethod}, + {"sshpop/sign", &SSHPOP{}, SignMethod}, + {"sshpop/renew", &SSHPOP{}, RenewMethod}, + {"sshpop/revoke", &SSHPOP{}, RevokeMethod}, + {"sshpop/sshSign", &SSHPOP{}, SSHSignMethod}, + {"k8ssa/sshRekey", &K8sSA{}, SSHRekeyMethod}, + {"k8ssa/sshRenew", &K8sSA{}, SSHRenewMethod}, + {"k8ssa/sshRevoke", &K8sSA{}, SSHRevokeMethod}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + err error + msg string + ) + + switch tt.method { + case SignMethod: + var signOpts []SignOption + signOpts, err = tt.p.AuthorizeSign(context.Background(), "") + assert.Nil(t, signOpts) + msg = "provisioner.AuthorizeSign not implemented" + case RenewMethod: + err = tt.p.AuthorizeRenew(context.Background(), nil) + msg = "provisioner.AuthorizeRenew not implemented" + case RevokeMethod: + err = tt.p.AuthorizeRevoke(context.Background(), "") + msg = "provisioner.AuthorizeRevoke not implemented" + case SSHSignMethod: + var signOpts []SignOption + signOpts, err = tt.p.AuthorizeSSHSign(context.Background(), "") + assert.Nil(t, signOpts) + msg = "provisioner.AuthorizeSSHSign not implemented" + case SSHRenewMethod: + var cert *ssh.Certificate + cert, err = tt.p.AuthorizeSSHRenew(context.Background(), "") + assert.Nil(t, cert) + msg = "provisioner.AuthorizeSSHRenew not implemented" + case SSHRekeyMethod: + var ( + cert *ssh.Certificate + signOpts []SignOption + ) + cert, signOpts, err = tt.p.AuthorizeSSHRekey(context.Background(), "") + assert.Nil(t, cert) + assert.Nil(t, signOpts) + msg = "provisioner.AuthorizeSSHRekey not implemented" + case SSHRevokeMethod: + err = tt.p.AuthorizeSSHRevoke(context.Background(), "") + msg = "provisioner.AuthorizeSSHRevoke not implemented" + default: + t.Errorf("unexpected method %s", tt.method) + } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), http.StatusUnauthorized) + assert.Equals(t, err.Error(), msg) + }) + } +} diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 1e6547b7..ed049b6c 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -30,7 +30,7 @@ type SignOption interface{} // CertificateValidator is the interface used to validate a X.509 certificate. type CertificateValidator interface { SignOption - Valid(crt *x509.Certificate) error + Valid(cert *x509.Certificate, o Options) error } // CertificateRequestValidator is the interface used to validate a X.509 @@ -106,7 +106,7 @@ func (v commonNameValidator) Valid(req *x509.CertificateRequest) error { return errors.New("certificate request cannot contain an empty common name") } if req.Subject.CommonName != string(v) { - return errors.Errorf("certificate request does not contain the valid common name, got %s, want %s", req.Subject.CommonName, v) + return errors.Errorf("certificate request does not contain the valid common name; requested common name = %s, token subject = %s", req.Subject.CommonName, v) } return nil } @@ -265,33 +265,30 @@ func newValidityValidator(min, max time.Duration) *validityValidator { // Valid validates the certificate validity settings (notBefore/notAfter) and // and total duration. -func (v *validityValidator) Valid(crt *x509.Certificate) error { +func (v *validityValidator) Valid(cert *x509.Certificate, o Options) error { var ( - na = crt.NotAfter.Truncate(time.Second) - nb = crt.NotBefore.Truncate(time.Second) + na = cert.NotAfter.Truncate(time.Second) + nb = cert.NotBefore.Truncate(time.Second) now = time.Now().Truncate(time.Second) ) - // To not take into account the backdate, time.Now() will be used to - // calculate the duration if NotBefore is in the past. - var d time.Duration - if now.After(nb) { - d = na.Sub(now) - } else { - d = na.Sub(nb) - } + d := na.Sub(nb) if na.Before(now) { - return errors.Errorf("NotAfter: %v cannot be in the past", na) + return errors.Errorf("notAfter cannot be in the past; na=%v", na) } if na.Before(nb) { - return errors.Errorf("NotAfter: %v cannot be before NotBefore: %v", na, nb) + return errors.Errorf("notAfter cannot be before notBefore; na=%v, nb=%v", na, nb) } if d < v.min { return errors.Errorf("requested duration of %v is less than the authorized minimum certificate duration of %v", d, v.min) } - if d > v.max { + // NOTE: this check is not "technically correct". We're allowing the max + // duration of a cert to be "max + backdate" and not all certificates will + // be backdated (e.g. if a user passes the NotBefore value then we do not + // apply a backdate). This is good enough. + if d > v.max+o.Backdate { return errors.Errorf("requested duration of %v is more than the authorized maximum certificate duration of %v", d, v.max) } diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index 1076d3b5..74c8d1f4 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -3,9 +3,10 @@ package provisioner import ( "crypto/x509" "crypto/x509/pkix" + "fmt" "net" "net/url" - "reflect" + "strings" "testing" "time" @@ -48,22 +49,22 @@ func Test_emailOnlyIdentity_Valid(t *testing.T) { } func Test_defaultPublicKeyValidator_Valid(t *testing.T) { - _shortRSA, err := pemutil.Read("./testdata/short-rsa.csr") + _shortRSA, err := pemutil.Read("./testdata/certs/short-rsa.csr") assert.FatalError(t, err) shortRSA, ok := _shortRSA.(*x509.CertificateRequest) assert.Fatal(t, ok) - _rsa, err := pemutil.Read("./testdata/rsa.csr") + _rsa, err := pemutil.Read("./testdata/certs/rsa.csr") assert.FatalError(t, err) rsaCSR, ok := _rsa.(*x509.CertificateRequest) assert.Fatal(t, ok) - _ecdsa, err := pemutil.Read("./testdata/ecdsa.csr") + _ecdsa, err := pemutil.Read("./testdata/certs/ecdsa.csr") assert.FatalError(t, err) ecdsaCSR, ok := _ecdsa.(*x509.CertificateRequest) assert.Fatal(t, ok) - _ed25519, err := pemutil.Read("./testdata/ed25519.csr") + _ed25519, err := pemutil.Read("./testdata/certs/ed25519.csr") assert.FatalError(t, err) ed25519CSR, ok := _ed25519.(*x509.CertificateRequest) assert.Fatal(t, ok) @@ -246,30 +247,191 @@ func Test_ipAddressesValidator_Valid(t *testing.T) { } func Test_validityValidator_Valid(t *testing.T) { - type fields struct { - min time.Duration - max time.Duration + type test struct { + cert *x509.Certificate + opts Options + vv *validityValidator + err error } - type args struct { - crt *x509.Certificate + tests := map[string]func() test{ + "fail/notAfter-past": func() test { + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: &x509.Certificate{NotAfter: time.Now().Add(-5 * time.Minute)}, + opts: Options{}, + err: errors.New("notAfter cannot be in the past"), + } + }, + "fail/notBefore-after-notAfter": func() test { + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: &x509.Certificate{NotBefore: time.Now().Add(10 * time.Minute), + NotAfter: time.Now().Add(5 * time.Minute)}, + opts: Options{}, + err: errors.New("notAfter cannot be before notBefore"), + } + }, + "fail/duration-too-short": func() test { + n := now() + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: &x509.Certificate{NotBefore: n, + NotAfter: n.Add(3 * time.Minute)}, + opts: Options{}, + err: errors.New("is less than the authorized minimum certificate duration of "), + } + }, + "ok/duration-exactly-min": func() test { + n := now() + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: &x509.Certificate{NotBefore: n, + NotAfter: n.Add(5 * time.Minute)}, + opts: Options{}, + } + }, + "fail/duration-too-great": func() test { + n := now() + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: &x509.Certificate{NotBefore: n, + NotAfter: n.Add(24*time.Hour + time.Second)}, + err: errors.New("is more than the authorized maximum certificate duration of "), + } + }, + "ok/duration-exactly-max": func() test { + n := time.Now() + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: &x509.Certificate{NotBefore: n, + NotAfter: n.Add(24 * time.Hour)}, + } + }, + "ok/duration-exact-min-with-backdate": func() test { + now := time.Now() + cert := &x509.Certificate{NotBefore: now, NotAfter: now.Add(5 * time.Minute)} + time.Sleep(time.Second) + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: cert, + opts: Options{Backdate: time.Second}, + } + }, + "ok/duration-exact-max-with-backdate": func() test { + backdate := time.Second + now := time.Now() + cert := &x509.Certificate{NotBefore: now, NotAfter: now.Add(24*time.Hour + backdate)} + time.Sleep(backdate) + return test{ + vv: &validityValidator{5 * time.Minute, 24 * time.Hour}, + cert: cert, + opts: Options{Backdate: backdate}, + } + }, } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - // TODO: Add test cases. + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tt := run() + if err := tt.vv.Valid(tt.cert, tt.opts); err != nil { + if assert.NotNil(t, tt.err, fmt.Sprintf("expected no error, but got err = %s", err.Error())) { + assert.True(t, strings.Contains(err.Error(), tt.err.Error()), + fmt.Sprintf("want err = %s, but got err = %s", tt.err.Error(), err.Error())) + } + } else { + assert.Nil(t, tt.err, fmt.Sprintf("expected err = %s, but not ", tt.err)) + } + }) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v := &validityValidator{ - min: tt.fields.min, - max: tt.fields.max, +} + +func Test_profileDefaultDuration_Option(t *testing.T) { + type test struct { + so Options + pdd profileDefaultDuration + cert *x509.Certificate + valid func(*x509.Certificate) + } + tests := map[string]func() test{ + "ok/notBefore-notAfter-duration-empty": func() test { + return test{ + pdd: profileDefaultDuration(0), + so: Options{}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + n := now() + assert.True(t, n.After(cert.NotBefore)) + assert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore)) + + assert.True(t, n.Add(24*time.Hour).After(cert.NotAfter)) + assert.True(t, n.Add(24*time.Hour).Add(-1*time.Minute).Before(cert.NotAfter)) + }, } - if err := v.Valid(tt.args.crt); (err != nil) != tt.wantErr { - t.Errorf("validityValidator.Valid() error = %v, wantErr %v", err, tt.wantErr) + }, + "ok/notBefore-set": func() test { + nb := time.Now().Add(5 * time.Minute).UTC() + return test{ + pdd: profileDefaultDuration(0), + so: Options{NotBefore: NewTimeDuration(nb)}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, nb) + assert.Equals(t, cert.NotAfter, nb.Add(24*time.Hour)) + }, } + }, + "ok/duration-set": func() test { + d := 4 * time.Hour + return test{ + pdd: profileDefaultDuration(d), + so: Options{Backdate: time.Second}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + n := now() + assert.True(t, n.After(cert.NotBefore), fmt.Sprintf("expected now = %s to be after cert.NotBefore = %s", n, cert.NotBefore)) + assert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore)) + + assert.True(t, n.Add(d).After(cert.NotAfter)) + assert.True(t, n.Add(d).Add(-1*time.Minute).Before(cert.NotAfter)) + }, + } + }, + "ok/notAfter-set": func() test { + na := now().Add(10 * time.Minute).UTC() + return test{ + pdd: profileDefaultDuration(0), + so: Options{NotAfter: NewTimeDuration(na)}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + n := now() + assert.True(t, n.After(cert.NotBefore), fmt.Sprintf("expected now = %s to be after cert.NotBefore = %s", n, cert.NotBefore)) + assert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore)) + + assert.Equals(t, cert.NotAfter, na) + }, + } + }, + "ok/notBefore-and-notAfter-set": func() test { + nb := time.Now().Add(5 * time.Minute).UTC() + na := time.Now().Add(10 * time.Minute).UTC() + d := 4 * time.Hour + return test{ + pdd: profileDefaultDuration(d), + so: Options{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, nb) + assert.Equals(t, cert.NotAfter, na) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tt := run() + prof := &x509util.Leaf{} + prof.SetSubject(tt.cert) + assert.FatalError(t, tt.pdd.Option(tt.so)(prof), "unexpected error") + tt.valid(prof.Subject()) }) } } @@ -381,43 +543,3 @@ func Test_profileLimitDuration_Option(t *testing.T) { }) } } - -func Test_profileDefaultDuration_Option(t *testing.T) { - tm, fn := mockNow() - defer fn() - - v := profileDefaultDuration(24 * time.Hour) - type args struct { - so Options - } - tests := []struct { - name string - v profileDefaultDuration - args args - want *x509.Certificate - }{ - {"default", v, args{Options{}}, &x509.Certificate{NotBefore: tm, NotAfter: tm.Add(24 * time.Hour)}}, - {"backdate", v, args{Options{Backdate: 1 * time.Minute}}, &x509.Certificate{NotBefore: tm.Add(-1 * time.Minute), NotAfter: tm.Add(24 * time.Hour)}}, - {"notBefore", v, args{Options{NotBefore: NewTimeDuration(tm.Add(10 * time.Second))}}, &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(24*time.Hour + 10*time.Second)}}, - {"notAfter", v, args{Options{NotAfter: NewTimeDuration(tm.Add(1 * time.Hour))}}, &x509.Certificate{NotBefore: tm, NotAfter: tm.Add(1 * time.Hour)}}, - {"notBefore and notAfter", v, args{Options{NotBefore: NewTimeDuration(tm.Add(10 * time.Second)), NotAfter: NewTimeDuration(tm.Add(1 * time.Hour))}}, - &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(1 * time.Hour)}}, - {"notBefore and backdate", v, args{Options{Backdate: 1 * time.Minute, NotBefore: NewTimeDuration(tm.Add(10 * time.Second))}}, - &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(24*time.Hour + 10*time.Second)}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cert := &x509.Certificate{} - profile := &x509util.Leaf{} - profile.SetSubject(cert) - - fn := tt.v.Option(tt.args.so) - if err := fn(profile); err != nil { - t.Errorf("profileDefaultDuration.Option() error = %v", err) - } - if !reflect.DeepEqual(cert, tt.want) { - t.Errorf("profileDefaultDuration.Option() = %v, \nwant %v", cert, tt.want) - } - }) - } -} diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 643e0645..ec67baf1 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -78,7 +78,7 @@ func (o SSHOptions) Modify(cert *ssh.Certificate) error { case SSHHostCert: cert.CertType = ssh.HostCert default: - return errors.Errorf("ssh certificate has an unknown type: %s", o.CertType) + return errors.Errorf("ssh certificate has an unknown type - %s", o.CertType) } cert.KeyId = o.KeyID @@ -126,11 +126,11 @@ func (o sshCertPrincipalsModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertificateKeyIDModifier is an SSHCertificateModifier that sets the given +// sshCertKeyIDModifier is an SSHCertificateModifier that sets the given // Key ID in the SSH certificate. -type sshCertificateKeyIDModifier string +type sshCertKeyIDModifier string -func (m sshCertificateKeyIDModifier) Modify(cert *ssh.Certificate) error { +func (m sshCertKeyIDModifier) Modify(cert *ssh.Certificate) error { cert.KeyId = string(m) return nil } @@ -145,30 +145,30 @@ func (m sshCertTypeModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertificateValidAfterModifier is an SSHCertificateModifier that sets the +// sshCertValidAfterModifier is an SSHCertificateModifier that sets the // ValidAfter in the SSH certificate. -type sshCertificateValidAfterModifier uint64 +type sshCertValidAfterModifier uint64 -func (m sshCertificateValidAfterModifier) Modify(cert *ssh.Certificate) error { +func (m sshCertValidAfterModifier) Modify(cert *ssh.Certificate) error { cert.ValidAfter = uint64(m) return nil } -// sshCertificateValidBeforeModifier is an SSHCertificateModifier that sets the +// sshCertValidBeforeModifier is an SSHCertificateModifier that sets the // ValidBefore in the SSH certificate. -type sshCertificateValidBeforeModifier uint64 +type sshCertValidBeforeModifier uint64 -func (m sshCertificateValidBeforeModifier) Modify(cert *ssh.Certificate) error { +func (m sshCertValidBeforeModifier) Modify(cert *ssh.Certificate) error { cert.ValidBefore = uint64(m) return nil } -// sshCertificateDefaultModifier implements a SSHCertificateModifier that +// sshCertDefaultsModifier implements a SSHCertificateModifier that // modifies the certificate with the given options if they are not set. -type sshCertificateDefaultsModifier SSHOptions +type sshCertDefaultsModifier SSHOptions // Modify implements the SSHCertificateModifier interface. -func (m sshCertificateDefaultsModifier) Modify(cert *ssh.Certificate) error { +func (m sshCertDefaultsModifier) Modify(cert *ssh.Certificate) error { if cert.CertType == 0 { cert.CertType = sshCertTypeUInt32(m.CertType) } diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index e447065b..87716e37 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -38,6 +38,457 @@ func TestSSHOptions_Type(t *testing.T) { } } +func TestSSHOptions_Modify(t *testing.T) { + type test struct { + so *SSHOptions + cert *ssh.Certificate + valid func(*ssh.Certificate) + err error + } + tests := map[string](func() test){ + "fail/unexpected-cert-type": func() test { + return test{ + so: &SSHOptions{CertType: "foo"}, + cert: new(ssh.Certificate), + err: errors.Errorf("ssh certificate has an unknown type - foo"), + } + }, + "fail/validAfter-greater-validBefore": func() test { + return test{ + so: &SSHOptions{CertType: "user"}, + cert: &ssh.Certificate{ValidAfter: uint64(15), ValidBefore: uint64(10)}, + err: errors.Errorf("ssh certificate valid after cannot be greater than valid before"), + } + }, + "ok/user-cert": func() test { + return test{ + so: &SSHOptions{CertType: "user"}, + cert: new(ssh.Certificate), + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.CertType, uint32(ssh.UserCert)) + }, + } + }, + "ok/host-cert": func() test { + return test{ + so: &SSHOptions{CertType: "host"}, + cert: new(ssh.Certificate), + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.CertType, uint32(ssh.HostCert)) + }, + } + }, + "ok": func() test { + va := time.Now().Add(5 * time.Minute) + vb := time.Now().Add(1 * time.Hour) + so := &SSHOptions{CertType: "host", KeyID: "foo", Principals: []string{"foo", "bar"}, + ValidAfter: NewTimeDuration(va), ValidBefore: NewTimeDuration(vb)} + return test{ + so: so, + cert: new(ssh.Certificate), + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.CertType, uint32(ssh.HostCert)) + assert.Equals(t, cert.KeyId, so.KeyID) + assert.Equals(t, cert.ValidPrincipals, so.Principals) + assert.Equals(t, cert.ValidAfter, uint64(so.ValidAfter.RelativeTime(time.Now()).Unix())) + assert.Equals(t, cert.ValidBefore, uint64(so.ValidBefore.RelativeTime(time.Now()).Unix())) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if err := tc.so.Modify(tc.cert); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + tc.valid(tc.cert) + } + } + }) + } +} + +func TestSSHOptions_Match(t *testing.T) { + type test struct { + so SSHOptions + cmp SSHOptions + err error + } + tests := map[string](func() test){ + "fail/cert-type": func() test { + return test{ + so: SSHOptions{CertType: "foo"}, + cmp: SSHOptions{CertType: "bar"}, + err: errors.Errorf("ssh certificate type does not match - got bar, want foo"), + } + }, + "fail/pricipals": func() test { + return test{ + so: SSHOptions{Principals: []string{"foo"}}, + cmp: SSHOptions{Principals: []string{"bar"}}, + err: errors.Errorf("ssh certificate principals does not match - got [bar], want [foo]"), + } + }, + "fail/validAfter": func() test { + return test{ + so: SSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute))}, + cmp: SSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(5 * time.Minute))}, + err: errors.Errorf("ssh certificate valid after does not match"), + } + }, + "fail/validBefore": func() test { + return test{ + so: SSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(1 * time.Minute))}, + cmp: SSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute))}, + err: errors.Errorf("ssh certificate valid before does not match"), + } + }, + "ok/original-empty": func() test { + return test{ + so: SSHOptions{}, + cmp: SSHOptions{ + CertType: "foo", + Principals: []string{"foo"}, + ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute)), + ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute)), + }, + } + }, + "ok/cmp-empty": func() test { + return test{ + cmp: SSHOptions{}, + so: SSHOptions{ + CertType: "foo", + Principals: []string{"foo"}, + ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute)), + ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute)), + }, + } + }, + "ok/equal": func() test { + n := time.Now() + va := NewTimeDuration(n.Add(1 * time.Minute)) + vb := NewTimeDuration(n.Add(5 * time.Minute)) + return test{ + cmp: SSHOptions{ + CertType: "foo", + Principals: []string{"foo"}, + ValidAfter: va, + ValidBefore: vb, + }, + so: SSHOptions{ + CertType: "foo", + Principals: []string{"foo"}, + ValidAfter: va, + ValidBefore: vb, + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if err := tc.so.match(tc.cmp); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func Test_sshCertPrincipalsModifier_Modify(t *testing.T) { + type test struct { + modifier sshCertPrincipalsModifier + cert *ssh.Certificate + expected []string + } + tests := map[string](func() test){ + "ok": func() test { + a := []string{"foo", "bar"} + return test{ + modifier: sshCertPrincipalsModifier(a), + cert: new(ssh.Certificate), + expected: a, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if assert.Nil(t, tc.modifier.Modify(tc.cert)) { + assert.Equals(t, tc.cert.ValidPrincipals, tc.expected) + } + }) + } +} + +func Test_sshCertKeyIDModifier_Modify(t *testing.T) { + type test struct { + modifier sshCertKeyIDModifier + cert *ssh.Certificate + expected string + } + tests := map[string](func() test){ + "ok": func() test { + a := "foo" + return test{ + modifier: sshCertKeyIDModifier(a), + cert: new(ssh.Certificate), + expected: a, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if assert.Nil(t, tc.modifier.Modify(tc.cert)) { + assert.Equals(t, tc.cert.KeyId, tc.expected) + } + }) + } +} + +func Test_sshCertTypeModifier_Modify(t *testing.T) { + type test struct { + modifier sshCertTypeModifier + cert *ssh.Certificate + expected uint32 + } + tests := map[string](func() test){ + "ok/user": func() test { + return test{ + modifier: sshCertTypeModifier("user"), + cert: new(ssh.Certificate), + expected: ssh.UserCert, + } + }, + "ok/host": func() test { + return test{ + modifier: sshCertTypeModifier("host"), + cert: new(ssh.Certificate), + expected: ssh.HostCert, + } + }, + "ok/default": func() test { + return test{ + modifier: sshCertTypeModifier("foo"), + cert: new(ssh.Certificate), + expected: 0, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if assert.Nil(t, tc.modifier.Modify(tc.cert)) { + assert.Equals(t, tc.cert.CertType, uint32(tc.expected)) + } + }) + } +} + +func Test_sshCertValidAfterModifier_Modify(t *testing.T) { + type test struct { + modifier sshCertValidAfterModifier + cert *ssh.Certificate + expected uint64 + } + tests := map[string](func() test){ + "ok": func() test { + return test{ + modifier: sshCertValidAfterModifier(15), + cert: new(ssh.Certificate), + expected: 15, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if assert.Nil(t, tc.modifier.Modify(tc.cert)) { + assert.Equals(t, tc.cert.ValidAfter, tc.expected) + } + }) + } +} + +func Test_sshCertDefaultsModifier_Modify(t *testing.T) { + type test struct { + modifier sshCertDefaultsModifier + cert *ssh.Certificate + valid func(*ssh.Certificate) + } + tests := map[string](func() test){ + "ok/changes": func() test { + n := time.Now() + va := NewTimeDuration(n.Add(1 * time.Minute)) + vb := NewTimeDuration(n.Add(5 * time.Minute)) + so := SSHOptions{ + Principals: []string{"foo", "bar"}, + CertType: "host", + ValidAfter: va, + ValidBefore: vb, + } + return test{ + modifier: sshCertDefaultsModifier(so), + cert: new(ssh.Certificate), + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidPrincipals, so.Principals) + assert.Equals(t, cert.CertType, uint32(ssh.HostCert)) + assert.Equals(t, cert.ValidAfter, uint64(so.ValidAfter.RelativeTime(time.Now()).Unix())) + assert.Equals(t, cert.ValidBefore, uint64(so.ValidBefore.RelativeTime(time.Now()).Unix())) + }, + } + }, + "ok/no-changes": func() test { + n := time.Now() + so := SSHOptions{ + Principals: []string{"foo", "bar"}, + CertType: "host", + ValidAfter: NewTimeDuration(n.Add(15 * time.Minute)), + ValidBefore: NewTimeDuration(n.Add(25 * time.Minute)), + } + return test{ + modifier: sshCertDefaultsModifier(so), + cert: &ssh.Certificate{ + CertType: uint32(ssh.UserCert), + ValidPrincipals: []string{"zap", "zoop"}, + ValidAfter: 15, + ValidBefore: 25, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidPrincipals, []string{"zap", "zoop"}) + assert.Equals(t, cert.CertType, uint32(ssh.UserCert)) + assert.Equals(t, cert.ValidAfter, uint64(15)) + assert.Equals(t, cert.ValidBefore, uint64(25)) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if assert.Nil(t, tc.modifier.Modify(tc.cert)) { + tc.valid(tc.cert) + } + }) + } +} + +func Test_sshDefaultExtensionModifier_Modify(t *testing.T) { + type test struct { + modifier sshDefaultExtensionModifier + cert *ssh.Certificate + valid func(*ssh.Certificate) + err error + } + tests := map[string](func() test){ + "fail/unexpected-cert-type": func() test { + cert := &ssh.Certificate{CertType: 3} + return test{ + modifier: sshDefaultExtensionModifier{}, + cert: cert, + err: errors.New("ssh certificate type has not been set or is invalid"), + } + }, + "ok/host": func() test { + cert := &ssh.Certificate{CertType: ssh.HostCert} + return test{ + modifier: sshDefaultExtensionModifier{}, + cert: cert, + valid: func(cert *ssh.Certificate) { + assert.Len(t, 0, cert.Extensions) + }, + } + }, + "ok/user/extensions-exists": func() test { + cert := &ssh.Certificate{CertType: ssh.UserCert, Permissions: ssh.Permissions{Extensions: map[string]string{ + "foo": "bar", + }}} + return test{ + modifier: sshDefaultExtensionModifier{}, + cert: cert, + valid: func(cert *ssh.Certificate) { + val, ok := cert.Extensions["foo"] + assert.True(t, ok) + assert.Equals(t, val, "bar") + + val, ok = cert.Extensions["permit-X11-forwarding"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-agent-forwarding"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-port-forwarding"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-pty"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-user-rc"] + assert.True(t, ok) + assert.Equals(t, val, "") + }, + } + }, + "ok/user/no-extensions": func() test { + return test{ + modifier: sshDefaultExtensionModifier{}, + cert: &ssh.Certificate{CertType: ssh.UserCert}, + valid: func(cert *ssh.Certificate) { + _, ok := cert.Extensions["foo"] + assert.False(t, ok) + + val, ok := cert.Extensions["permit-X11-forwarding"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-agent-forwarding"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-port-forwarding"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-pty"] + assert.True(t, ok) + assert.Equals(t, val, "") + + val, ok = cert.Extensions["permit-user-rc"] + assert.True(t, ok) + assert.Equals(t, val, "") + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tc := run() + if err := tc.modifier.Modify(tc.cert); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + tc.valid(tc.cert) + } + } + }) + } +} + func Test_sshCertificateDefaultValidator_Valid(t *testing.T) { pub, _, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) @@ -505,7 +956,7 @@ func Test_sshDefaultDuration_Option(t *testing.T) { {"host backdate", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert}}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(30 * 24 * time.Hour)}, false}, {"user validAfter", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(1 * time.Hour)}}, - &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Minute), ValidBefore: unix(17 * time.Hour)}, false}, + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Hour), ValidBefore: unix(17 * time.Hour)}, false}, {"user validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidBefore: unix(1 * time.Hour)}}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(time.Hour)}, false}, {"host validAfter validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}}, diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index 407a7a3a..3c55aada 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -3,11 +3,13 @@ package provisioner import ( "context" "encoding/base64" + "net/http" "strconv" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" "golang.org/x/crypto/ssh" ) @@ -99,33 +101,31 @@ func (p *SSHPOP) Init(config Config) error { // claims for case specific downstream parsing. // e.g. a Sign request will auth/validate different fields than a Revoke request. func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) { - sshCert, err := ExtractSSHPOPCert(token) + sshCert, jwt, err := ExtractSSHPOPCert(token) if err != nil { - return nil, errors.Wrap(err, "authorizeToken ssh-pop") + return nil, errs.Wrap(http.StatusUnauthorized, err, + "sshpop.authorizeToken; error extracting sshpop header from token") } // Check for revocation. if isRevoked, err := p.db.IsSSHRevoked(strconv.FormatUint(sshCert.Serial, 10)); err != nil { - return nil, errors.Wrap(err, "authorizeToken ssh-pop") + return nil, errs.Wrap(http.StatusInternalServerError, err, + "sshpop.authorizeToken; error checking checking sshpop cert revocation") } else if isRevoked { - return nil, errors.New("authorizeToken ssh-pop: ssh certificate has been revoked") + return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate is revoked")) } - jwt, err := jose.ParseSigned(token) - if err != nil { - return nil, errors.Wrapf(err, "error parsing token") - } // Check validity period of the certificate. n := time.Now() if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) { - return nil, errors.New("sshpop certificate validAfter is in the future") + return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate validAfter is in the future")) } if sshCert.ValidBefore != 0 && time.Unix(int64(sshCert.ValidBefore), 0).Before(n) { - return nil, errors.New("sshpop certificate validBefore is in the past") + return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate validBefore is in the past")) } sshCryptoPubKey, ok := sshCert.Key.(ssh.CryptoPublicKey) if !ok { - return nil, errors.New("ssh public key could not be cast to ssh CryptoPublicKey") + return nil, errs.InternalServerError(errors.New("sshpop.authorizeToken; sshpop public key could not be cast to ssh CryptoPublicKey")) } pubKey := sshCryptoPubKey.CryptoPublicKey() @@ -146,7 +146,7 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa } } if !found { - return nil, errors.New("error: provisioner could could not verify the sshpop header certificate") + return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate")) } // Using the ssh certificates key to validate the claims accomplishes two @@ -156,7 +156,7 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa // 2. Asserts that the claims are valid - have not been tampered with. var claims sshPOPPayload if err = jwt.Claims(pubKey, &claims); err != nil { - return nil, errors.Wrap(err, "error parsing claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, "sshpop.authorizeToken; error parsing sshpop token claims") } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -165,16 +165,17 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa Issuer: p.Name, Time: time.Now().UTC(), }, time.Minute); err != nil { - return nil, errors.Wrapf(err, "invalid token") + return nil, errs.Wrap(http.StatusUnauthorized, err, "sshpop.authorizeToken; invalid sshpop token") } // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errors.New("invalid token: invalid audience claim (aud)") + return nil, errs.Unauthorized(errors.Errorf("sshpop.authorizeToken; sshpop token has invalid audience "+ + "claim (aud): expected %s, but got %s", audiences, claims.Audience)) } if claims.Subject == "" { - return nil, errors.New("token subject cannot be empty") + return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop token subject cannot be empty")) } claims.sshCert = sshCert @@ -186,12 +187,13 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error { claims, err := p.authorizeToken(token, p.audiences.SSHRevoke) if err != nil { - return err + return errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRevoke") } if claims.Subject != strconv.FormatUint(claims.sshCert.Serial, 10) { - return errors.New("token subject must be equivalent to certificate serial number") + return errs.BadRequest(errors.New("sshpop.AuthorizeSSHRevoke; sshpop token subject " + + "must be equivalent to sshpop certificate serial number")) } - return err + return nil } // AuthorizeSSHRenew validates the authorization token and extracts/validates @@ -199,10 +201,10 @@ func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error { func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { claims, err := p.authorizeToken(token, p.audiences.SSHRenew) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRenew") } if claims.sshCert.CertType != ssh.HostCert { - return nil, errors.New("sshpop AuthorizeSSHRenew: sshpop certificate must be a host ssh certificate") + return nil, errs.BadRequest(errors.New("sshpop.AuthorizeSSHRenew; sshpop certificate must be a host ssh certificate")) } return claims.sshCert, nil @@ -214,10 +216,10 @@ func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Cert func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { claims, err := p.authorizeToken(token, p.audiences.SSHRekey) if err != nil { - return nil, nil, err + return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRekey") } if claims.sshCert.CertType != ssh.HostCert { - return nil, nil, errors.New("sshpop AuthorizeSSHRekey: sshpop certificate must be a host ssh certificate") + return nil, nil, errs.BadRequest(errors.New("sshpop.AuthorizeSSHRekey; sshpop certificate must be a host ssh certificate")) } return claims.sshCert, []SignOption{ // Validate public key @@ -232,33 +234,34 @@ func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Cert // ExtractSSHPOPCert parses a JWT and extracts and loads the SSH Certificate // in the sshpop header. If the header is missing, an error is returned. -func ExtractSSHPOPCert(token string) (*ssh.Certificate, error) { +func ExtractSSHPOPCert(token string) (*ssh.Certificate, *jose.JSONWebToken, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, nil, errors.Wrapf(err, "extractSSHPOPCert; error parsing token") } encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"] if !ok { - return nil, errors.New("token missing sshpop header") + return nil, nil, errors.New("extractSSHPOPCert; token missing sshpop header") } encodedSSHCertStr, ok := encodedSSHCert.(string) if !ok { - return nil, errors.New("error unexpected type for sshpop header") + return nil, nil, errors.Errorf("extractSSHPOPCert; error unexpected type for sshpop header: "+ + "want 'string', but got '%T'", encodedSSHCert) } sshCertBytes, err := base64.StdEncoding.DecodeString(encodedSSHCertStr) if err != nil { - return nil, errors.Wrap(err, "error decoding sshpop header") + return nil, nil, errors.Wrap(err, "extractSSHPOPCert; error base64 decoding sshpop header") } sshPub, err := ssh.ParsePublicKey(sshCertBytes) if err != nil { - return nil, errors.Wrap(err, "error parsing ssh public key") + return nil, nil, errors.Wrap(err, "extractSSHPOPCert; error parsing ssh public key") } sshCert, ok := sshPub.(*ssh.Certificate) if !ok { - return nil, errors.New("error converting ssh public key to ssh certificate") + return nil, nil, errors.New("extractSSHPOPCert; error converting ssh public key to ssh certificate") } - return sshCert, nil + return sshCert, jwt, nil } func bytesForSigning(cert *ssh.Certificate) []byte { diff --git a/authority/provisioner/sshpop_test.go b/authority/provisioner/sshpop_test.go new file mode 100644 index 00000000..32f58879 --- /dev/null +++ b/authority/provisioner/sshpop_test.go @@ -0,0 +1,684 @@ +package provisioner + +import ( + "context" + "crypto" + "crypto/rand" + "encoding/base64" + "net/http" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/jose" + "golang.org/x/crypto/ssh" +) + +func TestSSHPOP_Getters(t *testing.T) { + p, err := generateSSHPOP() + assert.FatalError(t, err) + id := "sshpop/" + p.Name + if got := p.GetID(); got != id { + t.Errorf("SSHPOP.GetID() = %v, want %v", got, id) + } + if got := p.GetName(); got != p.Name { + t.Errorf("SSHPOP.GetName() = %v, want %v", got, p.Name) + } + if got := p.GetType(); got != TypeSSHPOP { + t.Errorf("SSHPOP.GetType() = %v, want %v", got, TypeSSHPOP) + } + kid, key, ok := p.GetEncryptedKey() + if kid != "" || key != "" || ok == true { + t.Errorf("SSHPOP.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)", + kid, key, ok, "", "", false) + } +} + +func createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, *jose.JSONWebKey, error) { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "foo", 0) + if err != nil { + return nil, nil, err + } + cert.Key, err = ssh.NewPublicKey(jwk.Public().Key) + if err != nil { + return nil, nil, err + } + if err = cert.SignCert(rand.Reader, signer); err != nil { + return nil, nil, err + } + return cert, jwk, nil +} + +func generateSSHPOPToken(p Interface, cert *ssh.Certificate, jwk *jose.JSONWebKey) (string, error) { + return generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) +} + +func TestSSHPOP_authorizeToken(t *testing.T) { + key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + type test struct { + p *SSHPOP + token string + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "), + } + }, + "fail/error-revoked-db-check": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, errors.New("force") + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusInternalServerError, + err: errors.New("sshpop.authorizeToken; error checking checking sshpop cert revocation: force"), + } + }, + "fail/cert-already-revoked": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return true, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; sshpop certificate is revoked"), + } + }, + "fail/cert-not-yet-valid": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{ + CertType: ssh.UserCert, + ValidAfter: uint64(time.Now().Add(time.Minute).Unix()), + }, sshSigner) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; sshpop certificate validAfter is in the future"), + } + }, + "fail/cert-past-validity": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{ + CertType: ssh.UserCert, + ValidBefore: uint64(time.Now().Add(-time.Minute).Unix()), + }, sshSigner) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; sshpop certificate validBefore is in the past"), + } + }, + "fail/no-signer-found": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate"), + } + }, + "fail/error-parsing-claims-bad-sig": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, _, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + otherJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, otherJWK) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; error parsing sshpop token claims"), + } + }, + "fail/invalid-claims-issuer": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateToken("foo", "bar", testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; invalid sshpop token"), + } + }, + "fail/invalid-audience": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), "invalid-aud", "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; sshpop token has invalid audience claim (aud)"), + } + }, + "fail/empty-subject": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("sshpop.authorizeToken; sshpop token subject cannot be empty"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateSSHPOPToken(p, cert, jwk) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.NotNil(t, claims) + } + } + }) + } +} + +func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) { + key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key") + assert.FatalError(t, err) + signer, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer") + sshSigner, err := ssh.NewSignerFromSigner(signer) + assert.FatalError(t, err) + + type test struct { + p *SSHPOP + token string + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("sshpop.AuthorizeSSHRevoke: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "), + } + }, + "fail/subject-not-equal-serial": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRevoke[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusBadRequest, + err: errors.New("sshpop.AuthorizeSSHRevoke; sshpop token subject must be equivalent to sshpop certificate serial number"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.UserCert}, sshSigner) + assert.FatalError(t, err) + tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRevoke[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) { + key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key") + assert.FatalError(t, err) + userSigner, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh user signing key to crypto signer") + sshUserSigner, err := ssh.NewSignerFromSigner(userSigner) + assert.FatalError(t, err) + + hostKey, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + hostSigner, ok := hostKey.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh host signing key to crypto signer") + sshHostSigner, err := ssh.NewSignerFromSigner(hostSigner) + assert.FatalError(t, err) + + type test struct { + p *SSHPOP + token string + cert *ssh.Certificate + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("sshpop.AuthorizeSSHRenew: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "), + } + }, + "fail/not-host-cert": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusBadRequest, + err: errors.New("sshpop.AuthorizeSSHRenew; sshpop certificate must be a host ssh certificate"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner) + assert.FatalError(t, err) + tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRenew[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + cert: cert, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if cert, err := tc.p.AuthorizeSSHRenew(context.Background(), tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, tc.cert.Nonce, cert.Nonce) + } + } + }) + } +} + +func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) { + key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key") + assert.FatalError(t, err) + userSigner, ok := key.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh user signing key to crypto signer") + sshUserSigner, err := ssh.NewSignerFromSigner(userSigner) + assert.FatalError(t, err) + + hostKey, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + hostSigner, ok := hostKey.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh host signing key to crypto signer") + sshHostSigner, err := ssh.NewSignerFromSigner(hostSigner) + assert.FatalError(t, err) + + type test struct { + p *SSHPOP + token string + cert *ssh.Certificate + err error + code int + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("sshpop.AuthorizeSSHRekey: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "), + } + }, + "fail/not-host-cert": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + code: http.StatusBadRequest, + err: errors.New("sshpop.AuthorizeSSHRekey; sshpop certificate must be a host ssh certificate"), + } + }, + "ok": func(t *testing.T) test { + p, err := generateSSHPOP() + assert.FatalError(t, err) + p.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } + cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner) + assert.FatalError(t, err) + tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + cert: cert, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if cert, opts, err := tc.p.AuthorizeSSHRekey(context.Background(), tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Len(t, 3, opts) + for _, o := range opts { + switch v := o.(type) { + case *sshDefaultPublicKeyValidator: + case *sshCertificateDefaultValidator: + case *sshCertificateValidityValidator: + assert.Equals(t, v.Claimer, tc.p.claimer) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + } + assert.Equals(t, tc.cert.Nonce, cert.Nonce) + } + } + }) + } +} + +func TestSSHPOP_ExtractSSHPOPCert(t *testing.T) { + hostKey, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key") + assert.FatalError(t, err) + hostSigner, ok := hostKey.(crypto.Signer) + assert.Fatal(t, ok, "could not cast ssh host signing key to crypto signer") + sshHostSigner, err := ssh.NewSignerFromSigner(hostSigner) + assert.FatalError(t, err) + + type test struct { + token string + cert *ssh.Certificate + jwk *jose.JSONWebKey + err error + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + return test{ + token: "foo", + err: errors.New("extractSSHPOPCert; error parsing token"), + } + }, + "fail/sshpop-missing": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateToken("sub", "sshpop-provisioner", testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk) + assert.FatalError(t, err) + return test{ + token: tok, + err: errors.New("extractSSHPOPCert; token missing sshpop header"), + } + }, + "fail/wrong-sshpop-type": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, func(so *jose.SignerOptions) error { + so.WithHeader("sshpop", 12345) + return nil + }) + assert.FatalError(t, err) + return test{ + token: tok, + err: errors.New("extractSSHPOPCert; error unexpected type for sshpop header: "), + } + }, + "fail/base64decode-error": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, func(so *jose.SignerOptions) error { + so.WithHeader("sshpop", "!@#$%^&*") + return nil + }) + assert.FatalError(t, err) + return test{ + token: tok, + err: errors.New("extractSSHPOPCert; error base64 decoding sshpop header: illegal base64"), + } + }, + "fail/parsing-sshpop-pubkey": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, func(so *jose.SignerOptions) error { + so.WithHeader("sshpop", base64.StdEncoding.EncodeToString([]byte("foo"))) + return nil + }) + assert.FatalError(t, err) + return test{ + token: tok, + err: errors.New("extractSSHPOPCert; error parsing ssh public key"), + } + }, + "ok": func(t *testing.T) test { + cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner) + + assert.FatalError(t, err) + tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert)) + assert.FatalError(t, err) + return test{ + token: tok, + jwk: jwk, + cert: cert, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if cert, jwt, err := ExtractSSHPOPCert(tc.token); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, tc.cert.Nonce, cert.Nonce) + assert.Equals(t, tc.jwk.KeyID, jwt.Headers[0].KeyID) + } + } + }) + } +} diff --git a/authority/provisioner/testdata/bar.pub b/authority/provisioner/testdata/certs/bar.pub similarity index 100% rename from authority/provisioner/testdata/bar.pub rename to authority/provisioner/testdata/certs/bar.pub diff --git a/authority/provisioner/testdata/ecdsa.csr b/authority/provisioner/testdata/certs/ecdsa.csr similarity index 100% rename from authority/provisioner/testdata/ecdsa.csr rename to authority/provisioner/testdata/certs/ecdsa.csr diff --git a/authority/provisioner/testdata/ed25519.csr b/authority/provisioner/testdata/certs/ed25519.csr similarity index 100% rename from authority/provisioner/testdata/ed25519.csr rename to authority/provisioner/testdata/certs/ed25519.csr diff --git a/authority/provisioner/testdata/foo.pub b/authority/provisioner/testdata/certs/foo.pub similarity index 100% rename from authority/provisioner/testdata/foo.pub rename to authority/provisioner/testdata/certs/foo.pub diff --git a/authority/provisioner/testdata/root_ca.crt b/authority/provisioner/testdata/certs/root_ca.crt similarity index 100% rename from authority/provisioner/testdata/root_ca.crt rename to authority/provisioner/testdata/certs/root_ca.crt diff --git a/authority/provisioner/testdata/rsa.csr b/authority/provisioner/testdata/certs/rsa.csr similarity index 100% rename from authority/provisioner/testdata/rsa.csr rename to authority/provisioner/testdata/certs/rsa.csr diff --git a/authority/provisioner/testdata/short-rsa.csr b/authority/provisioner/testdata/certs/short-rsa.csr similarity index 100% rename from authority/provisioner/testdata/short-rsa.csr rename to authority/provisioner/testdata/certs/short-rsa.csr diff --git a/authority/provisioner/testdata/certs/ssh_host_ca_key.pub b/authority/provisioner/testdata/certs/ssh_host_ca_key.pub new file mode 100644 index 00000000..aa5685da --- /dev/null +++ b/authority/provisioner/testdata/certs/ssh_host_ca_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY= diff --git a/authority/provisioner/testdata/certs/ssh_user_ca_key.pub b/authority/provisioner/testdata/certs/ssh_user_ca_key.pub new file mode 100644 index 00000000..5909ce43 --- /dev/null +++ b/authority/provisioner/testdata/certs/ssh_user_ca_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y= diff --git a/authority/provisioner/testdata/x5c-leaf.crt b/authority/provisioner/testdata/certs/x5c-leaf.crt similarity index 100% rename from authority/provisioner/testdata/x5c-leaf.crt rename to authority/provisioner/testdata/certs/x5c-leaf.crt diff --git a/authority/provisioner/testdata/bar.priv b/authority/provisioner/testdata/secrets/bar.priv similarity index 100% rename from authority/provisioner/testdata/bar.priv rename to authority/provisioner/testdata/secrets/bar.priv diff --git a/authority/provisioner/testdata/secrets/bar_host_ssh_key b/authority/provisioner/testdata/secrets/bar_host_ssh_key new file mode 100644 index 00000000..7662c1a6 --- /dev/null +++ b/authority/provisioner/testdata/secrets/bar_host_ssh_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHzAUYu3h8e1gL5ONGZo+lghJJa9rl1TvP2UlqDXazxvoAoGCCqGSM49 +AwEHoUQDQgAEOLScS+1Yzmqdyots9lSC0tzTSXUXEgyOD9wYrQ0BqnVZtBXlQw1p +m3fnF/7Ehl6bD1YZWjrF1t+IBZQMq1uBBw== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/testdata/ecdsa.key b/authority/provisioner/testdata/secrets/ecdsa.key similarity index 100% rename from authority/provisioner/testdata/ecdsa.key rename to authority/provisioner/testdata/secrets/ecdsa.key diff --git a/authority/provisioner/testdata/ed25519.key b/authority/provisioner/testdata/secrets/ed25519.key similarity index 100% rename from authority/provisioner/testdata/ed25519.key rename to authority/provisioner/testdata/secrets/ed25519.key diff --git a/authority/provisioner/testdata/foo.priv b/authority/provisioner/testdata/secrets/foo.priv similarity index 100% rename from authority/provisioner/testdata/foo.priv rename to authority/provisioner/testdata/secrets/foo.priv diff --git a/authority/provisioner/testdata/secrets/foo_user_ssh_key b/authority/provisioner/testdata/secrets/foo_user_ssh_key new file mode 100644 index 00000000..8bda30c6 --- /dev/null +++ b/authority/provisioner/testdata/secrets/foo_user_ssh_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINWGD2xneE43YeytQzORItISxv6d/oH+9TXvDKHo6TyXoAoGCCqGSM49 +AwEHoUQDQgAEVK/EtXgVV7+7ppnQSjCtI5qb/gIGnQUF4i//F/JKKho7kRNyMDSn +BP3kndiv8Yfxg4PsyIRY5ZofbEo5eJE6bg== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/testdata/rsa.key b/authority/provisioner/testdata/secrets/rsa.key similarity index 100% rename from authority/provisioner/testdata/rsa.key rename to authority/provisioner/testdata/secrets/rsa.key diff --git a/authority/provisioner/testdata/secrets/ssh_host_ca_key b/authority/provisioner/testdata/secrets/ssh_host_ca_key new file mode 100644 index 00000000..7a7e4c44 --- /dev/null +++ b/authority/provisioner/testdata/secrets/ssh_host_ca_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49 +AwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+ +MrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/testdata/secrets/ssh_user_ca_key b/authority/provisioner/testdata/secrets/ssh_user_ca_key new file mode 100644 index 00000000..92d35ec2 --- /dev/null +++ b/authority/provisioner/testdata/secrets/ssh_user_ca_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49 +AwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h +YpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/testdata/x5c-leaf.key b/authority/provisioner/testdata/secrets/x5c-leaf.key similarity index 100% rename from authority/provisioner/testdata/x5c-leaf.key rename to authority/provisioner/testdata/secrets/x5c-leaf.key diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 76c9a567..7d200d33 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -19,6 +19,7 @@ import ( "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/jose" + "golang.org/x/crypto/ssh" ) var ( @@ -47,24 +48,6 @@ var ( } ) -func provisionerClaims() *Claims { - ddr := false - des := true - return &Claims{ - MinTLSDur: &Duration{5 * time.Minute}, - MaxTLSDur: &Duration{24 * time.Hour}, - DefaultTLSDur: &Duration{24 * time.Hour}, - DisableRenewal: &ddr, - MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs - MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, - MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs - MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - EnableSSHCA: &des, - } -} - const awsTestCertificate = `-----BEGIN CERTIFICATE----- MIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw GDEWMBQGA1UEAxMNQVdTIFRlc3QgQ2VydDAeFw0xOTA0MjQyMjU3MzlaFw0yOTA0 @@ -204,7 +187,7 @@ func generateJWK() (*JWK, error) { } func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) { - fooPubB, err := ioutil.ReadFile("./testdata/foo.pub") + fooPubB, err := ioutil.ReadFile("./testdata/certs/foo.pub") if err != nil { return nil, err } @@ -212,7 +195,7 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) { if err != nil { return nil, err } - barPubB, err := ioutil.ReadFile("./testdata/bar.pub") + barPubB, err := ioutil.ReadFile("./testdata/certs/bar.pub") if err != nil { return nil, err } @@ -240,6 +223,46 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) { }, nil } +func generateSSHPOP() (*SSHPOP, error) { + name, err := randutil.Alphanumeric(10) + if err != nil { + return nil, err + } + claimer, err := NewClaimer(nil, globalProvisionerClaims) + if err != nil { + return nil, err + } + + userB, err := ioutil.ReadFile("./testdata/certs/ssh_user_ca_key.pub") + if err != nil { + return nil, err + } + userKey, _, _, _, err := ssh.ParseAuthorizedKey(userB) + if err != nil { + return nil, err + } + hostB, err := ioutil.ReadFile("./testdata/certs/ssh_host_ca_key.pub") + if err != nil { + return nil, err + } + hostKey, _, _, _, err := ssh.ParseAuthorizedKey(hostB) + if err != nil { + return nil, err + } + + return &SSHPOP{ + Name: name, + Type: "SSHPOP", + Claims: &globalProvisionerClaims, + audiences: testAudiences, + claimer: claimer, + sshPubKeys: &SSHKeys{ + UserKeys: []ssh.PublicKey{userKey}, + HostKeys: []ssh.PublicKey{hostKey}, + }, + }, nil +} + func generateX5C(root []byte) (*X5C, error) { if root == nil { root = []byte(`-----BEGIN CERTIFICATE----- @@ -589,6 +612,13 @@ func withX5CHdr(certs []*x509.Certificate) tokOption { } } +func withSSHPOPFile(cert *ssh.Certificate) tokOption { + return func(so *jose.SignerOptions) error { + so.WithHeader("sshpop", base64.StdEncoding.EncodeToString(cert.Marshal())) + return nil + } +} + func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { so := new(jose.SignerOptions) so.WithType("JWT") @@ -630,6 +660,24 @@ func generateToken(sub, iss, aud string, email string, sans []string, iat time.T return jose.Signed(sig).Claims(claims).CompactSerialize() } +func generateX5CSSHToken(jwk *jose.JSONWebKey, claims *x5cPayload, tokOpts ...tokOption) (string, error) { + so := new(jose.SignerOptions) + so.WithType("JWT") + so.WithHeader("kid", jwk.KeyID) + + for _, o := range tokOpts { + if err := o(so); err != nil { + return "", err + } + } + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so) + if err != nil { + return "", err + } + return jose.Signed(sig).Claims(claims).CompactSerialize() +} + func getK8sSAPayload() *k8sSAPayload { return &k8sSAPayload{ Claims: jose.Claims{ diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 1be728db..692cd963 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -4,9 +4,11 @@ import ( "context" "crypto/x509" "encoding/pem" + "net/http" "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/jose" ) @@ -121,19 +123,20 @@ func (p *X5C) Init(config Config) error { func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, error) { jwt, err := jose.ParseSigned(token) if err != nil { - return nil, errors.Wrapf(err, "error parsing token") + return nil, errs.Wrap(http.StatusUnauthorized, err, "x5c.authorizeToken; error parsing x5c token") } verifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{ Roots: p.rootPool, }) if err != nil { - return nil, errors.Wrap(err, "error verifying x5c certificate chain") + return nil, errs.Wrap(http.StatusUnauthorized, err, + "x5c.authorizeToken; error verifying x5c certificate chain in token") } leaf := verifiedChains[0][0] if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 { - return nil, errors.New("certificate used to sign x5c token cannot be used for digital signature") + return nil, errs.Unauthorized(errors.New("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature")) } // Using the leaf certificates key to validate the claims accomplishes two @@ -143,7 +146,7 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err // 2. Asserts that the claims are valid - have not been tampered with. var claims x5cPayload if err = jwt.Claims(leaf.PublicKey, &claims); err != nil { - return nil, errors.Wrap(err, "error parsing claims") + return nil, errs.Wrap(http.StatusUnauthorized, err, "x5c.authorizeToken; error parsing x5c claims") } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -152,16 +155,17 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err Issuer: p.Name, Time: time.Now().UTC(), }, time.Minute); err != nil { - return nil, errors.Wrapf(err, "invalid token") + return nil, errs.Wrapf(http.StatusUnauthorized, err, "x5c.authorizeToken; invalid x5c claims") } // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errors.New("invalid token: invalid audience claim (aud)") + return nil, errs.Unauthorized(errors.Errorf("x5c.authorizeToken; x5c token has invalid audience "+ + "claim (aud); expected %s, but got %s", audiences, claims.Audience)) } if claims.Subject == "" { - return nil, errors.New("token subject cannot be empty") + return nil, errs.Unauthorized(errors.New("x5c.authorizeToken; x5c token subject cannot be empty")) } // Save the verified chains on the x5c payload object. @@ -173,14 +177,14 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err // revoke the certificate with serial number in the `sub` property. func (p *X5C) AuthorizeRevoke(ctx context.Context, token string) error { _, err := p.authorizeToken(token, p.audiences.Revoke) - return err + return errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeRevoke") } // AuthorizeSign validates the given token. func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { claims, err := p.authorizeToken(token, p.audiences.Sign) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSign") } // NOTE: This is for backwards compatibility with older versions of cli @@ -209,7 +213,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // AuthorizeRenew returns an error if the renewal is disabled. func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + return errs.Unauthorized(errors.Errorf("x5c.AuthorizeRenew; renew is disabled for x5c provisioner %s", p.GetID())) } return nil } @@ -217,16 +221,16 @@ func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + return nil, errs.Unauthorized(errors.Errorf("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner %s", p.GetID())) } claims, err := p.authorizeToken(token, p.audiences.SSHSign) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSSHSign") } if claims.Step == nil || claims.Step.SSH == nil { - return nil, errors.New("authorization token must be an SSH provisioning token") + return nil, errs.Unauthorized(errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token")) } opts := claims.Step.SSH @@ -245,18 +249,18 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, } t := now() if !opts.ValidAfter.IsZero() { - signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) + signOptions = append(signOptions, sshCertValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) } if !opts.ValidBefore.IsZero() { - signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) + signOptions = append(signOptions, sshCertValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) } // Make sure to define the the KeyID if opts.KeyID == "" { - signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject)) + signOptions = append(signOptions, sshCertKeyIDModifier(claims.Subject)) } // Default to a user certificate with no principals if not set - signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) + signOptions = append(signOptions, sshCertDefaultsModifier{CertType: SSHUserCert}) return append(signOptions, // Set the default extensions. diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 65147d24..775f3202 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -2,14 +2,16 @@ package provisioner import ( "context" - "crypto/x509" "net" + "net/http" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/jose" ) @@ -151,9 +153,15 @@ M46l92gdOozT } func TestX5C_authorizeToken(t *testing.T) { + x5cCerts, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt") + assert.FatalError(t, err) + x5cJWK, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key") + assert.FatalError(t, err) + type test struct { p *X5C token string + code int err error } tests := map[string]func(*testing.T) test{ @@ -163,7 +171,8 @@ func TestX5C_authorizeToken(t *testing.T) { return test{ p: p, token: "foo", - err: errors.New("error parsing token"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; error parsing x5c token"), } }, "fail/invalid-cert-chain": func(t *testing.T) test { @@ -190,7 +199,8 @@ a5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo= return test{ p: p, token: tok, - err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; error verifying x5c certificate chain in token"), } }, "fail/doubled-up-self-signed-cert": func(t *testing.T) test { @@ -228,7 +238,8 @@ EXAHTA9L return test{ p: p, token: tok, - err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; error verifying x5c certificate chain in token"), } }, "fail/digital-signature-ext-required": func(t *testing.T) test { @@ -269,7 +280,8 @@ lgsqsR63is+0YQ== return test{ p: p, token: tok, - err: errors.New("certificate used to sign x5c token cannot be used for digital signature"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature"), } }, "fail/signature-does-not-match-x5c-pub-key": func(t *testing.T) test { @@ -309,74 +321,58 @@ lgsqsR63is+0YQ== return test{ p: p, token: tok, - err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; error parsing x5c claims"), } }, "fail/invalid-issuer": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - p, err := generateX5C(nil) assert.FatalError(t, err) tok, err := generateToken("", "foobar", testAudiences.Sign[0], "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) + []string{"test.smallstep.com"}, time.Now(), x5cJWK, + withX5CHdr(x5cCerts)) assert.FatalError(t, err) return test{ p: p, token: tok, - err: errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; invalid x5c claims"), } }, "fail/invalid-audience": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - p, err := generateX5C(nil) assert.FatalError(t, err) tok, err := generateToken("", p.GetName(), "foobar", "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) + []string{"test.smallstep.com"}, time.Now(), x5cJWK, + withX5CHdr(x5cCerts)) assert.FatalError(t, err) return test{ p: p, token: tok, - err: errors.New("invalid token: invalid audience claim (aud)"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; x5c token has invalid audience claim (aud)"), } }, "fail/empty-subject": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - p, err := generateX5C(nil) assert.FatalError(t, err) tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) + []string{"test.smallstep.com"}, time.Now(), x5cJWK, + withX5CHdr(x5cCerts)) assert.FatalError(t, err) return test{ p: p, token: tok, - err: errors.New("token subject cannot be empty"), + code: http.StatusUnauthorized, + err: errors.New("x5c.authorizeToken; x5c token subject cannot be empty"), } }, "ok": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - p, err := generateX5C(nil) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) + []string{"test.smallstep.com"}, time.Now(), x5cJWK, + withX5CHdr(x5cCerts)) assert.FatalError(t, err) return test{ p: p, @@ -389,6 +385,9 @@ lgsqsR63is+0YQ== tc := tt(t) if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -402,10 +401,15 @@ lgsqsR63is+0YQ== } func TestX5C_AuthorizeSign(t *testing.T) { + certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key") + assert.FatalError(t, err) + type test struct { p *X5C token string - ctx context.Context + code int err error dns []string emails []string @@ -418,56 +422,11 @@ func TestX5C_AuthorizeSign(t *testing.T) { return test{ p: p, token: "foo", - ctx: NewContextWithMethod(context.Background(), SignMethod), - err: errors.New("error parsing token"), - } - }, - "fail/ssh/disabled": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - - p, err := generateX5C(nil) - assert.FatalError(t, err) - p.claimer.claims = provisionerClaims() - *p.claimer.claims.EnableSSHCA = false - tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) - assert.FatalError(t, err) - return test{ - p: p, - ctx: NewContextWithMethod(context.Background(), SignSSHMethod), - token: tok, - err: errors.Errorf("ssh ca is disabled for provisioner x5c/%s", p.GetName()), - } - }, - "fail/ssh/invalid-token": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - - p, err := generateX5C(nil) - assert.FatalError(t, err) - tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) - assert.FatalError(t, err) - return test{ - p: p, - ctx: NewContextWithMethod(context.Background(), SignSSHMethod), - token: tok, - err: errors.New("authorization token must be an SSH provisioning token"), + code: http.StatusUnauthorized, + err: errors.New("x5c.AuthorizeSign: x5c.authorizeToken; error parsing x5c token"), } }, "ok/empty-sans": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - p, err := generateX5C(nil) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", @@ -476,7 +435,6 @@ func TestX5C_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) return test{ p: p, - ctx: NewContextWithMethod(context.Background(), SignMethod), token: tok, dns: []string{"foo"}, emails: []string{}, @@ -484,11 +442,6 @@ func TestX5C_AuthorizeSign(t *testing.T) { } }, "ok/multi-sans": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - p, err := generateX5C(nil) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", @@ -497,7 +450,6 @@ func TestX5C_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) return test{ p: p, - ctx: NewContextWithMethod(context.Background(), SignMethod), token: tok, dns: []string{"foo"}, emails: []string{"max@smallstep.com"}, @@ -508,8 +460,11 @@ func TestX5C_AuthorizeSign(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil { + if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -554,65 +509,245 @@ func TestX5C_AuthorizeSign(t *testing.T) { } } +func TestX5C_AuthorizeRevoke(t *testing.T) { + type test struct { + p *X5C + token string + code int + err error + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("x5c.AuthorizeRevoke: x5c.authorizeToken; error parsing x5c token"), + } + }, + "ok": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Revoke[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestX5C_AuthorizeRenew(t *testing.T) { + type test struct { + p *X5C + code int + err error + } + tests := map[string]func(*testing.T) test{ + "fail/renew-disabled": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + // disable renewal + disable := true + p.Claims = &Claims{DisableRenewal: &disable} + p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + return test{ + p: p, + code: http.StatusUnauthorized, + err: errors.Errorf("x5c.AuthorizeRenew; renew is disabled for x5c provisioner %s", p.GetID()), + } + }, + "ok": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRenew(context.TODO(), nil); err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + func TestX5C_AuthorizeSSHSign(t *testing.T) { + x5cCerts, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt") + assert.FatalError(t, err) + x5cJWK, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key") + assert.FatalError(t, err) + _, fn := mockNow() defer fn() type test struct { p *X5C token string claims *x5cPayload + code int err error } tests := map[string]func(*testing.T) test{ + "fail/sshCA-disabled": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + // disable sshCA + enable := false + p.Claims = &Claims{EnableSSHCA: &enable} + p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.Errorf("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner %s", p.GetID()), + } + }, + "fail/invalid-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + code: http.StatusUnauthorized, + err: errors.New("x5c.AuthorizeSSHSign: x5c.authorizeToken; error parsing x5c token"), + } + }, "fail/no-Step-claim": func(t *testing.T) test { p, err := generateX5C(nil) assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.SSHSign[0], "", + []string{"test.smallstep.com"}, time.Now(), x5cJWK, + withX5CHdr(x5cCerts)) + assert.FatalError(t, err) return test{ - p: p, - claims: new(x5cPayload), - err: errors.New("authorization token must be an SSH provisioning token"), + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"), } }, "fail/no-SSH-subattribute-in-claims": func(t *testing.T) test { p, err := generateX5C(nil) assert.FatalError(t, err) + + id, err := randutil.ASCII(64) + assert.FatalError(t, err) + now := time.Now() + claims := &x5cPayload{ + Claims: jose.Claims{ + ID: id, + Subject: "foo", + Issuer: p.GetName(), + IssuedAt: jose.NewNumericDate(now), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), + Audience: []string{testAudiences.SSHSign[0]}, + }, + Step: &stepPayload{}, + } + tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts)) + assert.FatalError(t, err) return test{ - p: p, - claims: &x5cPayload{Step: new(stepPayload)}, - err: errors.New("authorization token must be an SSH provisioning token"), + p: p, + token: tok, + code: http.StatusUnauthorized, + err: errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"), } }, "ok/with-claims": func(t *testing.T) test { p, err := generateX5C(nil) assert.FatalError(t, err) - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + + id, err := randutil.ASCII(64) + assert.FatalError(t, err) + now := time.Now() + claims := &x5cPayload{ + Claims: jose.Claims{ + ID: id, + Subject: "foo", + Issuer: p.GetName(), + IssuedAt: jose.NewNumericDate(now), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), + Audience: []string{testAudiences.SSHSign[0]}, + }, + Step: &stepPayload{SSH: &SSHOptions{ + CertType: SSHHostCert, + Principals: []string{"max", "mariano", "alan"}, + ValidAfter: TimeDuration{d: 5 * time.Minute}, + ValidBefore: TimeDuration{d: 10 * time.Minute}, + }}, + } + tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts)) assert.FatalError(t, err) return test{ - p: p, - claims: &x5cPayload{ - Step: &stepPayload{SSH: &SSHOptions{ - CertType: SSHHostCert, - Principals: []string{"max", "mariano", "alan"}, - ValidAfter: TimeDuration{d: 5 * time.Minute}, - ValidBefore: TimeDuration{d: 10 * time.Minute}, - }}, - Claims: jose.Claims{Subject: "foo"}, - chains: [][]*x509.Certificate{certs}, - }, + p: p, + claims: claims, + token: tok, } }, "ok/without-claims": func(t *testing.T) test { p, err := generateX5C(nil) assert.FatalError(t, err) - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + + id, err := randutil.ASCII(64) + assert.FatalError(t, err) + now := time.Now() + claims := &x5cPayload{ + Claims: jose.Claims{ + ID: id, + Subject: "foo", + Issuer: p.GetName(), + IssuedAt: jose.NewNumericDate(now), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)), + Audience: []string{testAudiences.SSHSign[0]}, + }, + Step: &stepPayload{SSH: &SSHOptions{}}, + } + tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts)) assert.FatalError(t, err) return test{ - p: p, - claims: &x5cPayload{ - Step: &stepPayload{SSH: &SSHOptions{}}, - Claims: jose.Claims{Subject: "foo"}, - chains: [][]*x509.Certificate{certs}, - }, + p: p, + claims: claims, + token: tok, } }, } @@ -621,6 +756,9 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { tc := tt(t) if opts, err := tc.p.AuthorizeSSHSign(context.TODO(), tc.token); err != nil { if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -634,34 +772,36 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { tc.claims.Step.SSH.ValidAfter.t = time.Time{} tc.claims.Step.SSH.ValidBefore.t = time.Time{} assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH) - case sshCertificateKeyIDModifier: + case sshCertKeyIDModifier: assert.Equals(t, string(v), "foo") case sshCertTypeModifier: assert.Equals(t, string(v), tc.claims.Step.SSH.CertType) case sshCertPrincipalsModifier: assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals) - case sshCertificateValidAfterModifier: + case sshCertValidAfterModifier: assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix()) - case sshCertificateValidBeforeModifier: + case sshCertValidBeforeModifier: assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix()) - case sshCertificateDefaultsModifier: + case sshCertDefaultsModifier: assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert}) case *sshLimitDuration: assert.Equals(t, v.Claimer, tc.p.claimer) - assert.Equals(t, v.NotAfter, tc.claims.chains[0][0].NotAfter) + assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter) case *sshCertificateValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator, *sshCertificateDefaultValidator: + case sshCertKeyIDValidator: + assert.Equals(t, string(v), "foo") default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) } tot++ } if len(tc.claims.Step.SSH.CertType) > 0 { - assert.Equals(t, tot, 12) + assert.Equals(t, tot, 13) } else { - assert.Equals(t, tot, 8) + assert.Equals(t, tot, 9) } } } @@ -669,84 +809,3 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { }) } } - -func TestX5C_AuthorizeRevoke(t *testing.T) { - type test struct { - p *X5C - token string - err error - } - tests := map[string]func(*testing.T) test{ - "fail/invalid-token": func(t *testing.T) test { - p, err := generateX5C(nil) - assert.FatalError(t, err) - return test{ - p: p, - token: "foo", - err: errors.New("error parsing token"), - } - }, - "ok": func(t *testing.T) test { - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") - assert.FatalError(t, err) - - p, err := generateX5C(nil) - assert.FatalError(t, err) - tok, err := generateToken("foo", p.GetName(), testAudiences.Revoke[0], "", - []string{"test.smallstep.com"}, time.Now(), jwk, - withX5CHdr(certs)) - assert.FatalError(t, err) - return test{ - p: p, - token: tok, - } - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - tc := tt(t) - if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil { - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } else { - assert.Nil(t, tc.err) - } - }) - } -} - -func TestX5C_AuthorizeRenew(t *testing.T) { - p1, err := generateX5C(nil) - assert.FatalError(t, err) - p2, err := generateX5C(nil) - assert.FatalError(t, err) - - // disable renewal - disable := true - p2.Claims = &Claims{DisableRenewal: &disable} - p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) - assert.FatalError(t, err) - - type args struct { - cert *x509.Certificate - } - tests := []struct { - name string - prov *X5C - args args - wantErr bool - }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { - t.Errorf("X5C.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/authority/ssh.go b/authority/ssh.go index cfd5ed37..5d80a427 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -122,10 +122,7 @@ func (a *Authority) GetSSHFederation() (*SSHKeys, error) { // GetSSHConfig returns rendered templates for clients (user) or servers (host). func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) { if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil { - return nil, &apiError{ - err: errors.New("getSSHConfig: ssh is not configured"), - code: http.StatusNotFound, - } + return nil, errs.NotFound(errors.New("getSSHConfig: ssh is not configured")) } var ts []templates.Template @@ -139,10 +136,7 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template ts = a.config.Templates.SSH.Host } default: - return nil, &apiError{ - err: errors.Errorf("getSSHConfig: type %s is not valid", typ), - code: http.StatusBadRequest, - } + return nil, errs.BadRequest(errors.Errorf("getSSHConfig: type %s is not valid", typ)) } // Merge user and default data @@ -174,7 +168,8 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template // hostname. func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error) { if a.sshBastionFunc != nil { - return a.sshBastionFunc(user, hostname) + bs, err := a.sshBastionFunc(user, hostname) + return bs, errs.Wrap(http.StatusInternalServerError, err, "authority.GetSSHBastion") } if a.config.SSH != nil { if a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != "" { @@ -182,26 +177,7 @@ func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error } return nil, nil } - return nil, &apiError{ - err: errors.New("getSSHBastion: ssh is not configured"), - code: http.StatusNotFound, - } -} - -// authorizeSSHSign loads the provisioner from the token, checks that it has not -// been used again and calls the provisioner AuthorizeSSHSign method. Returns a -// list of methods to apply to the signing flow. -func (a *Authority) authorizeSSHSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - var errContext = apiCtx{"ott": ott} - p, err := a.authorizeToken(ctx, ott) - if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext} - } - opts, err := p.AuthorizeSSHSign(ctx, ott) - if err != nil { - return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext} - } - return opts, nil + return nil, errs.NotFound(errors.New("authority.GetSSHBastion; ssh is not configured")) } // SignSSH creates a signed SSH certificate with the given public key and options. @@ -226,27 +202,21 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // validate the given SSHOptions case provisioner.SSHCertificateOptionsValidator: if err := o.Valid(opts); err != nil { - return nil, &apiError{err: err, code: http.StatusForbidden} + return nil, errs.Forbidden(err) } default: - return nil, &apiError{ - err: errors.Errorf("signSSH: invalid extra option type %T", o), - code: http.StatusInternalServerError, - } + return nil, errs.InternalServerError(errors.Errorf("signSSH: invalid extra option type %T", o)) } } nonce, err := randutil.ASCII(32) if err != nil { - return nil, &apiError{err: err, code: http.StatusInternalServerError} + return nil, errs.InternalServerError(err) } var serial uint64 if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error reading random number"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error reading random number") } // Build base certificate with the key and some random values @@ -258,13 +228,13 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // Use opts to modify the certificate if err := opts.Modify(cert); err != nil { - return nil, &apiError{err: err, code: http.StatusForbidden} + return nil, errs.Forbidden(err) } // Use provisioner modifiers for _, m := range mods { if err := m.Modify(cert); err != nil { - return nil, &apiError{err: err, code: http.StatusForbidden} + return nil, errs.Forbidden(err) } } @@ -273,25 +243,16 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, &apiError{ - err: errors.New("signSSH: user certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("signSSH: user certificate signing is not enabled")) } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, &apiError{ - err: errors.New("signSSH: host certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("signSSH: host certificate signing is not enabled")) } signer = a.sshCAHostCertSignKey default: - return nil, &apiError{ - err: errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType), - code: http.StatusInternalServerError, - } + return nil, errs.InternalServerError(errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType)) } cert.SignatureKey = signer.PublicKey() @@ -302,71 +263,38 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // Sign the certificate sig, err := signer.Sign(rand.Reader, data) if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error signing certificate"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") } cert.Signature = sig // User provisioners validators for _, v := range validators { if err := v.Valid(cert); err != nil { - return nil, &apiError{err: err, code: http.StatusForbidden} + return nil, errs.Forbidden(err) } } if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error storing certificate in db"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error storing certificate in db") } return cert, nil } -// authorizeSSHRenew authorizes an SSH certificate renewal request, by -// validating the contents of an SSHPOP token. -func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { - errContext := map[string]interface{}{"ott": token} - - p, err := a.authorizeToken(ctx, token) - if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "authorizeSSHRenew"), - code: http.StatusUnauthorized, - context: errContext, - } - } - cert, err := p.AuthorizeSSHRenew(ctx, token) - if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "authorizeSSHRenew"), - code: http.StatusUnauthorized, - context: errContext, - } - } - return cert, nil -} - // RenewSSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) { nonce, err := randutil.ASCII(32) if err != nil { - return nil, &apiError{err: err, code: http.StatusInternalServerError} + return nil, errs.InternalServerError(err) } var serial uint64 if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "renewSSH: error reading random number"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error reading random number") } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errors.New("rewnewSSH: cannot renew certificate without validity period") + return nil, errs.BadRequest(errors.New("rewnewSSH: cannot renew certificate without validity period")) } backdate := a.config.AuthorityConfig.Backdate.Duration @@ -393,25 +321,16 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, &apiError{ - err: errors.New("renewSSH: user certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("renewSSH: user certificate signing is not enabled")) } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, &apiError{ - err: errors.New("renewSSH: host certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("renewSSH: host certificate signing is not enabled")) } signer = a.sshCAHostCertSignKey default: - return nil, &apiError{ - err: errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType), - code: http.StatusInternalServerError, - } + return nil, errs.InternalServerError(errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType)) } cert.SignatureKey = signer.PublicKey() @@ -422,47 +341,17 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) // Sign the certificate sig, err := signer.Sign(rand.Reader, data) if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "renewSSH: error signing certificate"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error signing certificate") } cert.Signature = sig if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { - return nil, &apiError{ - err: errors.Wrap(err, "renewSSH: error storing certificate in db"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db") } return cert, nil } -// authorizeSSHRekey authorizes an SSH certificate rekey request, by -// validating the contents of an SSHPOP token. -func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) { - errContext := map[string]interface{}{"ott": token} - - p, err := a.authorizeToken(ctx, token) - if err != nil { - return nil, nil, &apiError{ - err: errors.Wrap(err, "authorizeSSHRenew"), - code: http.StatusUnauthorized, - context: errContext, - } - } - cert, opts, err := p.AuthorizeSSHRekey(ctx, token) - if err != nil { - return nil, nil, &apiError{ - err: errors.Wrap(err, "authorizeSSHRekey"), - code: http.StatusUnauthorized, - context: errContext, - } - } - return cert, opts, nil -} - // RekeySSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var validators []provisioner.SSHCertificateValidator @@ -473,28 +362,22 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp case provisioner.SSHCertificateValidator: validators = append(validators, o) default: - return nil, &apiError{ - err: errors.Errorf("rekeySSH: invalid extra option type %T", o), - code: http.StatusInternalServerError, - } + return nil, errs.InternalServerError(errors.Errorf("rekeySSH; invalid extra option type %T", o)) } } nonce, err := randutil.ASCII(32) if err != nil { - return nil, &apiError{err: err, code: http.StatusInternalServerError} + return nil, errs.InternalServerError(err) } var serial uint64 if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "rekeySSH: error reading random number"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error reading random number") } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errors.New("rekeySSH: cannot rekey certificate without validity period") + return nil, errs.BadRequest(errors.New("rekeySSH; cannot rekey certificate without validity period")) } backdate := a.config.AuthorityConfig.Backdate.Duration @@ -521,25 +404,16 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, &apiError{ - err: errors.New("rekeySSH: user certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("rekeySSH; user certificate signing is not enabled")) } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, &apiError{ - err: errors.New("rekeySSH: host certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("rekeySSH; host certificate signing is not enabled")) } signer = a.sshCAHostCertSignKey default: - return nil, &apiError{ - err: errors.Errorf("rekeySSH: unexpected ssh certificate type: %d", cert.CertType), - code: http.StatusInternalServerError, - } + return nil, errs.BadRequest(errors.Errorf("rekeySSH; unexpected ssh certificate type: %d", cert.CertType)) } cert.SignatureKey = signer.PublicKey() @@ -547,80 +421,47 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp data := cert.Marshal() data = data[:len(data)-4] - // Sign the certificate + // Sign the certificate. sig, err := signer.Sign(rand.Reader, data) if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "rekeySSH: error signing certificate"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error signing certificate") } cert.Signature = sig - // User provisioners validators + // Apply validators from provisioner.. for _, v := range validators { if err := v.Valid(cert); err != nil { - return nil, &apiError{err: err, code: http.StatusForbidden} + return nil, errs.Forbidden(err) } } if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { - return nil, &apiError{ - err: errors.Wrap(err, "rekeySSH: error storing certificate in db"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db") } return cert, nil } -// authorizeSSHRevoke authorizes an SSH certificate revoke request, by -// validating the contents of an SSHPOP token. -func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error { - errContext := map[string]interface{}{"ott": token} - - p, err := a.authorizeToken(ctx, token) - if err != nil { - return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext} - } - if err = p.AuthorizeSSHRevoke(ctx, token); err != nil { - return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext} - } - return nil -} - // SignSSHAddUser signs a certificate that provisions a new user in a server. func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) { if a.sshCAUserCertSignKey == nil { - return nil, &apiError{ - err: errors.New("signSSHAddUser: user certificate signing is not enabled"), - code: http.StatusNotImplemented, - } + return nil, errs.NotImplemented(errors.New("signSSHAddUser: user certificate signing is not enabled")) } if subject.CertType != ssh.UserCert { - return nil, &apiError{ - err: errors.New("signSSHAddUser: certificate is not a user certificate"), - code: http.StatusForbidden, - } + return nil, errs.Forbidden(errors.New("signSSHAddUser: certificate is not a user certificate")) } if len(subject.ValidPrincipals) != 1 { - return nil, &apiError{ - err: errors.New("signSSHAddUser: certificate does not have only one principal"), - code: http.StatusForbidden, - } + return nil, errs.Forbidden(errors.New("signSSHAddUser: certificate does not have only one principal")) } nonce, err := randutil.ASCII(32) if err != nil { - return nil, &apiError{err: err, code: http.StatusInternalServerError} + return nil, errs.InternalServerError(err) } var serial uint64 if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSHAddUser: error reading random number"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error reading random number") } signer := a.sshCAUserCertSignKey @@ -656,10 +497,7 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) cert.Signature = sig if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { - return nil, &apiError{ - err: errors.Wrap(err, "signSSHAddUser: error storing certificate in db"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db") } return cert, nil @@ -691,14 +529,12 @@ func (a *Authority) CheckSSHHost(ctx context.Context, principal string, token st // GetSSHHosts returns a list of valid host principals. func (a *Authority) GetSSHHosts(cert *x509.Certificate) ([]sshutil.Host, error) { if a.sshGetHostsFunc != nil { - return a.sshGetHostsFunc(cert) + hosts, err := a.sshGetHostsFunc(cert) + return hosts, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts") } hostnames, err := a.db.GetSSHHostPrincipals() if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "getSSHHosts"), - code: http.StatusInternalServerError, - } + return nil, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts") } hosts := make([]sshutil.Host, len(hostnames)) diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 9b403132..db5dc85d 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -5,8 +5,10 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" "encoding/base64" "fmt" + "net/http" "reflect" "testing" "time" @@ -15,6 +17,8 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/sshutil" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/jose" "golang.org/x/crypto/ssh" @@ -498,8 +502,8 @@ func TestAuthority_CheckSSHHost(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := testAuthority(t) - a.db = &MockAuthDB{ - isSSHHost: func(_ string) (bool, error) { + a.db = &db.MockAuthDB{ + MIsSSHHost: func(_ string) (bool, error) { return tt.fields.exists, tt.fields.err }, } @@ -640,6 +644,9 @@ func TestAuthority_GetSSHBastion(t *testing.T) { if (err != nil) != tt.wantErr { t.Errorf("Authority.GetSSHBastion() error = %v, wantErr %v", err, tt.wantErr) return + } else if err != nil { + _, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Authority.GetSSHBastion() = %v, want %v", got, tt.want) @@ -647,3 +654,266 @@ func TestAuthority_GetSSHBastion(t *testing.T) { }) } } + +func TestAuthority_GetSSHHosts(t *testing.T) { + a := testAuthority(t) + + type test struct { + getHostsFunc func(*x509.Certificate) ([]sshutil.Host, error) + auth *Authority + cert *x509.Certificate + cmp func(got []sshutil.Host) + err error + code int + } + tests := map[string]func(t *testing.T) *test{ + "fail/getHostsFunc-fail": func(t *testing.T) *test { + return &test{ + getHostsFunc: func(cert *x509.Certificate) ([]sshutil.Host, error) { + return nil, errors.New("force") + }, + cert: &x509.Certificate{}, + err: errors.New("getSSHHosts: force"), + code: http.StatusInternalServerError, + } + }, + "ok/getHostsFunc-defined": func(t *testing.T) *test { + hosts := []sshutil.Host{ + {HostID: "1", Hostname: "foo"}, + {HostID: "2", Hostname: "bar"}, + } + + return &test{ + getHostsFunc: func(cert *x509.Certificate) ([]sshutil.Host, error) { + return hosts, nil + }, + cert: &x509.Certificate{}, + cmp: func(got []sshutil.Host) { + assert.Equals(t, got, hosts) + }, + } + }, + "fail/db-get-fail": func(t *testing.T) *test { + return &test{ + auth: testAuthority(t, WithDatabase(&db.MockAuthDB{ + MGetSSHHostPrincipals: func() ([]string, error) { + return nil, errors.New("force") + }, + })), + cert: &x509.Certificate{}, + err: errors.New("getSSHHosts: force"), + code: http.StatusInternalServerError, + } + }, + "ok": func(t *testing.T) *test { + return &test{ + auth: testAuthority(t, WithDatabase(&db.MockAuthDB{ + MGetSSHHostPrincipals: func() ([]string, error) { + return []string{"foo", "bar"}, nil + }, + })), + cert: &x509.Certificate{}, + cmp: func(got []sshutil.Host) { + assert.Equals(t, got, []sshutil.Host{ + {Hostname: "foo"}, + {Hostname: "bar"}, + }) + }, + } + }, + } + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + auth := tc.auth + if auth == nil { + auth = a + } + auth.sshGetHostsFunc = tc.getHostsFunc + + hosts, err := auth.GetSSHHosts(tc.cert) + if err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + tc.cmp(hosts) + } + } + }) + } +} + +func TestAuthority_RekeySSH(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + pub, err := ssh.NewPublicKey(key.Public()) + assert.FatalError(t, err) + signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.FatalError(t, err) + signer, err := ssh.NewSignerFromKey(signKey) + assert.FatalError(t, err) + + userOptions := sshTestModifier{ + CertType: ssh.UserCert, + } + + now := time.Now().UTC() + + a := testAuthority(t) + + type test struct { + auth *Authority + userSigner ssh.Signer + hostSigner ssh.Signer + cert *ssh.Certificate + key ssh.PublicKey + signOpts []provisioner.SignOption + cmpResult func(old, n *ssh.Certificate) + err error + code int + } + tests := map[string]func(t *testing.T) *test{ + "fail/opts-type": func(t *testing.T) *test { + return &test{ + userSigner: signer, + hostSigner: signer, + key: pub, + signOpts: []provisioner.SignOption{userOptions}, + err: errors.New("rekeySSH; invalid extra option type"), + code: http.StatusInternalServerError, + } + }, + "fail/old-cert-validAfter": func(t *testing.T) *test { + return &test{ + userSigner: signer, + hostSigner: signer, + cert: &ssh.Certificate{}, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("rekeySSH; cannot rekey certificate without validity period"), + code: http.StatusBadRequest, + } + }, + "fail/old-cert-validBefore": func(t *testing.T) *test { + return &test{ + userSigner: signer, + hostSigner: signer, + cert: &ssh.Certificate{ValidAfter: uint64(now.Unix())}, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("rekeySSH; cannot rekey certificate without validity period"), + code: http.StatusBadRequest, + } + }, + "fail/old-cert-no-user-key": func(t *testing.T) *test { + return &test{ + userSigner: nil, + hostSigner: signer, + cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.UserCert}, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("rekeySSH; user certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + }, + "fail/old-cert-no-host-key": func(t *testing.T) *test { + return &test{ + userSigner: signer, + hostSigner: nil, + cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.HostCert}, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("rekeySSH; host certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + }, + "fail/unexpected-old-cert-type": func(t *testing.T) *test { + return &test{ + userSigner: signer, + hostSigner: signer, + cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: 0}, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("rekeySSH; unexpected ssh certificate type: 0"), + code: http.StatusBadRequest, + } + }, + "fail/db-store": func(t *testing.T) *test { + return &test{ + auth: testAuthority(t, WithDatabase(&db.MockAuthDB{ + MStoreSSHCertificate: func(cert *ssh.Certificate) error { + return errors.New("force") + }, + })), + userSigner: signer, + hostSigner: nil, + cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.UserCert}, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("rekeySSH; error storing certificate in db: force"), + code: http.StatusInternalServerError, + } + }, + "ok": func(t *testing.T) *test { + va1 := now.Add(-24 * time.Hour) + vb1 := now.Add(-23 * time.Hour) + return &test{ + userSigner: signer, + hostSigner: nil, + cert: &ssh.Certificate{ + ValidAfter: uint64(va1.Unix()), + ValidBefore: uint64(vb1.Unix()), + CertType: ssh.UserCert, + ValidPrincipals: []string{"foo", "bar"}, + KeyId: "foo", + }, + key: pub, + signOpts: []provisioner.SignOption{}, + cmpResult: func(old, n *ssh.Certificate) { + assert.Equals(t, n.CertType, old.CertType) + assert.Equals(t, n.ValidPrincipals, old.ValidPrincipals) + assert.Equals(t, n.KeyId, old.KeyId) + + assert.True(t, n.ValidAfter > uint64(now.Add(-5*time.Minute).Unix())) + assert.True(t, n.ValidAfter < uint64(now.Add(5*time.Minute).Unix())) + + l8r := now.Add(1 * time.Hour) + assert.True(t, n.ValidBefore > uint64(l8r.Add(-5*time.Minute).Unix())) + assert.True(t, n.ValidBefore < uint64(l8r.Add(5*time.Minute).Unix())) + }, + } + }, + } + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + auth := tc.auth + if auth == nil { + auth = a + } + a.sshCAUserCertSignKey = tc.userSigner + a.sshCAHostCertSignKey = tc.hostSigner + + cert, err := auth.RekeySSH(tc.cert, tc.key, tc.signOpts...) + if err != nil { + if assert.NotNil(t, tc.err) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + tc.cmpResult(tc.cert, cert) + } + } + }) + } +} diff --git a/authority/testdata/certs/ssh_host_ca_key.pub b/authority/testdata/certs/ssh_host_ca_key.pub new file mode 100644 index 00000000..aa5685da --- /dev/null +++ b/authority/testdata/certs/ssh_host_ca_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY= diff --git a/authority/testdata/certs/ssh_user_ca_key.pub b/authority/testdata/certs/ssh_user_ca_key.pub new file mode 100644 index 00000000..5909ce43 --- /dev/null +++ b/authority/testdata/certs/ssh_user_ca_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y= diff --git a/authority/testdata/secrets/ssh_host_ca_key b/authority/testdata/secrets/ssh_host_ca_key new file mode 100644 index 00000000..7a7e4c44 --- /dev/null +++ b/authority/testdata/secrets/ssh_host_ca_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49 +AwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+ +MrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg== +-----END EC PRIVATE KEY----- diff --git a/authority/testdata/secrets/ssh_user_ca_key b/authority/testdata/secrets/ssh_user_ca_key new file mode 100644 index 00000000..92d35ec2 --- /dev/null +++ b/authority/testdata/secrets/ssh_user_ca_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49 +AwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h +YpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg== +-----END EC PRIVATE KEY----- diff --git a/authority/tls.go b/authority/tls.go index eb7cb86a..9199c040 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -14,6 +14,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" @@ -60,7 +61,7 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption { // Sign creates a signed certificate from a certificate signing request. func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { var ( - errContext = apiCtx{"csr": csr, "signOptions": signOpts} + opts = []errs.Option{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)} mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)} certValidators = []provisioner.CertificateValidator{} issIdentity = a.intermediateIdentity @@ -75,54 +76,52 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti certValidators = append(certValidators, k) case provisioner.CertificateRequestValidator: if err := k.Valid(csr); err != nil { - return nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext} + return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...) } case provisioner.ProfileModifier: mods = append(mods, k.Option(signOpts)) default: - return nil, &apiError{errors.Errorf("sign: invalid extra option type %T", k), - http.StatusInternalServerError, errContext} + return nil, errs.InternalServerError(errors.Errorf("authority.Sign; invalid extra option type %T", k), opts...) } } if err := csr.CheckSignature(); err != nil { - return nil, &apiError{errors.Wrap(err, "sign: invalid certificate request"), - http.StatusBadRequest, errContext} + return nil, errs.Wrap(http.StatusBadRequest, err, "authority.Sign; invalid certificate request", opts...) } leaf, err := x509util.NewLeafProfileWithCSR(csr, issIdentity.Crt, issIdentity.Key, mods...) if err != nil { - return nil, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...) } for _, v := range certValidators { - if err := v.Valid(leaf.Subject()); err != nil { - return nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext} + if err := v.Valid(leaf.Subject(), signOpts); err != nil { + return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...) } } crtBytes, err := leaf.CreateCertificate() if err != nil { - return nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"), - http.StatusInternalServerError, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.Sign; error creating new leaf certificate", opts...) } serverCert, err := x509.ParseCertificate(crtBytes) if err != nil { - return nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"), - http.StatusInternalServerError, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.Sign; error parsing new leaf certificate", opts...) } caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) if err != nil { - return nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"), - http.StatusInternalServerError, errContext} + 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, &apiError{errors.Wrap(err, "sign: error storing certificate in db"), - http.StatusInternalServerError, errContext} + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.Sign; error storing certificate in db", opts...) } } @@ -132,9 +131,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti // Renew creates a new Certificate identical to the old certificate, except // with a validity window that begins 'now'. func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { + opts := []errs.Option{errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String())} + // Check step provisioner extensions if err := a.authorizeRenew(oldCert); err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...) } // Issuer @@ -161,16 +162,16 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error MaxPathLenZero: oldCert.MaxPathLenZero, OCSPServer: oldCert.OCSPServer, IssuingCertificateURL: oldCert.IssuingCertificateURL, + PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical, + PermittedEmailAddresses: oldCert.PermittedEmailAddresses, DNSNames: oldCert.DNSNames, EmailAddresses: oldCert.EmailAddresses, IPAddresses: oldCert.IPAddresses, URIs: oldCert.URIs, - PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical, PermittedDNSDomains: oldCert.PermittedDNSDomains, ExcludedDNSDomains: oldCert.ExcludedDNSDomains, PermittedIPRanges: oldCert.PermittedIPRanges, ExcludedIPRanges: oldCert.ExcludedIPRanges, - PermittedEmailAddresses: oldCert.PermittedEmailAddresses, ExcludedEmailAddresses: oldCert.ExcludedEmailAddresses, PermittedURIDomains: oldCert.PermittedURIDomains, ExcludedURIDomains: oldCert.ExcludedURIDomains, @@ -190,29 +191,28 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error leaf, err := x509util.NewLeafProfileWithTemplate(newCert, issIdentity.Crt, issIdentity.Key) if err != nil { - return nil, &apiError{err, http.StatusInternalServerError, apiCtx{}} + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...) } crtBytes, err := leaf.CreateCertificate() if err != nil { - return nil, &apiError{errors.Wrap(err, "error renewing certificate from existing server certificate"), - http.StatusInternalServerError, apiCtx{}} + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.Renew; error renewing certificate from existing server certificate", opts...) } serverCert, err := x509.ParseCertificate(crtBytes) if err != nil { - return nil, &apiError{errors.Wrap(err, "error parsing new server certificate"), - http.StatusInternalServerError, apiCtx{}} + 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, &apiError{errors.Wrap(err, "error parsing intermediate certificate"), - http.StatusInternalServerError, apiCtx{}} + 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 { - return nil, &apiError{errors.Wrap(err, "error storing certificate in db"), - http.StatusInternalServerError, apiCtx{}} + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew; error storing certificate in db", opts...) } } @@ -236,26 +236,26 @@ type RevokeOptions struct { // being renewed. // // TODO: Add OCSP and CRL support. -func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error { - errContext := apiCtx{ - "serialNumber": opts.Serial, - "reasonCode": opts.ReasonCode, - "reason": opts.Reason, - "passiveOnly": opts.PassiveOnly, - "mTLS": opts.MTLS, - "context": string(provisioner.MethodFromContext(ctx)), +func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error { + opts := []errs.Option{ + errs.WithKeyVal("serialNumber", revokeOpts.Serial), + errs.WithKeyVal("reasonCode", revokeOpts.ReasonCode), + errs.WithKeyVal("reason", revokeOpts.Reason), + errs.WithKeyVal("passiveOnly", revokeOpts.PassiveOnly), + errs.WithKeyVal("MTLS", revokeOpts.MTLS), + errs.WithKeyVal("context", string(provisioner.MethodFromContext(ctx))), } - if opts.MTLS { - errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw) + if revokeOpts.MTLS { + opts = append(opts, errs.WithKeyVal("certificate", base64.StdEncoding.EncodeToString(revokeOpts.Crt.Raw))) } else { - errContext["ott"] = opts.OTT + opts = append(opts, errs.WithKeyVal("token", revokeOpts.OTT)) } rci := &db.RevokedCertificateInfo{ - Serial: opts.Serial, - ReasonCode: opts.ReasonCode, - Reason: opts.Reason, - MTLS: opts.MTLS, + Serial: revokeOpts.Serial, + ReasonCode: revokeOpts.ReasonCode, + Reason: revokeOpts.Reason, + MTLS: revokeOpts.MTLS, RevokedAt: time.Now().UTC(), } @@ -264,48 +264,43 @@ func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error { err error ) // If not mTLS then get the TokenID of the token. - if !opts.MTLS { - // Validate payload - token, err := jose.ParseSigned(opts.OTT) + if !revokeOpts.MTLS { + token, err := jose.ParseSigned(revokeOpts.OTT) if err != nil { - return &apiError{errors.Wrapf(err, "revoke: error parsing token"), - http.StatusUnauthorized, errContext} + return errs.Wrap(http.StatusUnauthorized, err, + "authority.Revoke; error parsing token", opts...) } - // Get claims w/out verification. We should have already verified this token - // earlier with a call to authorizeSSHRevoke. + // Get claims w/out verification. var claims Claims if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { - return &apiError{errors.Wrap(err, "revoke"), http.StatusUnauthorized, errContext} + return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke", opts...) } // This method will also validate the audiences for JWK provisioners. var ok bool p, ok = a.provisioners.LoadByToken(token, &claims.Claims) if !ok { - return &apiError{ - errors.Errorf("revoke: provisioner not found"), - http.StatusInternalServerError, errContext} + return errs.InternalServerError(errors.Errorf("authority.Revoke; provisioner not found"), opts...) } - rci.TokenID, err = p.GetTokenID(opts.OTT) + rci.TokenID, err = p.GetTokenID(revokeOpts.OTT) if err != nil { - return &apiError{errors.Wrap(err, "revoke: could not get ID for token"), - http.StatusInternalServerError, errContext} + return errs.Wrap(http.StatusInternalServerError, err, + "authority.Revoke; could not get ID for token") } - errContext["tokenID"] = rci.TokenID + opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID)) } else { // Load the Certificate provisioner if one exists. - p, err = a.LoadProvisionerByCertificate(opts.Crt) + p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt) if err != nil { - return &apiError{ - errors.Wrap(err, "revoke: unable to load certificate provisioner"), - http.StatusUnauthorized, errContext} + return errs.Wrap(http.StatusUnauthorized, err, + "authority.Revoke: unable to load certificate provisioner", opts...) } } rci.ProvisionerID = p.GetID() - errContext["provisionerID"] = rci.ProvisionerID + opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) - if provisioner.MethodFromContext(ctx) == provisioner.RevokeSSHMethod { + if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod { err = a.db.RevokeSSH(rci) } else { // default to revoke x509 err = a.db.Revoke(rci) @@ -314,13 +309,12 @@ func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error { case nil: return nil case db.ErrNotImplemented: - return &apiError{errors.New("revoke: no persistence layer configured"), - http.StatusNotImplemented, errContext} + return errs.NotImplemented(errors.New("authority.Revoke; no persistence layer configured"), opts...) case db.ErrAlreadyExists: - return &apiError{errors.Errorf("revoke: certificate with serial number %s has already been revoked", rci.Serial), - http.StatusBadRequest, errContext} + return errs.BadRequest(errors.Errorf("authority.Revoke; certificate with serial "+ + "number %s has already been revoked", rci.Serial), opts...) default: - return &apiError{err, http.StatusInternalServerError, errContext} + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) } } @@ -330,17 +324,17 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { a.intermediateIdentity.Crt, a.intermediateIdentity.Key, x509util.WithHosts(strings.Join(a.config.DNSNames, ","))) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") } crtBytes, err := profile.CreateCertificate() if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") } keyPEM, err := pemutil.Serialize(profile.SubjectPrivateKey()) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") } crtPEM := pem.EncodeToMemory(&pem.Block{ @@ -352,19 +346,21 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { // to a tls.Certificate. intermediatePEM, err := pemutil.Serialize(a.intermediateIdentity.Crt) if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") } tlsCrt, err := tls.X509KeyPair(append(crtPEM, pem.EncodeToMemory(intermediatePEM)...), pem.EncodeToMemory(keyPEM)) if err != nil { - return nil, errors.Wrap(err, "error creating tls certificate") + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.GetTLSCertificate; error creating tls certificate") } // Get the 'leaf' certificate and set the attribute accordingly. leaf, err := x509.ParseCertificate(tlsCrt.Certificate[0]) if err != nil { - return nil, errors.Wrap(err, "error parsing tls certificate") + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.GetTLSCertificate; error parsing tls certificate") } tlsCrt.Leaf = leaf diff --git a/authority/tls_test.go b/authority/tls_test.go index c5c7f8c1..3fbd21bf 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" - "encoding/base64" "encoding/pem" "fmt" "net/http" @@ -19,6 +18,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/tlsutil" @@ -77,7 +77,7 @@ func getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateReques return csr } -func TestSign(t *testing.T) { +func TestAuthority_Sign(t *testing.T) { pub, priv, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) @@ -102,7 +102,7 @@ func TestSign(t *testing.T) { p := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK) key, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) - token, err := generateToken("smallstep test", "step-cli", "https://test.ca.smallstep.com/sign", []string{"test.smallstep.com"}, time.Now(), key) + token, err := generateToken("smallstep test", "step-cli", testAudiences.Sign[0], []string{"test.smallstep.com"}, time.Now(), key) assert.FatalError(t, err) ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) extraOpts, err := a.Authorize(ctx, token) @@ -113,7 +113,8 @@ func TestSign(t *testing.T) { csr *x509.CertificateRequest signOpts provisioner.Options extraOpts []provisioner.SignOption - err *apiError + err error + code int } tests := map[string]func(*testing.T) *signTest{ "fail invalid signature": func(t *testing.T) *signTest { @@ -124,10 +125,8 @@ func TestSign(t *testing.T) { csr: csr, extraOpts: extraOpts, signOpts: signOpts, - err: &apiError{errors.New("sign: invalid certificate request"), - http.StatusBadRequest, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign; invalid certificate request"), + code: http.StatusBadRequest, } }, "fail invalid extra option": func(t *testing.T) *signTest { @@ -138,10 +137,8 @@ func TestSign(t *testing.T) { csr: csr, extraOpts: append(extraOpts, "42"), signOpts: signOpts, - err: &apiError{errors.New("sign: invalid extra option type string"), - http.StatusInternalServerError, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign; invalid extra option type string"), + code: http.StatusInternalServerError, } }, "fail merge default ASN1DN": func(t *testing.T) *signTest { @@ -153,10 +150,8 @@ func TestSign(t *testing.T) { csr: csr, extraOpts: extraOpts, signOpts: signOpts, - err: &apiError{errors.New("sign: default ASN1DN template cannot be nil"), - http.StatusInternalServerError, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign: default ASN1DN template cannot be nil"), + code: http.StatusInternalServerError, } }, "fail create cert": func(t *testing.T) *signTest { @@ -168,10 +163,8 @@ func TestSign(t *testing.T) { csr: csr, extraOpts: extraOpts, signOpts: signOpts, - err: &apiError{errors.New("sign: error creating new leaf certificate"), - http.StatusInternalServerError, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign; error creating new leaf certificate"), + code: http.StatusInternalServerError, } }, "fail provisioner duration claim": func(t *testing.T) *signTest { @@ -185,10 +178,8 @@ func TestSign(t *testing.T) { csr: csr, extraOpts: extraOpts, signOpts: _signOpts, - err: &apiError{errors.New("sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s"), - http.StatusUnauthorized, - apiCtx{"csr": csr, "signOptions": _signOpts}, - }, + err: errors.New("authority.Sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s"), + code: http.StatusUnauthorized, } }, "fail validate sans when adding common name not in claims": func(t *testing.T) *signTest { @@ -200,10 +191,8 @@ func TestSign(t *testing.T) { csr: csr, extraOpts: extraOpts, signOpts: signOpts, - err: &apiError{errors.New("sign: certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"), - http.StatusUnauthorized, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign: certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"), + code: http.StatusUnauthorized, } }, "fail rsa key too short": func(t *testing.T) *signTest { @@ -228,20 +217,16 @@ ZYtQ9Ot36qc= csr: csr, extraOpts: extraOpts, signOpts: signOpts, - err: &apiError{errors.New("sign: rsa key in CSR must be at least 2048 bits (256 bytes)"), - http.StatusUnauthorized, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign: rsa key in CSR must be at least 2048 bits (256 bytes)"), + code: http.StatusUnauthorized, } }, "fail store cert in db": func(t *testing.T) *signTest { csr := getCSR(t, priv) _a := testAuthority(t) - _a.db = &MockAuthDB{ - storeCertificate: func(crt *x509.Certificate) error { - return &apiError{errors.New("force"), - http.StatusInternalServerError, - apiCtx{"csr": csr, "signOptions": signOpts}} + _a.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + return errors.New("force") }, } return &signTest{ @@ -249,17 +234,15 @@ ZYtQ9Ot36qc= csr: csr, extraOpts: extraOpts, signOpts: signOpts, - err: &apiError{errors.New("sign: error storing certificate in db: force"), - http.StatusInternalServerError, - apiCtx{"csr": csr, "signOptions": signOpts}, - }, + err: errors.New("authority.Sign; error storing certificate in db: force"), + code: http.StatusInternalServerError, } }, "ok": func(t *testing.T) *signTest { csr := getCSR(t, priv) _a := testAuthority(t) - _a.db = &MockAuthDB{ - storeCertificate: func(crt *x509.Certificate) error { + _a.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { assert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, @@ -279,15 +262,17 @@ ZYtQ9Ot36qc= certChain, err := tc.auth.Sign(tc.csr, tc.signOpts, tc.extraOpts...) if err != nil { - if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { + assert.Nil(t, certChain) + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + + ctxErr, ok := err.(*errs.Error) + assert.Fatal(t, ok, "error is not of type *errs.Error") + assert.Equals(t, ctxErr.Details["csr"], tc.csr) + assert.Equals(t, ctxErr.Details["signOptions"], tc.signOpts) } } else { leaf := certChain[0] @@ -346,7 +331,7 @@ ZYtQ9Ot36qc= } } -func TestRenew(t *testing.T) { +func TestAuthority_Renew(t *testing.T) { pub, _, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) @@ -375,9 +360,9 @@ func TestRenew(t *testing.T) { x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"), withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID)) assert.FatalError(t, err) - crtBytes, err := leaf.CreateCertificate() + certBytes, err := leaf.CreateCertificate() assert.FatalError(t, err) - crt, err := x509.ParseCertificate(crtBytes) + cert, err := x509.ParseCertificate(certBytes) assert.FatalError(t, err) leafNoRenew, err := x509util.NewLeafProfile("norenew", a.intermediateIdentity.Crt, @@ -388,15 +373,16 @@ func TestRenew(t *testing.T) { withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID), ) assert.FatalError(t, err) - crtBytesNoRenew, err := leafNoRenew.CreateCertificate() + certBytesNoRenew, err := leafNoRenew.CreateCertificate() assert.FatalError(t, err) - crtNoRenew, err := x509.ParseCertificate(crtBytesNoRenew) + certNoRenew, err := x509.ParseCertificate(certBytesNoRenew) assert.FatalError(t, err) type renewTest struct { auth *Authority - crt *x509.Certificate - err *apiError + cert *x509.Certificate + err error + code int } tests := map[string]func() (*renewTest, error){ "fail-create-cert": func() (*renewTest, error) { @@ -404,25 +390,22 @@ func TestRenew(t *testing.T) { _a.intermediateIdentity.Key = nil return &renewTest{ auth: _a, - crt: crt, - err: &apiError{errors.New("error renewing certificate from existing server certificate"), - http.StatusInternalServerError, apiCtx{}}, + cert: cert, + err: errors.New("authority.Renew; error renewing certificate from existing server certificate"), + code: http.StatusInternalServerError, }, nil }, "fail-unauthorized": func() (*renewTest, error) { - ctx := map[string]interface{}{ - "serialNumber": crtNoRenew.SerialNumber.String(), - } return &renewTest{ - crt: crtNoRenew, - err: &apiError{errors.New("renew: renew is disabled for provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"), - http.StatusUnauthorized, ctx}, + cert: certNoRenew, + err: errors.New("authority.Renew: authority.authorizeRenew: jwk.AuthorizeRenew; renew is disabled for jwk provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"), + code: http.StatusUnauthorized, }, nil }, "success": func() (*renewTest, error) { return &renewTest{ auth: a, - crt: crt, + cert: cert, }, nil }, "success-new-intermediate": func() (*renewTest, error) { @@ -430,23 +413,23 @@ func TestRenew(t *testing.T) { assert.FatalError(t, err) newRootBytes, err := newRootProfile.CreateCertificate() assert.FatalError(t, err) - newRootCrt, err := x509.ParseCertificate(newRootBytes) + newRootCert, err := x509.ParseCertificate(newRootBytes) assert.FatalError(t, err) newIntermediateProfile, err := x509util.NewIntermediateProfile("new-intermediate", - newRootCrt, newRootProfile.SubjectPrivateKey()) + newRootCert, newRootProfile.SubjectPrivateKey()) assert.FatalError(t, err) newIntermediateBytes, err := newIntermediateProfile.CreateCertificate() assert.FatalError(t, err) - newIntermediateCrt, err := x509.ParseCertificate(newIntermediateBytes) + newIntermediateCert, err := x509.ParseCertificate(newIntermediateBytes) assert.FatalError(t, err) _a := testAuthority(t) _a.intermediateIdentity.Key = newIntermediateProfile.SubjectPrivateKey() - _a.intermediateIdentity.Crt = newIntermediateCrt + _a.intermediateIdentity.Crt = newIntermediateCert return &renewTest{ auth: _a, - crt: crt, + cert: cert, }, nil }, } @@ -458,32 +441,33 @@ func TestRenew(t *testing.T) { var certChain []*x509.Certificate if tc.auth != nil { - certChain, err = tc.auth.Renew(tc.crt) + certChain, err = tc.auth.Renew(tc.cert) } else { - certChain, err = a.Renew(tc.crt) + certChain, err = a.Renew(tc.cert) } if err != nil { - if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { + assert.Nil(t, certChain) + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + + ctxErr, ok := err.(*errs.Error) + assert.Fatal(t, ok, "error is not of type *errs.Error") + assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String()) } } else { leaf := certChain[0] intermediate := certChain[1] if assert.Nil(t, tc.err) { - assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.crt.NotAfter.Sub(crt.NotBefore)) + assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore)) - assert.True(t, leaf.NotBefore.After(now.Add(-time.Minute))) + assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute))) assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute))) expiry := now.Add(time.Minute * 7) - assert.True(t, leaf.NotAfter.After(expiry.Add(-time.Minute))) + assert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute))) assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Minute))) tmplt := a.config.AuthorityConfig.Template @@ -513,7 +497,7 @@ func TestRenew(t *testing.T) { if a.intermediateIdentity.Crt.SerialNumber == tc.auth.intermediateIdentity.Crt.SerialNumber { assert.Equals(t, leaf.AuthorityKeyId, a.intermediateIdentity.Crt.SubjectKeyId) // Compare extensions: they can be in a different order - for _, ext1 := range tc.crt.Extensions { + for _, ext1 := range tc.cert.Extensions { found := false for _, ext2 := range leaf.Extensions { if reflect.DeepEqual(ext1, ext2) { @@ -529,7 +513,7 @@ func TestRenew(t *testing.T) { // We did change the intermediate before renewing. assert.Equals(t, leaf.AuthorityKeyId, tc.auth.intermediateIdentity.Crt.SubjectKeyId) // Compare extensions: they can be in a different order - for _, ext1 := range tc.crt.Extensions { + for _, ext1 := range tc.cert.Extensions { // The authority key id extension should be different b/c the intermediates are different. if ext1.Id.Equal(oidAuthorityKeyIdentifier) { for _, ext2 := range leaf.Extensions { @@ -560,7 +544,7 @@ func TestRenew(t *testing.T) { } } -func TestGetTLSOptions(t *testing.T) { +func TestAuthority_GetTLSOptions(t *testing.T) { type renewTest struct { auth *Authority opts *tlsutil.TLSOptions @@ -596,21 +580,12 @@ func TestGetTLSOptions(t *testing.T) { } } -func TestRevoke(t *testing.T) { +func TestAuthority_Revoke(t *testing.T) { reasonCode := 2 reason := "bob was let go" validIssuer := "step-cli" - validAudience := []string{"https://test.ca.smallstep.com/revoke"} + validAudience := testAudiences.Revoke now := time.Now().UTC() - getCtx := func() map[string]interface{} { - return apiCtx{ - "serialNumber": "sn", - "reasonCode": reasonCode, - "reason": reason, - "mTLS": false, - "passiveOnly": false, - } - } jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) assert.FatalError(t, err) @@ -619,30 +594,30 @@ func TestRevoke(t *testing.T) { (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) + a := testAuthority(t) + type test struct { - a *Authority - opts *RevokeOptions - err *apiError + auth *Authority + opts *RevokeOptions + err error + code int + checkErrDetails func(err *errs.Error) } tests := map[string]func() test{ - "error/token/authorizeRevoke error": func() test { - a := testAuthority(t) - ctx := getCtx() - ctx["ott"] = "foo" + "fail/token/authorizeRevoke error": func() test { return test{ - a: a, + auth: a, opts: &RevokeOptions{ OTT: "foo", Serial: "sn", ReasonCode: reasonCode, Reason: reason, }, - err: &apiError{errors.New("revoke: authorizeRevoke: authorizeToken: error parsing token"), - http.StatusUnauthorized, ctx}, + err: errors.New("authority.Revoke; error parsing token"), + code: http.StatusUnauthorized, } }, - "error/nil-db": func() test { - a := testAuthority(t) + "fail/nil-db": func() test { cl := jwt.Claims{ Subject: "sn", Issuer: validIssuer, @@ -654,30 +629,30 @@ func TestRevoke(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) - ctx := getCtx() - ctx["ott"] = raw - ctx["tokenID"] = "44" - ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc" return test{ - a: a, + auth: a, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, - err: &apiError{errors.New("revoke: no persistence layer configured"), - http.StatusNotImplemented, ctx}, + err: errors.New("authority.Revoke; no persistence layer configured"), + code: http.StatusNotImplemented, + checkErrDetails: func(err *errs.Error) { + assert.Equals(t, err.Details["token"], raw) + assert.Equals(t, err.Details["tokenID"], "44") + assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") + }, } }, - "error/db-revoke": func() test { - a := testAuthority(t) - a.db = &MockAuthDB{ - useToken: func(id, tok string) (bool, error) { + "fail/db-revoke": func() test { + _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { return true, nil }, - err: errors.New("force"), - } + Err: errors.New("force"), + })) cl := jwt.Claims{ Subject: "sn", @@ -690,30 +665,30 @@ func TestRevoke(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) - ctx := getCtx() - ctx["ott"] = raw - ctx["tokenID"] = "44" - ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc" return test{ - a: a, + auth: _a, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, - err: &apiError{errors.New("force"), - http.StatusInternalServerError, ctx}, + err: errors.New("authority.Revoke: force"), + code: http.StatusInternalServerError, + checkErrDetails: func(err *errs.Error) { + assert.Equals(t, err.Details["token"], raw) + assert.Equals(t, err.Details["tokenID"], "44") + assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") + }, } }, - "error/already-revoked": func() test { - a := testAuthority(t) - a.db = &MockAuthDB{ - useToken: func(id, tok string) (bool, error) { + "fail/already-revoked": func() test { + _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { return true, nil }, - err: db.ErrAlreadyExists, - } + Err: db.ErrAlreadyExists, + })) cl := jwt.Claims{ Subject: "sn", @@ -726,29 +701,29 @@ func TestRevoke(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) - ctx := getCtx() - ctx["ott"] = raw - ctx["tokenID"] = "44" - ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc" return test{ - a: a, + auth: _a, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, Reason: reason, OTT: raw, }, - err: &apiError{errors.New("revoke: certificate with serial number sn has already been revoked"), - http.StatusBadRequest, ctx}, + err: errors.New("authority.Revoke; certificate with serial number sn has already been revoked"), + code: http.StatusBadRequest, + checkErrDetails: func(err *errs.Error) { + assert.Equals(t, err.Details["token"], raw) + assert.Equals(t, err.Details["tokenID"], "44") + assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") + }, } }, "ok/token": func() test { - a := testAuthority(t) - a.db = &MockAuthDB{ - useToken: func(id, tok string) (bool, error) { + _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { return true, nil }, - } + })) cl := jwt.Claims{ Subject: "sn", @@ -761,7 +736,7 @@ func TestRevoke(t *testing.T) { raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return test{ - a: a, + auth: _a, opts: &RevokeOptions{ Serial: "sn", ReasonCode: reasonCode, @@ -770,39 +745,14 @@ func TestRevoke(t *testing.T) { }, } }, - "error/mTLS/authorizeRevoke": func() test { - a := testAuthority(t) - a.db = &MockAuthDB{} - - crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") - assert.FatalError(t, err) - - ctx := getCtx() - ctx["certificate"] = base64.StdEncoding.EncodeToString(crt.Raw) - ctx["mTLS"] = true - - return test{ - a: a, - opts: &RevokeOptions{ - Crt: crt, - Serial: "sn", - ReasonCode: reasonCode, - Reason: reason, - MTLS: true, - }, - err: &apiError{errors.New("revoke: authorizeRevoke: serial number in certificate different than body"), - http.StatusUnauthorized, ctx}, - } - }, "ok/mTLS": func() test { - a := testAuthority(t) - a.db = &MockAuthDB{} + _a := testAuthority(t, WithDatabase(&db.MockAuthDB{})) crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") assert.FatalError(t, err) return test{ - a: a, + auth: _a, opts: &RevokeOptions{ Crt: crt, Serial: "102012593071130646873265215610956555026", @@ -816,15 +766,24 @@ func TestRevoke(t *testing.T) { for name, f := range tests { tc := f() t.Run(name, func(t *testing.T) { - if err := tc.a.Revoke(context.TODO(), tc.opts); err != nil { - if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) + ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) + if err := tc.auth.Revoke(ctx, tc.opts); err != nil { + if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) + + ctxErr, ok := err.(*errs.Error) + assert.Fatal(t, ok, "error is not of type *errs.Error") + assert.Equals(t, ctxErr.Details["serialNumber"], tc.opts.Serial) + assert.Equals(t, ctxErr.Details["reasonCode"], tc.opts.ReasonCode) + assert.Equals(t, ctxErr.Details["reason"], tc.opts.Reason) + assert.Equals(t, ctxErr.Details["MTLS"], tc.opts.MTLS) + assert.Equals(t, ctxErr.Details["context"], string(provisioner.RevokeMethod)) + + if tc.checkErrDetails != nil { + tc.checkErrDetails(ctxErr) } } } else { diff --git a/ca/ca_test.go b/ca/ca_test.go index ef00132c..4a04756d 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -22,6 +22,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/randutil" @@ -102,7 +103,7 @@ func TestCASign(t *testing.T) { ca: ca, body: "invalid json", status: http.StatusBadRequest, - errMsg: "Bad Request", + errMsg: errs.BadRequestDefaultMsg, } }, "fail invalid-csr-sig": func(t *testing.T) *signTest { @@ -140,7 +141,7 @@ ZEp7knvU2psWRw== ca: ca, body: string(body), status: http.StatusBadRequest, - errMsg: "Bad Request", + errMsg: errs.BadRequestDefaultMsg, } }, "fail unauthorized-ott": func(t *testing.T) *signTest { @@ -155,7 +156,7 @@ ZEp7knvU2psWRw== ca: ca, body: string(body), status: http.StatusUnauthorized, - errMsg: "Unauthorized", + errMsg: errs.UnauthorizedDefaultMsg, } }, "fail commonname-claim": func(t *testing.T) *signTest { @@ -188,7 +189,7 @@ ZEp7knvU2psWRw== ca: ca, body: string(body), status: http.StatusUnauthorized, - errMsg: "Unauthorized", + errMsg: errs.UnauthorizedDefaultMsg, } }, "ok": func(t *testing.T) *signTest { @@ -392,7 +393,7 @@ func TestCAProvisionerEncryptedKey(t *testing.T) { ca: ca, kid: "foo", status: http.StatusNotFound, - errMsg: "Not Found", + errMsg: errs.NotFoundDefaultMsg, } }, "ok": func(t *testing.T) *ekt { @@ -455,7 +456,7 @@ func TestCARoot(t *testing.T) { ca: ca, sha: "foo", status: http.StatusNotFound, - errMsg: "Not Found", + errMsg: errs.NotFoundDefaultMsg, } }, "success": func(t *testing.T) *rootTest { @@ -575,7 +576,7 @@ func TestCARenew(t *testing.T) { ca: ca, tlsConnState: nil, status: http.StatusBadRequest, - errMsg: "Bad Request", + errMsg: errs.BadRequestDefaultMsg, } }, "request-missing-peer-certificate": func(t *testing.T) *renewTest { @@ -583,7 +584,7 @@ func TestCARenew(t *testing.T) { ca: ca, tlsConnState: &tls.ConnectionState{PeerCertificates: []*x509.Certificate{}}, status: http.StatusBadRequest, - errMsg: "Bad Request", + errMsg: errs.BadRequestDefaultMsg, } }, "success": func(t *testing.T) *renewTest { diff --git a/ca/client.go b/ca/client.go index 051bac5b..e6fdab92 100644 --- a/ca/client.go +++ b/ca/client.go @@ -486,7 +486,7 @@ func (c *Client) Version() (*api.VersionResponse, error) { retry: resp, err := c.client.Get(u.String()) if err != nil { - return nil, errors.Wrapf(err, "client GET %s failed", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Version; client GET %s failed", u) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -497,7 +497,7 @@ retry: } var version api.VersionResponse if err := readJSON(resp.Body, &version); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Version; error reading %s", u) } return &version, nil } @@ -510,7 +510,7 @@ func (c *Client) Health() (*api.HealthResponse, error) { retry: resp, err := c.client.Get(u.String()) if err != nil { - return nil, errors.Wrapf(err, "client GET %s failed", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Health; client GET %s failed", u) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -521,7 +521,7 @@ retry: } var health api.HealthResponse if err := readJSON(resp.Body, &health); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Health; error reading %s", u) } return &health, nil } @@ -537,7 +537,7 @@ func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) { retry: resp, err := newInsecureClient().Get(u.String()) if err != nil { - return nil, errors.Wrapf(err, "client GET %s failed", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Root; client GET %s failed", u) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -548,12 +548,12 @@ retry: } var root api.RootResponse if err := readJSON(resp.Body, &root); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Root; error reading %s", u) } // verify the sha256 sum := sha256.Sum256(root.RootPEM.Raw) if sha256Sum != strings.ToLower(hex.EncodeToString(sum[:])) { - return nil, errors.New("root certificate SHA256 fingerprint do not match") + return nil, errs.BadRequest(errors.New("client.Root; root certificate SHA256 fingerprint do not match")) } return &root, nil } @@ -564,13 +564,13 @@ func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) { var retried bool body, err := json.Marshal(req) if err != nil { - return nil, errors.Wrap(err, "error marshaling request") + return nil, errs.Wrap(http.StatusInternalServerError, err, "client.Sign; error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/sign"}) retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { - return nil, errors.Wrapf(err, "client POST %s failed", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Sign; client POST %s failed", u) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -581,7 +581,7 @@ retry: } var sign api.SignResponse if err := readJSON(resp.Body, &sign); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Sign; error reading %s", u) } // Add tls.ConnectionState: // We'll extract the root certificate from the verified chains @@ -598,7 +598,7 @@ func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) { retry: resp, err := client.Post(u.String(), "application/json", http.NoBody) if err != nil { - return nil, errors.Wrapf(err, "client POST %s failed", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Renew; client POST %s failed", u) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -609,7 +609,7 @@ retry: } var sign api.SignResponse if err := readJSON(resp.Body, &sign); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Renew; error reading %s", u) } return &sign, nil } @@ -1008,13 +1008,13 @@ func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse var retried bool body, err := json.Marshal(req) if err != nil { - return nil, errors.Wrap(err, "error marshaling request") + return nil, errors.Wrap(err, "client.SSHBastion; error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/bastion"}) retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { - return nil, errors.Wrapf(err, "client POST %s failed", u) + return nil, errors.Wrapf(err, "client.SSHBastion; client POST %s failed", u) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -1025,7 +1025,7 @@ retry: } var bastion api.SSHBastionResponse if err := readJSON(resp.Body, &bastion); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) + return nil, errors.Wrapf(err, "client.SSHBastion; error reading %s", u) } return &bastion, nil } diff --git a/ca/client_test.go b/ca/client_test.go index c2e0063e..5b74f5cb 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -16,12 +16,12 @@ import ( "testing" "time" - "github.com/smallstep/certificates/errs" - + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/x509util" "golang.org/x/crypto/ssh" ) @@ -154,18 +154,17 @@ func equalJSON(t *testing.T, a interface{}, b interface{}) bool { func TestClient_Version(t *testing.T) { ok := &api.VersionResponse{Version: "test"} - internal := errs.InternalServerError(fmt.Errorf("Internal Server Error")) - notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string response interface{} responseCode int wantErr bool + expectedErr error }{ - {"ok", ok, 200, false}, - {"500", internal, 500, true}, - {"404", notFound, 404, true}, + {"ok", ok, 200, false, nil}, + {"500", errs.InternalServerError(errors.New("force")), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)}, + {"404", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -185,7 +184,6 @@ func TestClient_Version(t *testing.T) { got, err := c.Version() if (err != nil) != tt.wantErr { - fmt.Printf("%+v", err) t.Errorf("Client.Version() error = %v, wantErr %v", err, tt.wantErr) return } @@ -195,9 +193,7 @@ func TestClient_Version(t *testing.T) { if got != nil { t.Errorf("Client.Version() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Version() error = %v, want %v", err, tt.response) - } + assert.HasPrefix(t, tt.expectedErr.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Version() = %v, want %v", got, tt.response) @@ -209,16 +205,16 @@ func TestClient_Version(t *testing.T) { func TestClient_Health(t *testing.T) { ok := &api.HealthResponse{Status: "ok"} - nok := errs.InternalServerError(fmt.Errorf("Internal Server Error")) tests := []struct { name string response interface{} responseCode int wantErr bool + expectedErr error }{ - {"ok", ok, 200, false}, - {"not ok", nok, 500, true}, + {"ok", ok, 200, false, nil}, + {"not ok", errs.InternalServerError(errors.New("force")), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -248,9 +244,7 @@ func TestClient_Health(t *testing.T) { if got != nil { t.Errorf("Client.Health() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Health() error = %v, want %v", err, tt.response) - } + assert.HasPrefix(t, tt.expectedErr.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Health() = %v, want %v", got, tt.response) @@ -264,7 +258,6 @@ func TestClient_Root(t *testing.T) { ok := &api.RootResponse{ RootPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, } - notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string @@ -272,9 +265,10 @@ func TestClient_Root(t *testing.T) { response interface{} responseCode int wantErr bool + expectedErr error }{ - {"ok", "a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d", ok, 200, false}, - {"not found", "invalid", notFound, 404, true}, + {"ok", "a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d", ok, 200, false, nil}, + {"not found", "invalid", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -307,9 +301,7 @@ func TestClient_Root(t *testing.T) { if got != nil { t.Errorf("Client.Root() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Root() error = %v, want %v", err, tt.response) - } + assert.HasPrefix(t, tt.expectedErr.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Root() = %v, want %v", got, tt.response) @@ -334,8 +326,6 @@ func TestClient_Sign(t *testing.T) { NotBefore: api.NewTimeDuration(time.Now()), NotAfter: api.NewTimeDuration(time.Now().AddDate(0, 1, 0)), } - unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -343,11 +333,12 @@ func TestClient_Sign(t *testing.T) { response interface{} responseCode int wantErr bool + expectedErr error }{ - {"ok", request, ok, 200, false}, - {"unauthorized", request, unauthorized, 401, true}, - {"empty request", &api.SignRequest{}, badRequest, 403, true}, - {"nil request", nil, badRequest, 403, true}, + {"ok", request, ok, 200, false, nil}, + {"unauthorized", request, errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"empty request", &api.SignRequest{}, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"nil request", nil, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -364,7 +355,9 @@ func TestClient_Sign(t *testing.T) { srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { body := new(api.SignRequest) if err := api.ReadJSON(req.Body, body); err != nil { - api.WriteError(w, badRequest) + e, ok := tt.response.(error) + assert.Fatal(t, ok, "response expected to be error type") + api.WriteError(w, e) return } else if !equalJSON(t, body, tt.request) { if tt.request == nil { @@ -390,9 +383,7 @@ func TestClient_Sign(t *testing.T) { if got != nil { t.Errorf("Client.Sign() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Sign() error = %v, want %v", err, tt.response) - } + assert.HasPrefix(t, tt.expectedErr.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Sign() = %v, want %v", got, tt.response) @@ -409,19 +400,17 @@ func TestClient_Revoke(t *testing.T) { OTT: "the-ott", ReasonCode: 4, } - unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) - tests := []struct { name string request *api.RevokeRequest response interface{} responseCode int wantErr bool + expectedErr error }{ - {"ok", request, ok, 200, false}, - {"unauthorized", request, unauthorized, 401, true}, - {"nil request", nil, badRequest, 403, true}, + {"ok", request, ok, 200, false, nil}, + {"unauthorized", request, errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"nil request", nil, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -438,7 +427,9 @@ func TestClient_Revoke(t *testing.T) { srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { body := new(api.RevokeRequest) if err := api.ReadJSON(req.Body, body); err != nil { - api.WriteError(w, badRequest) + e, ok := tt.response.(error) + assert.Fatal(t, ok, "response expected to be error type") + api.WriteError(w, e) return } else if !equalJSON(t, body, tt.request) { if tt.request == nil { @@ -464,9 +455,7 @@ func TestClient_Revoke(t *testing.T) { if got != nil { t.Errorf("Client.Revoke() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Revoke() error = %v, want %v", err, tt.response) - } + assert.HasPrefix(t, tt.expectedErr.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Revoke() = %v, want %v", got, tt.response) @@ -485,19 +474,18 @@ func TestClient_Renew(t *testing.T) { {Certificate: parseCertificate(rootPEM)}, }, } - unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string response interface{} responseCode int wantErr bool + err error }{ - {"ok", ok, 200, false}, - {"unauthorized", unauthorized, 401, true}, - {"empty request", badRequest, 403, true}, - {"nil request", badRequest, 403, true}, + {"ok", ok, 200, false, nil}, + {"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"empty request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"nil request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -527,9 +515,11 @@ func TestClient_Renew(t *testing.T) { if got != nil { t.Errorf("Client.Renew() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Renew() error = %v, want %v", err, tt.response) - } + + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.responseCode) + assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Renew() = %v, want %v", got, tt.response) @@ -589,9 +579,7 @@ func TestClient_Provisioners(t *testing.T) { if got != nil { t.Errorf("Client.Provisioners() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Provisioners() error = %v, want %v", err, tt.response) - } + assert.HasPrefix(t, errs.InternalServerErrorDefaultMsg, err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Provisioners() = %v, want %v", got, tt.response) @@ -605,7 +593,6 @@ func TestClient_ProvisionerKey(t *testing.T) { ok := &api.ProvisionerKeyResponse{ Key: "an encrypted key", } - notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string @@ -613,9 +600,10 @@ func TestClient_ProvisionerKey(t *testing.T) { response interface{} responseCode int wantErr bool + err error }{ - {"ok", "kid", ok, 200, false}, - {"fail", "invalid", notFound, 500, true}, + {"ok", "kid", ok, 200, false, nil}, + {"fail", "invalid", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -648,9 +636,11 @@ func TestClient_ProvisionerKey(t *testing.T) { if got != nil { t.Errorf("Client.ProvisionerKey() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.ProvisionerKey() error = %v, want %v", err, tt.response) - } + + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.responseCode) + assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.ProvisionerKey() = %v, want %v", got, tt.response) @@ -666,19 +656,17 @@ func TestClient_Roots(t *testing.T) { {Certificate: parseCertificate(rootPEM)}, }, } - unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string response interface{} responseCode int wantErr bool + err error }{ - {"ok", ok, 200, false}, - {"unauthorized", unauthorized, 401, true}, - {"empty request", badRequest, 403, true}, - {"nil request", badRequest, 403, true}, + {"ok", ok, 200, false, nil}, + {"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"bad-request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -708,9 +696,10 @@ func TestClient_Roots(t *testing.T) { if got != nil { t.Errorf("Client.Roots() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Roots() error = %v, want %v", err, tt.response) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.responseCode) + assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Roots() = %v, want %v", got, tt.response) @@ -726,19 +715,16 @@ func TestClient_Federation(t *testing.T) { {Certificate: parseCertificate(rootPEM)}, }, } - unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized")) - badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string response interface{} responseCode int wantErr bool + err error }{ - {"ok", ok, 200, false}, - {"unauthorized", unauthorized, 401, true}, - {"empty request", badRequest, 403, true}, - {"nil request", badRequest, 403, true}, + {"ok", ok, 200, false, nil}, + {"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -768,9 +754,10 @@ func TestClient_Federation(t *testing.T) { if got != nil { t.Errorf("Client.Federation() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.Federation() error = %v, want %v", err, tt.response) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.responseCode) + assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.Federation() = %v, want %v", got, tt.response) @@ -790,16 +777,16 @@ func TestClient_SSHRoots(t *testing.T) { HostKeys: []api.SSHPublicKey{{PublicKey: key}}, UserKeys: []api.SSHPublicKey{{PublicKey: key}}, } - notFound := errs.NotFound(fmt.Errorf("Not Found")) tests := []struct { name string response interface{} responseCode int wantErr bool + err error }{ - {"ok", ok, 200, false}, - {"not found", notFound, 404, true}, + {"ok", ok, 200, false, nil}, + {"not found", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -829,9 +816,10 @@ func TestClient_SSHRoots(t *testing.T) { if got != nil { t.Errorf("Client.SSHKeys() = %v, want nil", got) } - if !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.SSHKeys() error = %v, want %v", err, tt.response) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.responseCode) + assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { t.Errorf("Client.SSHKeys() = %v, want %v", got, tt.response) @@ -948,7 +936,6 @@ func TestClient_SSHBastion(t *testing.T) { Hostname: "bastion.local", }, } - badRequest := errs.BadRequest(fmt.Errorf("Bad Request")) tests := []struct { name string @@ -956,11 +943,11 @@ func TestClient_SSHBastion(t *testing.T) { response interface{} responseCode int wantErr bool + err error }{ - {"ok", &api.SSHBastionRequest{Hostname: "host.local"}, ok, 200, false}, - {"bad response", &api.SSHBastionRequest{Hostname: "host.local"}, "bad json", 200, true}, - {"empty request", &api.SSHBastionRequest{}, badRequest, 403, true}, - {"nil request", nil, badRequest, 403, true}, + {"ok", &api.SSHBastionRequest{Hostname: "host.local"}, ok, 200, false, nil}, + {"bad-response", &api.SSHBastionRequest{Hostname: "host.local"}, "bad json", 200, true, nil}, + {"bad-request", &api.SSHBastionRequest{}, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -990,8 +977,11 @@ func TestClient_SSHBastion(t *testing.T) { if got != nil { t.Errorf("Client.SSHBastion() = %v, want nil", got) } - if tt.responseCode != 200 && !reflect.DeepEqual(err, tt.response) { - t.Errorf("Client.SSHBastion() error = %v, want %v", err, tt.response) + if tt.responseCode != 200 { + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tt.responseCode) + assert.HasPrefix(t, tt.err.Error(), err.Error()) } default: if !reflect.DeepEqual(got, tt.response) { diff --git a/ca/identity/identity_test.go b/ca/identity/identity_test.go index 3c04f982..139c6917 100644 --- a/ca/identity/identity_test.go +++ b/ca/identity/identity_test.go @@ -276,6 +276,7 @@ func TestIdentity_Renew(t *testing.T) { } oldIdentityDir := identityDir + identityDir = "testdata/identity" defer func() { identityDir = oldIdentityDir os.RemoveAll(tmpDir) diff --git a/db/db.go b/db/db.go index 8753cc1a..0934bffa 100644 --- a/db/db.go +++ b/db/db.go @@ -270,6 +270,105 @@ func (db *DB) Shutdown() error { return nil } +// MockAuthDB mocks the AuthDB interface. // +type MockAuthDB struct { + Err error + Ret1 interface{} + MIsRevoked func(string) (bool, error) + MIsSSHRevoked func(string) (bool, error) + MRevoke func(rci *RevokedCertificateInfo) error + MRevokeSSH func(rci *RevokedCertificateInfo) error + MStoreCertificate func(crt *x509.Certificate) error + MUseToken func(id, tok string) (bool, error) + MIsSSHHost func(principal string) (bool, error) + MStoreSSHCertificate func(crt *ssh.Certificate) error + MGetSSHHostPrincipals func() ([]string, error) + MShutdown func() error +} + +// IsRevoked mock. +func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { + if m.MIsRevoked != nil { + return m.MIsRevoked(sn) + } + return m.Ret1.(bool), m.Err +} + +// IsSSHRevoked mock. +func (m *MockAuthDB) IsSSHRevoked(sn string) (bool, error) { + if m.MIsSSHRevoked != nil { + return m.MIsSSHRevoked(sn) + } + return m.Ret1.(bool), m.Err +} + +// UseToken mock. +func (m *MockAuthDB) UseToken(id, tok string) (bool, error) { + if m.MUseToken != nil { + return m.MUseToken(id, tok) + } + if m.Ret1 == nil { + return false, m.Err + } + return m.Ret1.(bool), m.Err +} + +// Revoke mock. +func (m *MockAuthDB) Revoke(rci *RevokedCertificateInfo) error { + if m.MRevoke != nil { + return m.MRevoke(rci) + } + return m.Err +} + +// RevokeSSH mock. +func (m *MockAuthDB) RevokeSSH(rci *RevokedCertificateInfo) error { + if m.MRevokeSSH != nil { + return m.MRevokeSSH(rci) + } + return m.Err +} + +// StoreCertificate mock. +func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error { + if m.MStoreCertificate != nil { + return m.MStoreCertificate(crt) + } + return m.Err +} + +// IsSSHHost mock. +func (m *MockAuthDB) IsSSHHost(principal string) (bool, error) { + if m.MIsSSHHost != nil { + return m.MIsSSHHost(principal) + } + return m.Ret1.(bool), m.Err +} + +// StoreSSHCertificate mock. +func (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error { + if m.MStoreSSHCertificate != nil { + return m.MStoreSSHCertificate(crt) + } + return m.Err +} + +// GetSSHHostPrincipals mock. +func (m *MockAuthDB) GetSSHHostPrincipals() ([]string, error) { + if m.MGetSSHHostPrincipals != nil { + return m.MGetSSHHostPrincipals() + } + return m.Ret1.([]string), m.Err +} + +// Shutdown mock. +func (m *MockAuthDB) Shutdown() error { + if m.MShutdown != nil { + return m.MShutdown() + } + return m.Err +} + // MockNoSQLDB // type MockNoSQLDB struct { Err error diff --git a/errs/error.go b/errs/error.go index 825cf549..adae017e 100644 --- a/errs/error.go +++ b/errs/error.go @@ -21,9 +21,9 @@ type StackTracer interface { // Option modifies the Error type. type Option func(e *Error) error -// WithMessage returns an Option that modifies the error by overwriting the +// withDefaultMessage returns an Option that modifies the error by overwriting the // message only if it is empty. -func WithMessage(format string, args ...interface{}) Option { +func withDefaultMessage(format string, args ...interface{}) Option { return func(e *Error) error { if len(e.Msg) > 0 { return e @@ -33,25 +33,52 @@ func WithMessage(format string, args ...interface{}) Option { } } +// WithMessage returns an Option that modifies the error by overwriting the +// message only if it is empty. +func WithMessage(format string, args ...interface{}) Option { + return func(e *Error) error { + e.Msg = fmt.Sprintf(format, args...) + return e + } +} + +// WithKeyVal returns an Option that adds the given key-value pair to the +// Error details. This is helpful for debugging errors. +func WithKeyVal(key string, val interface{}) Option { + return func(e *Error) error { + if e.Details == nil { + e.Details = make(map[string]interface{}) + } + e.Details[key] = val + return e + } +} + // Error represents the CA API errors. type Error struct { - Status int - Err error - Msg string + Status int + Err error + Msg string + Details map[string]interface{} } // New returns a new Error. If the given error implements the StatusCoder // interface we will ignore the given status. func New(status int, err error, opts ...Option) error { - var e *Error - if sc, ok := err.(StatusCoder); ok { - e = &Error{Status: sc.StatusCode(), Err: err} - } else { - cause := errors.Cause(err) - if sc, ok := cause.(StatusCoder); ok { + var ( + e *Error + ok bool + ) + if e, ok = err.(*Error); !ok { + if sc, ok := err.(StatusCoder); ok { e = &Error{Status: sc.StatusCode(), Err: err} } else { - e = &Error{Status: status, Err: err} + cause := errors.Cause(err) + if sc, ok := cause.(StatusCoder); ok { + e = &Error{Status: sc.StatusCode(), Err: err} + } else { + e = &Error{Status: status, Err: err} + } } } for _, o := range opts { @@ -188,63 +215,62 @@ func StatusCodeError(code int, e error, opts ...Option) error { } } -var seeLogs = "Please see the certificate authority logs for more info." +var ( + seeLogs = "Please see the certificate authority logs for more info." + // BadRequestDefaultMsg 400 default msg + BadRequestDefaultMsg = "The request could not be completed due to being poorly formatted or missing critical data. " + seeLogs + // UnauthorizedDefaultMsg 401 default msg + UnauthorizedDefaultMsg = "The request lacked necessary authorization to be completed. " + seeLogs + // ForbiddenDefaultMsg 403 default msg + ForbiddenDefaultMsg = "The request was forbidden by the certificate authority. " + seeLogs + // NotFoundDefaultMsg 404 default msg + NotFoundDefaultMsg = "The requested resource could not be found. " + seeLogs + // InternalServerErrorDefaultMsg 500 default msg + InternalServerErrorDefaultMsg = "The certificate authority encountered an Internal Server Error. " + seeLogs + // NotImplementedDefaultMsg 501 default msg + NotImplementedDefaultMsg = "The requested method is not implemented by the certificate authority. " + seeLogs +) // InternalServerError returns a 500 error with the given error. func InternalServerError(err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The certificate authority encountered an Internal Server Error. "+seeLogs)) - } + opts = append(opts, withDefaultMessage(InternalServerErrorDefaultMsg)) return New(http.StatusInternalServerError, err, opts...) } // NotImplemented returns a 501 error with the given error. func NotImplemented(err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The requested method is not implemented by the certificate authority. "+seeLogs)) - } + opts = append(opts, withDefaultMessage(NotImplementedDefaultMsg)) return New(http.StatusNotImplemented, err, opts...) } // BadRequest returns an 400 error with the given error. func BadRequest(err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The request could not be completed due to being poorly formatted or "+ - "missing critical data. "+seeLogs)) - } + opts = append(opts, withDefaultMessage(BadRequestDefaultMsg)) return New(http.StatusBadRequest, err, opts...) } // Unauthorized returns an 401 error with the given error. func Unauthorized(err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The request lacked necessary authorization to be completed. "+seeLogs)) - } + opts = append(opts, withDefaultMessage(UnauthorizedDefaultMsg)) return New(http.StatusUnauthorized, err, opts...) } // Forbidden returns an 403 error with the given error. func Forbidden(err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The request was Forbidden by the certificate authority. "+seeLogs)) - } + opts = append(opts, withDefaultMessage(ForbiddenDefaultMsg)) return New(http.StatusForbidden, err, opts...) } // NotFound returns an 404 error with the given error. func NotFound(err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The requested resource could not be found. "+seeLogs)) - } + opts = append(opts, withDefaultMessage(NotFoundDefaultMsg)) return New(http.StatusNotFound, err, opts...) } // UnexpectedError will be used when the certificate authority makes an outgoing // request and receives an unhandled status code. func UnexpectedError(code int, err error, opts ...Option) error { - if len(opts) == 0 { - opts = append(opts, WithMessage("The certificate authority received an "+ - "unexpected HTTP status code - '%d'. "+seeLogs, code)) - } + opts = append(opts, withDefaultMessage("The certificate authority received an "+ + "unexpected HTTP status code - '%d'. "+seeLogs, code)) return New(code, err, opts...) } diff --git a/api/errors_test.go b/errs/errors_test.go similarity index 87% rename from api/errors_test.go rename to errs/errors_test.go index 1f63142a..58b95437 100644 --- a/api/errors_test.go +++ b/errs/errors_test.go @@ -1,11 +1,9 @@ -package api +package errs import ( "fmt" "reflect" "testing" - - "github.com/smallstep/certificates/errs" ) func TestError_MarshalJSON(t *testing.T) { @@ -24,7 +22,7 @@ func TestError_MarshalJSON(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := &errs.Error{ + e := &Error{ Status: tt.fields.Status, Err: tt.fields.Err, } @@ -47,15 +45,15 @@ func TestError_UnmarshalJSON(t *testing.T) { tests := []struct { name string args args - expected *errs.Error + expected *Error wantErr bool }{ - {"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &errs.Error{Status: 400, Err: fmt.Errorf("bad request")}, false}, - {"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &errs.Error{}, true}, + {"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &Error{Status: 400, Err: fmt.Errorf("bad request")}, false}, + {"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &Error{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e := new(errs.Error) + e := new(Error) if err := e.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr { t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } From b26587705025ed7823a2f002a2cc0d9fa669860a Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 23 Jan 2020 22:04:34 -0800 Subject: [PATCH 149/163] Simplify statuscoder error generators. --- .golangci.yml | 1 + api/api.go | 12 +- api/api_test.go | 8 +- api/renew.go | 5 +- api/revoke.go | 17 +- api/revoke_test.go | 2 +- api/sign.go | 15 +- api/ssh.go | 46 ++-- api/sshRekey.go | 14 +- api/sshRenew.go | 12 +- api/sshRevoke.go | 15 +- api/utils.go | 3 +- authority/authorize.go | 25 ++- authority/authorize_test.go | 6 +- authority/provisioner/acme.go | 2 +- authority/provisioner/aws.go | 28 +-- authority/provisioner/aws_test.go | 2 +- authority/provisioner/azure.go | 22 +- authority/provisioner/azure_test.go | 2 +- authority/provisioner/gcp.go | 30 +-- authority/provisioner/gcp_test.go | 2 +- authority/provisioner/jwk.go | 18 +- authority/provisioner/jwk_test.go | 4 +- authority/provisioner/k8sSA.go | 14 +- authority/provisioner/k8sSA_test.go | 4 +- authority/provisioner/noop_test.go | 4 +- authority/provisioner/oidc.go | 24 +-- authority/provisioner/provisioner.go | 14 +- authority/provisioner/sign_ssh_options.go | 60 +++--- .../provisioner/sign_ssh_options_test.go | 10 +- authority/provisioner/ssh_test.go | 12 +- authority/provisioner/sshpop.go | 28 +-- authority/provisioner/sshpop_test.go | 4 +- authority/provisioner/x5c.go | 20 +- authority/provisioner/x5c_test.go | 12 +- authority/provisioners.go | 12 +- authority/provisioners_test.go | 28 ++- authority/root.go | 12 +- authority/root_test.go | 24 +-- authority/ssh.go | 72 +++---- authority/ssh_test.go | 14 +- authority/tls.go | 16 +- ca/client.go | 10 +- ca/client_test.go | 40 ++-- errs/error.go | 197 ++++++++++++------ 45 files changed, 482 insertions(+), 440 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f0c2eed0..0aed855d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -63,6 +63,7 @@ issues: - declaration of "err" shadows declaration at line - should have a package comment, unless it's in another file for this package - error strings should not be capitalized or end with punctuation or a newline + - Wrapf call needs 1 arg but has 2 args # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration service: diff --git a/api/api.go b/api/api.go index c4b307b3..37222be8 100644 --- a/api/api.go +++ b/api/api.go @@ -295,7 +295,7 @@ func (h *caHandler) Root(w http.ResponseWriter, r *http.Request) { // Load root certificate with the cert, err := h.Authority.Root(sum) if err != nil { - WriteError(w, errs.NotFound(errors.Wrapf(err, "%s was not found", r.RequestURI))) + WriteError(w, errs.Wrapf(http.StatusNotFound, err, "%s was not found", r.RequestURI)) return } @@ -314,13 +314,13 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate { func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := parseCursor(r) if err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } p, next, err := h.Authority.GetProvisioners(cursor, limit) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } JSON(w, &ProvisionersResponse{ @@ -334,7 +334,7 @@ func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) { kid := chi.URLParam(r, "kid") key, err := h.Authority.GetEncryptedKey(kid) if err != nil { - WriteError(w, errs.NotFound(err)) + WriteError(w, errs.NotFoundErr(err)) return } JSON(w, &ProvisionerKeyResponse{key}) @@ -344,7 +344,7 @@ func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) { func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { roots, err := h.Authority.GetRoots() if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } @@ -362,7 +362,7 @@ func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { func (h *caHandler) Federation(w http.ResponseWriter, r *http.Request) { federated, err := h.Authority.GetFederation() if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } diff --git a/api/api_test.go b/api/api_test.go index 9f40a8e0..cbaf806f 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -915,7 +915,7 @@ func Test_caHandler_Renew(t *testing.T) { {"ok", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated}, {"no tls", nil, nil, nil, nil, http.StatusBadRequest}, {"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest}, - {"renew error", cs, nil, nil, errs.Forbidden(fmt.Errorf("an error")), http.StatusForbidden}, + {"renew error", cs, nil, nil, errs.Forbidden("an error"), http.StatusForbidden}, } expected := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) @@ -1010,10 +1010,10 @@ func Test_caHandler_Provisioners(t *testing.T) { t.Fatal(err) } - expectedError400 := errs.BadRequest(errors.New("force")) + expectedError400 := errs.BadRequest("force") expectedError400Bytes, err := json.Marshal(expectedError400) assert.FatalError(t, err) - expectedError500 := errs.InternalServerError(errors.New("force")) + expectedError500 := errs.InternalServer("force") expectedError500Bytes, err := json.Marshal(expectedError500) assert.FatalError(t, err) for _, tt := range tests { @@ -1082,7 +1082,7 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { } expected := []byte(`{"key":"` + privKey + `"}`) - expectedError404 := errs.NotFound(errors.New("force")) + expectedError404 := errs.NotFound("force") expectedError404Bytes, err := json.Marshal(expectedError404) assert.FatalError(t, err) diff --git a/api/renew.go b/api/renew.go index bc42ec24..bf32518b 100644 --- a/api/renew.go +++ b/api/renew.go @@ -3,7 +3,6 @@ package api import ( "net/http" - "github.com/pkg/errors" "github.com/smallstep/certificates/errs" ) @@ -11,7 +10,7 @@ import ( // new one. func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { - WriteError(w, errs.BadRequest(errors.New("missing peer certificate"))) + WriteError(w, errs.BadRequest("missing peer certificate")) return } @@ -22,7 +21,7 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { } certChainPEM := certChainToPEM(certChain) var caPEM Certificate - if len(certChainPEM) > 0 { + if len(certChainPEM) > 1 { caPEM = certChainPEM[1] } diff --git a/api/revoke.go b/api/revoke.go index df974cbe..547ed366 100644 --- a/api/revoke.go +++ b/api/revoke.go @@ -4,7 +4,6 @@ import ( "context" "net/http" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" @@ -30,13 +29,13 @@ type RevokeRequest struct { // or an error if something is wrong. func (r *RevokeRequest) Validate() (err error) { if r.Serial == "" { - return errs.BadRequest(errors.New("missing serial")) + return errs.BadRequest("missing serial") } if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise { - return errs.BadRequest(errors.New("reasonCode out of bounds")) + return errs.BadRequest("reasonCode out of bounds") } if !r.Passive { - return errs.NotImplemented(errors.New("non-passive revocation not implemented")) + return errs.NotImplemented("non-passive revocation not implemented") } return @@ -50,7 +49,7 @@ func (r *RevokeRequest) Validate() (err error) { func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { var body RevokeRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } @@ -72,7 +71,7 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { if len(body.OTT) > 0 { logOtt(w, body.OTT) if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } opts.OTT = body.OTT @@ -81,12 +80,12 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { // the client certificate Serial Number must match the serial number // being revoked. if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { - WriteError(w, errs.BadRequest(errors.New("missing ott or peer certificate"))) + WriteError(w, errs.BadRequest("missing ott or peer certificate")) return } opts.Crt = r.TLS.PeerCertificates[0] if opts.Crt.SerialNumber.String() != opts.Serial { - WriteError(w, errs.BadRequest(errors.New("revoke: serial number in mtls certificate different than body"))) + WriteError(w, errs.BadRequest("revoke: serial number in mtls certificate different than body")) return } // TODO: should probably be checking if the certificate was revoked here. @@ -97,7 +96,7 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { } if err := h.Authority.Revoke(ctx, opts); err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } diff --git a/api/revoke_test.go b/api/revoke_test.go index e6aef11a..f44acebf 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -190,7 +190,7 @@ func Test_caHandler_Revoke(t *testing.T) { return nil, nil }, revoke: func(ctx context.Context, opts *authority.RevokeOptions) error { - return errs.InternalServerError(errors.New("force")) + return errs.InternalServer("force") }, }, } diff --git a/api/sign.go b/api/sign.go index e76f6256..f30b0b4b 100644 --- a/api/sign.go +++ b/api/sign.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "net/http" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/tlsutil" @@ -22,13 +21,13 @@ type SignRequest struct { // or an error if something is wrong. func (s *SignRequest) Validate() error { if s.CsrPEM.CertificateRequest == nil { - return errs.BadRequest(errors.New("missing csr")) + return errs.BadRequest("missing csr") } if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil { - return errs.BadRequest(errors.Wrap(err, "invalid csr")) + return errs.Wrap(http.StatusBadRequest, err, "invalid csr") } if s.OTT == "" { - return errs.BadRequest(errors.New("missing ott")) + return errs.BadRequest("missing ott") } return nil @@ -49,7 +48,7 @@ type SignResponse struct { func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { var body SignRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } @@ -66,18 +65,18 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { signOpts, err := h.Authority.AuthorizeSign(body.OTT) if err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } certChainPEM := certChainToPEM(certChain) var caPEM Certificate - if len(certChainPEM) > 0 { + if len(certChainPEM) > 1 { caPEM = certChainPEM[1] } logCertificate(w, certChain[0]) diff --git a/api/ssh.go b/api/ssh.go index 2206973b..f0b090d1 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -249,19 +249,19 @@ type SSHBastionResponse struct { func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { var body SSHSignRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } logOtt(w, body.OTT) if err := body.Validate(); err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } publicKey, err := ssh.ParsePublicKey(body.PublicKey) if err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error parsing publicKey"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error parsing publicKey")) return } @@ -269,7 +269,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { if body.AddUserPublicKey != nil { addUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey) if err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error parsing addUserPublicKey"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error parsing addUserPublicKey")) return } } @@ -285,13 +285,13 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } cert, err := h.Authority.SignSSH(publicKey, opts, signOpts...) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } @@ -299,7 +299,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { if addUserPublicKey != nil && cert.CertType == ssh.UserCert && len(cert.ValidPrincipals) == 1 { addUserCert, err := h.Authority.SignSSHAddUser(addUserPublicKey, cert) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } addUserCertificate = &SSHCertificate{addUserCert} @@ -320,12 +320,12 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } certChain, err := h.Authority.Sign(cr, opts, signOpts...) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } identityCertificate = certChainToPEM(certChain) @@ -343,12 +343,12 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHRoots() if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { - WriteError(w, errs.NotFound(errors.New("no keys found"))) + WriteError(w, errs.NotFound("no keys found")) return } @@ -368,12 +368,12 @@ func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHFederation() if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { - WriteError(w, errs.NotFound(errors.New("no keys found"))) + WriteError(w, errs.NotFound("no keys found")) return } @@ -393,17 +393,17 @@ func (h *caHandler) SSHFederation(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { var body SSHConfigRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } if err := body.Validate(); err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } ts, err := h.Authority.GetSSHConfig(body.Type, body.Data) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } @@ -414,7 +414,7 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { case provisioner.SSHHostCert: config.HostTemplates = ts default: - WriteError(w, errs.InternalServerError(errors.New("it should hot get here"))) + WriteError(w, errs.InternalServer("it should hot get here")) return } @@ -429,13 +429,13 @@ func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) { return } if err := body.Validate(); err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } exists, err := h.Authority.CheckSSHHost(r.Context(), body.Principal, body.Token) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } JSON(w, &SSHCheckPrincipalResponse{ @@ -452,7 +452,7 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { hosts, err := h.Authority.GetSSHHosts(cert) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } JSON(w, &SSHGetHostsResponse{ @@ -464,17 +464,17 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) { var body SSHBastionRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } if err := body.Validate(); err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } bastion, err := h.Authority.GetSSHBastion(body.User, body.Hostname) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) return } diff --git a/api/sshRekey.go b/api/sshRekey.go index efeee141..a5cc1f06 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -40,42 +40,42 @@ type SSHRekeyResponse struct { func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { var body SSHRekeyRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } logOtt(w, body.OTT) if err := body.Validate(); err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } publicKey, err := ssh.ParsePublicKey(body.PublicKey) if err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error parsing publicKey"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error parsing publicKey")) return } ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod) signOpts, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) } newCert, err := h.Authority.RekeySSH(oldCert, publicKey, signOpts...) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } identity, err := h.renewIdentityCertificate(r) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } diff --git a/api/sshRenew.go b/api/sshRenew.go index fd4ff1ee..11a9d8e8 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -36,36 +36,36 @@ type SSHRenewResponse struct { func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { var body SSHRenewRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } logOtt(w, body.OTT) if err := body.Validate(); err != nil { - WriteError(w, errs.BadRequest(err)) + WriteError(w, errs.BadRequestErr(err)) return } ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod) _, err := h.Authority.Authorize(ctx, body.OTT) if err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT) if err != nil { - WriteError(w, errs.InternalServerError(err)) + WriteError(w, errs.InternalServerErr(err)) } newCert, err := h.Authority.RenewSSH(oldCert) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } identity, err := h.renewIdentityCertificate(r) if err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } diff --git a/api/sshRevoke.go b/api/sshRevoke.go index cd4a3a3e..b8d1dadd 100644 --- a/api/sshRevoke.go +++ b/api/sshRevoke.go @@ -4,7 +4,6 @@ import ( "context" "net/http" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" @@ -30,16 +29,16 @@ type SSHRevokeRequest struct { // or an error if something is wrong. func (r *SSHRevokeRequest) Validate() (err error) { if r.Serial == "" { - return errs.BadRequest(errors.New("missing serial")) + return errs.BadRequest("missing serial") } if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise { - return errs.BadRequest(errors.New("reasonCode out of bounds")) + return errs.BadRequest("reasonCode out of bounds") } if !r.Passive { - return errs.NotImplemented(errors.New("non-passive revocation not implemented")) + return errs.NotImplemented("non-passive revocation not implemented") } if len(r.OTT) == 0 { - return errs.BadRequest(errors.New("missing ott")) + return errs.BadRequest("missing ott") } return } @@ -50,7 +49,7 @@ func (r *SSHRevokeRequest) Validate() (err error) { func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { var body SSHRevokeRequest if err := ReadJSON(r.Body, &body); err != nil { - WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body"))) + WriteError(w, errs.Wrap(http.StatusBadRequest, err, "error reading request body")) return } @@ -71,13 +70,13 @@ func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) { // otherwise it is assumed that the certificate is revoking itself over mTLS. logOtt(w, body.OTT) if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil { - WriteError(w, errs.Unauthorized(err)) + WriteError(w, errs.UnauthorizedErr(err)) return } opts.OTT = body.OTT if err := h.Authority.Revoke(ctx, opts); err != nil { - WriteError(w, errs.Forbidden(err)) + WriteError(w, errs.ForbiddenErr(err)) return } diff --git a/api/utils.go b/api/utils.go index 56beb2b5..0d87a065 100644 --- a/api/utils.go +++ b/api/utils.go @@ -6,7 +6,6 @@ import ( "log" "net/http" - "github.com/pkg/errors" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" ) @@ -69,7 +68,7 @@ func JSONStatus(w http.ResponseWriter, v interface{}, status int) { // pointed by v. func ReadJSON(r io.Reader, v interface{}) error { if err := json.NewDecoder(r).Decode(v); err != nil { - return errs.BadRequest(errors.Wrap(err, "error decoding json")) + return errs.Wrap(http.StatusBadRequest, err, "error decoding json") } return nil } diff --git a/authority/authorize.go b/authority/authorize.go index cdca026d..bda59520 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -6,7 +6,6 @@ import ( "net/http" "strings" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/jose" @@ -58,15 +57,15 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision // This check is meant as a stopgap solution to the current lack of a persistence layer. if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck { if claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) { - return nil, errs.Unauthorized(errors.New("authority.authorizeToken: token issued before the bootstrap of certificate authority")) + return nil, errs.Unauthorized("authority.authorizeToken: token issued before the bootstrap of certificate authority") } } // This method will also validate the audiences for JWK provisioners. p, ok := a.provisioners.LoadByToken(tok, &claims.Claims) if !ok { - return nil, errs.Unauthorized(errors.Errorf("authority.authorizeToken: provisioner "+ - "not found or invalid audience (%s)", strings.Join(claims.Audience, ", "))) + return nil, errs.Unauthorized("authority.authorizeToken: provisioner "+ + "not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")) } // Store the token to protect against reuse unless it's skipped. @@ -78,7 +77,7 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision "authority.authorizeToken: failed when attempting to store token") } if !ok { - return nil, errs.Unauthorized(errors.Errorf("authority.authorizeToken: token already used")) + return nil, errs.Unauthorized("authority.authorizeToken: token already used") } } } @@ -89,7 +88,7 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision // Authorize grabs the method from the context and authorizes the request by // validating the one-time-token. func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner.SignOption, error) { - var opts = []errs.Option{errs.WithKeyVal("token", token)} + var opts = []interface{}{errs.WithKeyVal("token", token)} switch m := provisioner.MethodFromContext(ctx); m { case provisioner.SignMethod: @@ -99,13 +98,13 @@ func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner. return nil, errs.Wrap(http.StatusInternalServerError, a.authorizeRevoke(ctx, token), "authority.Authorize", opts...) case provisioner.SSHSignMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) + return nil, errs.NotImplemented("authority.Authorize; ssh certificate flows are not enabled", opts...) } _, err := a.authorizeSSHSign(ctx, token) return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) case provisioner.SSHRenewMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) + return nil, errs.NotImplemented("authority.Authorize; ssh certificate flows are not enabled", opts...) } _, err := a.authorizeSSHRenew(ctx, token) return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) @@ -113,12 +112,12 @@ func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner. return nil, errs.Wrap(http.StatusInternalServerError, a.authorizeSSHRevoke(ctx, token), "authority.Authorize", opts...) case provisioner.SSHRekeyMethod: if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) + return nil, errs.NotImplemented("authority.Authorize; ssh certificate flows are not enabled", opts...) } _, signOpts, err := a.authorizeSSHRekey(ctx, token) return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...) default: - return nil, errs.InternalServerError(errors.Errorf("authority.Authorize; method %d is not supported", m), opts...) + return nil, errs.InternalServer("authority.Authorize; method %d is not supported", append([]interface{}{m}, opts...)...) } } @@ -165,7 +164,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { // // TODO(mariano): should we authorize by default? func (a *Authority) authorizeRenew(cert *x509.Certificate) error { - var opts = []errs.Option{errs.WithKeyVal("serialNumber", cert.SerialNumber.String())} + var opts = []interface{}{errs.WithKeyVal("serialNumber", cert.SerialNumber.String())} // Check the passive revocation table. isRevoked, err := a.db.IsRevoked(cert.SerialNumber.String()) @@ -173,12 +172,12 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error { return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) } if isRevoked { - return errs.Unauthorized(errors.New("authority.authorizeRenew: certificate has been revoked"), opts...) + return errs.Unauthorized("authority.authorizeRenew: certificate has been revoked", opts...) } p, ok := a.provisioners.LoadByCertificate(cert) if !ok { - return errs.Unauthorized(errors.New("authority.authorizeRenew: provisioner not found"), opts...) + return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) } if err := p.AuthorizeRenew(context.Background(), cert); err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) diff --git a/authority/authorize_test.go b/authority/authorize_test.go index 6f7bf940..e4863764 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -180,7 +180,7 @@ func TestAuthority_authorizeToken(t *testing.T) { } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) - _, err = _a.authorizeToken(context.TODO(), raw) + _, err = _a.authorizeToken(context.Background(), raw) assert.FatalError(t, err) return &authorizeTest{ auth: _a, @@ -268,7 +268,7 @@ func TestAuthority_authorizeToken(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - p, err := tc.auth.authorizeToken(context.TODO(), tc.token) + p, err := tc.auth.authorizeToken(context.Background(), tc.token) if err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(errs.StatusCoder) @@ -355,7 +355,7 @@ func TestAuthority_authorizeRevoke(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - if err := tc.auth.authorizeRevoke(context.TODO(), tc.token); err != nil { + if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 7adeb311..e414410b 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -80,7 +80,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // certificate was configured to allow renewals. func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("acme.AuthorizeRenew; renew is disabled for acme provisioner %s", p.GetID())) + return errs.Unauthorized("acme.AuthorizeRenew; renew is disabled for acme provisioner %s", p.GetID()) } return nil } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 39769118..16820909 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -306,7 +306,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // certificate was configured to allow renewals. func (p *AWS) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("aws.AuthorizeRenew; renew is disabled for aws provisioner %s", p.GetID())) + return errs.Unauthorized("aws.AuthorizeRenew; renew is disabled for aws provisioner %s", p.GetID()) } return nil } @@ -353,7 +353,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { return nil, errs.Wrapf(http.StatusUnauthorized, err, "aws.authorizeToken; error parsing aws token") } if len(jwt.Headers) == 0 { - return nil, errs.InternalServerError(errors.New("aws.authorizeToken; error parsing token, header is missing")) + return nil, errs.InternalServer("aws.authorizeToken; error parsing token, header is missing") } var unsafeClaims awsPayload @@ -378,13 +378,13 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { switch { case doc.AccountID == "": - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document accountId cannot be empty")) + return nil, errs.Unauthorized("aws.authorizeToken; aws identity document accountId cannot be empty") case doc.InstanceID == "": - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document instanceId cannot be empty")) + return nil, errs.Unauthorized("aws.authorizeToken; aws identity document instanceId cannot be empty") case doc.PrivateIP == "": - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document privateIp cannot be empty")) + return nil, errs.Unauthorized("aws.authorizeToken; aws identity document privateIp cannot be empty") case doc.Region == "": - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document region cannot be empty")) + return nil, errs.Unauthorized("aws.authorizeToken; aws identity document region cannot be empty") } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -399,7 +399,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { // validate audiences with the defaults if !matchesAudience(payload.Audience, p.audiences.Sign) { - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid token - invalid audience claim (aud)")) + return nil, errs.Unauthorized("aws.authorizeToken; invalid token - invalid audience claim (aud)") } // Validate subject, it has to be known if disableCustomSANs is enabled @@ -407,7 +407,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { if payload.Subject != doc.InstanceID && payload.Subject != doc.PrivateIP && payload.Subject != fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) { - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid token - invalid subject claim (sub)")) + return nil, errs.Unauthorized("aws.authorizeToken; invalid token - invalid subject claim (sub)") } } @@ -421,14 +421,14 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { } } if !found { - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid aws identity document - accountId is not valid")) + return nil, errs.Unauthorized("aws.authorizeToken; invalid aws identity document - accountId is not valid") } } // validate instance age if d := p.InstanceAge.Value(); d > 0 { if now.Sub(doc.PendingTime) > d { - return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document pendingTime is too old")) + return nil, errs.Unauthorized("aws.authorizeToken; aws identity document pendingTime is too old") } } @@ -439,7 +439,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner %s", p.GetID())) + return nil, errs.Unauthorized("aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner %s", p.GetID()) } claims, err := p.authorizeToken(token) if err != nil { @@ -462,7 +462,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, }, } // Validate user options - signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) + signOptions = append(signOptions, sshCertOptionsValidator(defaults)) // Set defaults if not given as user options signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) @@ -474,8 +474,8 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 8c59bebe..5e9ea92c 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -704,7 +704,7 @@ func TestAWS_AuthorizeRenew(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.aws.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + if err := tt.aws.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("AWS.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } else if err != nil { sc, ok := err.(errs.StatusCoder) diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 86eb516f..88755c2a 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -210,14 +210,14 @@ func (p *Azure) Init(config Config) (err error) { return nil } -// authorizeToken returs the claims, name, group, error. +// authorizeToken returns the claims, name, group, error. func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, error) { jwt, err := jose.ParseSigned(token) if err != nil { return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token") } if len(jwt.Headers) == 0 { - return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; azure token missing header")) + return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header") } var found bool @@ -230,7 +230,7 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err } } if !found { - return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; cannot validate azure token")) + return nil, "", "", errs.Unauthorized("azure.authorizeToken; cannot validate azure token") } if err := claims.ValidateWithLeeway(jose.Expected{ @@ -243,12 +243,12 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err // Validate TenantID if claims.TenantID != p.TenantID { - return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)")) + return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)") } re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID) if len(re) != 4 { - return nil, "", "", errs.Unauthorized(errors.Errorf("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID)) + return nil, "", "", errs.Unauthorized("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID) } group, name := re[2], re[3] return &claims, name, group, nil @@ -272,7 +272,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } } if !found { - return nil, errs.Unauthorized(errors.New("azure.AuthorizeSign; azure token validation failed - invalid resource group")) + return nil, errs.Unauthorized("azure.AuthorizeSign; azure token validation failed - invalid resource group") } } @@ -302,7 +302,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // certificate was configured to allow renewals. func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("azure.AuthorizeRenew; renew is disabled for azure provisioner %s", p.GetID())) + return errs.Unauthorized("azure.AuthorizeRenew; renew is disabled for azure provisioner %s", p.GetID()) } return nil } @@ -310,7 +310,7 @@ func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) erro // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("azure.AuthorizeSSHSign; sshCA is disabled for provisioner %s", p.GetID())) + return nil, errs.Unauthorized("azure.AuthorizeSSHSign; sshCA is disabled for provisioner %s", p.GetID()) } _, name, _, err := p.authorizeToken(token) @@ -328,7 +328,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio Principals: []string{name}, } // Validate user options - signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) + signOptions = append(signOptions, sshCertOptionsValidator(defaults)) // Set defaults if not given as user options signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) @@ -340,9 +340,9 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 13e6ac8e..f49624cc 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -488,7 +488,7 @@ func TestAzure_AuthorizeRenew(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.azure.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + if err := tt.azure.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } else if err != nil { sc, ok := err.(errs.StatusCoder) diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 69a3006a..d55b702f 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -243,7 +243,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // AuthorizeRenew returns an error if the renewal is disabled. func (p *GCP) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("gcp.AuthorizeRenew; renew is disabled for gcp provisioner %s", p.GetID())) + return errs.Unauthorized("gcp.AuthorizeRenew; renew is disabled for gcp provisioner %s", p.GetID()) } return nil } @@ -264,7 +264,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { return nil, errs.Wrap(http.StatusUnauthorized, err, "gcp.authorizeToken; error parsing gcp token") } if len(jwt.Headers) == 0 { - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; error parsing gcp token - header is missing")) + return nil, errs.Unauthorized("gcp.authorizeToken; error parsing gcp token - header is missing") } var found bool @@ -278,7 +278,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } if !found { - return nil, errs.Unauthorized(errors.Errorf("gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid %s", kid)) + return nil, errs.Unauthorized("gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid %s", kid) } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -293,7 +293,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { // validate audiences with the defaults if !matchesAudience(claims.Audience, p.audiences.Sign) { - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid audience claim (aud)")) + return nil, errs.Unauthorized("gcp.authorizeToken; invalid gcp token - invalid audience claim (aud)") } // validate subject (service account) @@ -306,7 +306,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } if !found { - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid subject claim")) + return nil, errs.Unauthorized("gcp.authorizeToken; invalid gcp token - invalid subject claim") } } @@ -320,26 +320,26 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { } } if !found { - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid project id")) + return nil, errs.Unauthorized("gcp.authorizeToken; invalid gcp token - invalid project id") } } // validate instance age if d := p.InstanceAge.Value(); d > 0 { if now.Sub(claims.Google.ComputeEngine.InstanceCreationTimestamp.Time()) > d { - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old")) + return nil, errs.Unauthorized("gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old") } } switch { case claims.Google.ComputeEngine.InstanceID == "": - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty")) + return nil, errs.Unauthorized("gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty") case claims.Google.ComputeEngine.InstanceName == "": - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty")) + return nil, errs.Unauthorized("gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty") case claims.Google.ComputeEngine.ProjectID == "": - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty")) + return nil, errs.Unauthorized("gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty") case claims.Google.ComputeEngine.Zone == "": - return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty")) + return nil, errs.Unauthorized("gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty") } return &claims, nil @@ -348,7 +348,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) { // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner %s", p.GetID())) + return nil, errs.Unauthorized("gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner %s", p.GetID()) } claims, err := p.authorizeToken(token) if err != nil { @@ -371,7 +371,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, }, } // Validate user options - signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) + signOptions = append(signOptions, sshCertOptionsValidator(defaults)) // Set defaults if not given as user options signOptions = append(signOptions, sshCertDefaultsModifier(defaults)) @@ -383,8 +383,8 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index bdda8fd9..0fbb4b41 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -680,7 +680,7 @@ func TestGCP_AuthorizeRenew(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + if err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("GCP.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } else if err != nil { sc, ok := err.(errs.StatusCoder) diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 1c613de6..57297f78 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -120,12 +120,12 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errs.Unauthorized(errors.Errorf("jwk.authorizeToken; invalid jwk token audience claim (aud); want %s, but got %s", - audiences, claims.Audience)) + return nil, errs.Unauthorized("jwk.authorizeToken; invalid jwk token audience claim (aud); want %s, but got %s", + audiences, claims.Audience) } if claims.Subject == "" { - return nil, errs.Unauthorized(errors.New("jwk.authorizeToken; jwk token subject cannot be empty")) + return nil, errs.Unauthorized("jwk.authorizeToken; jwk token subject cannot be empty") } return &claims, nil @@ -173,7 +173,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // certificate was configured to allow renewals. func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("jwk.AuthorizeRenew; renew is disabled for jwk provisioner %s", p.GetID())) + return errs.Unauthorized("jwk.AuthorizeRenew; renew is disabled for jwk provisioner %s", p.GetID()) } return nil } @@ -181,20 +181,20 @@ func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("jwk.AuthorizeSSHSign; sshCA is disabled for jwk provisioner %s", p.GetID())) + return nil, errs.Unauthorized("jwk.AuthorizeSSHSign; sshCA is disabled for jwk provisioner %s", p.GetID()) } claims, err := p.authorizeToken(token, p.audiences.SSHSign) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHSign") } if claims.Step == nil || claims.Step.SSH == nil { - return nil, errs.Unauthorized(errors.New("jwk.AuthorizeSSHSign; jwk token must be an SSH provisioning token")) + return nil, errs.Unauthorized("jwk.AuthorizeSSHSign; jwk token must be an SSH provisioning token") } opts := claims.Step.SSH signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token - sshCertificateOptionsValidator(*opts), + sshCertOptionsValidator(*opts), } t := now() @@ -231,9 +231,9 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require and validate all the default fields in the SSH certificate. - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index a0c48ee9..ed97d8f1 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -222,7 +222,7 @@ func TestJWK_AuthorizeRevoke(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token); err != nil { + if err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token); err != nil { if assert.NotNil(t, tt.err) { sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") @@ -337,7 +337,7 @@ func TestJWK_AuthorizeRenew(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr { + if err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr { t.Errorf("JWK.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr) } else if err != nil { sc, ok := err.(errs.StatusCoder) diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 0826028e..b63ce979 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -149,7 +149,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, claims k8sSAPayload ) if p.pubKeys == nil { - return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented")) + return nil, errs.Unauthorized("k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented") /* NOTE: We plan to support the TokenReview API in a future release. Below is some code that should be useful when we prioritize this integration. @@ -177,7 +177,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, } } if !valid { - return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; error validating k8sSA token and extracting claims")) + return nil, errs.Unauthorized("k8ssa.authorizeToken; error validating k8sSA token and extracting claims") } // According to "rfc7519 JSON Web Token" acceptable skew should be no @@ -189,7 +189,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, } if claims.Subject == "" { - return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; k8sSA token subject cannot be empty")) + return nil, errs.Unauthorized("k8ssa.authorizeToken; k8sSA token subject cannot be empty") } return &claims, nil @@ -221,7 +221,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, // AuthorizeRenew returns an error if the renewal is disabled. func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("k8ssa.AuthorizeRenew; renew is disabled for k8sSA provisioner %s", p.GetID())) + return errs.Unauthorized("k8ssa.AuthorizeRenew; renew is disabled for k8sSA provisioner %s", p.GetID()) } return nil } @@ -229,7 +229,7 @@ func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) erro // AuthorizeSSHSign validates an request for an SSH certificate. func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner %s", p.GetID())) + return nil, errs.Unauthorized("k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner %s", p.GetID()) } if _, err := p.authorizeToken(token, p.audiences.SSHSign); err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSSHSign") @@ -246,9 +246,9 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require and validate all the default fields in the SSH certificate. - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 09a856c5..f1d12b4a 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -363,10 +363,10 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { case sshCertDefaultsModifier: assert.Equals(t, v.CertType, SSHUserCert) case *sshDefaultExtensionModifier: - case *sshCertificateValidityValidator: + case *sshCertValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) case *sshDefaultPublicKeyValidator: - case *sshCertificateDefaultValidator: + case *sshCertDefaultValidator: case *sshDefaultDuration: assert.Equals(t, v.Claimer, tc.p.claimer) default: diff --git a/authority/provisioner/noop_test.go b/authority/provisioner/noop_test.go index c79e7460..19e4d235 100644 --- a/authority/provisioner/noop_test.go +++ b/authority/provisioner/noop_test.go @@ -14,8 +14,8 @@ func Test_noop(t *testing.T) { assert.Equals(t, "noop", p.GetName()) assert.Equals(t, noopType, p.GetType()) assert.Equals(t, nil, p.Init(Config{})) - assert.Equals(t, nil, p.AuthorizeRenew(context.TODO(), &x509.Certificate{})) - assert.Equals(t, nil, p.AuthorizeRevoke(context.TODO(), "foo")) + assert.Equals(t, nil, p.AuthorizeRenew(context.Background(), &x509.Certificate{})) + assert.Equals(t, nil, p.AuthorizeRevoke(context.Background(), "foo")) kid, key, ok := p.GetEncryptedKey() assert.Equals(t, "", kid) diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 87710ebb..0b5448af 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -195,12 +195,12 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { // Validate azp if present if p.AuthorizedParty != "" && p.AuthorizedParty != o.ClientID { - return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: invalid azp")) + return errs.Unauthorized("validatePayload: failed to validate oidc token payload: invalid azp") } // Enforce an email claim if p.Email == "" { - return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: email not found")) + return errs.Unauthorized("validatePayload: failed to validate oidc token payload: email not found") } // Validate domains (case-insensitive) @@ -214,7 +214,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { } } if !found { - return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: email is not allowed")) + return errs.Unauthorized("validatePayload: failed to validate oidc token payload: email is not allowed") } } @@ -230,7 +230,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { } } if !found { - return errs.Unauthorized(errors.New("validatePayload: oidc token payload validation failed: invalid group")) + return errs.Unauthorized("validatePayload: oidc token payload validation failed: invalid group") } } @@ -263,7 +263,7 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) { } } if !found { - return nil, errs.Unauthorized(errors.New("oidc.AuthorizeToken; cannot validate oidc token")) + return nil, errs.Unauthorized("oidc.AuthorizeToken; cannot validate oidc token") } if err := o.ValidatePayload(claims); err != nil { @@ -286,7 +286,7 @@ func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error { if o.IsAdmin(claims.Email) { return nil } - return errs.Unauthorized(errors.New("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token")) + return errs.Unauthorized("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token") } // AuthorizeSign validates the given token. @@ -318,7 +318,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // certificate was configured to allow renewals. func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if o.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("oidc.AuthorizeRenew; renew is disabled for oidc provisioner %s", o.GetID())) + return errs.Unauthorized("oidc.AuthorizeRenew; renew is disabled for oidc provisioner %s", o.GetID()) } return nil } @@ -326,7 +326,7 @@ func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !o.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("oidc.AuthorizeSSHSign; sshCA is disabled for oidc provisioner %s", o.GetID())) + return nil, errs.Unauthorized("oidc.AuthorizeSSHSign; sshCA is disabled for oidc provisioner %s", o.GetID()) } claims, err := o.authorizeToken(token) if err != nil { @@ -352,7 +352,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Non-admin users can only use principals returned by the identityFunc, and // can only sign user certificates. if !o.IsAdmin(claims.Email) { - signOptions = append(signOptions, sshCertificateOptionsValidator(defaults)) + signOptions = append(signOptions, sshCertOptionsValidator(defaults)) } // Default to a user certificate with usernames as principals if those options @@ -367,9 +367,9 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{o.claimer}, + &sshCertValidityValidator{o.claimer}, // Require all the fields in the SSH certificate - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } @@ -382,7 +382,7 @@ func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error { // Only admins can revoke certificates. if !o.IsAdmin(claims.Email) { - return errs.Unauthorized(errors.New("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")) + return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token") } return nil } diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 40e1e309..fd342b01 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -284,43 +284,43 @@ type base struct{} // AuthorizeSign returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for signing x509 Certificates. func (b *base) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { - return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSign not implemented")) + return nil, errs.Unauthorized("provisioner.AuthorizeSign not implemented") } // AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for revoking x509 Certificates. func (b *base) AuthorizeRevoke(ctx context.Context, token string) error { - return errs.Unauthorized(errors.New("provisioner.AuthorizeRevoke not implemented")) + return errs.Unauthorized("provisioner.AuthorizeRevoke not implemented") } // AuthorizeRenew returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for renewing x509 Certificates. func (b *base) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { - return errs.Unauthorized(errors.New("provisioner.AuthorizeRenew not implemented")) + return errs.Unauthorized("provisioner.AuthorizeRenew not implemented") } // AuthorizeSSHSign returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for signing SSH Certificates. func (b *base) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { - return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHSign not implemented")) + return nil, errs.Unauthorized("provisioner.AuthorizeSSHSign not implemented") } // AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for revoking SSH Certificates. func (b *base) AuthorizeSSHRevoke(ctx context.Context, token string) error { - return errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRevoke not implemented")) + return errs.Unauthorized("provisioner.AuthorizeSSHRevoke not implemented") } // AuthorizeSSHRenew returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for renewing SSH Certificates. func (b *base) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { - return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRenew not implemented")) + return nil, errs.Unauthorized("provisioner.AuthorizeSSHRenew not implemented") } // AuthorizeSSHRekey returns an unimplmented error. Provisioners should overwrite // this method if they will support authorizing tokens for rekeying SSH Certificates. func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) { - return nil, nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRekey not implemented")) + return nil, nil, errs.Unauthorized("provisioner.AuthorizeSSHRekey not implemented") } // Identity is the type representing an externally supplied identity that is used diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index ec67baf1..b0ab78ea 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -19,29 +19,29 @@ const ( SSHHostCert = "host" ) -// SSHCertificateModifier is the interface used to change properties in an SSH +// SSHCertModifier is the interface used to change properties in an SSH // certificate. -type SSHCertificateModifier interface { +type SSHCertModifier interface { SignOption Modify(cert *ssh.Certificate) error } -// SSHCertificateOptionModifier is the interface used to add custom options used +// SSHCertOptionModifier is the interface used to add custom options used // to modify the SSH certificate. -type SSHCertificateOptionModifier interface { +type SSHCertOptionModifier interface { SignOption - Option(o SSHOptions) SSHCertificateModifier + Option(o SSHOptions) SSHCertModifier } -// SSHCertificateValidator is the interface used to validate an SSH certificate. -type SSHCertificateValidator interface { +// SSHCertValidator is the interface used to validate an SSH certificate. +type SSHCertValidator interface { SignOption Valid(cert *ssh.Certificate) error } -// SSHCertificateOptionsValidator is the interface used to validate the custom +// SSHCertOptionsValidator is the interface used to validate the custom // options used to modify the SSH certificate. -type SSHCertificateOptionsValidator interface { +type SSHCertOptionsValidator interface { SignOption Valid(got SSHOptions) error } @@ -69,7 +69,7 @@ func (o SSHOptions) Type() uint32 { return sshCertTypeUInt32(o.CertType) } -// Modify implements SSHCertificateModifier and sets the SSHOption in the ssh.Certificate. +// Modify implements SSHCertModifier and sets the SSHOption in the ssh.Certificate. func (o SSHOptions) Modify(cert *ssh.Certificate) error { switch o.CertType { case "": // ignore @@ -116,7 +116,7 @@ func (o SSHOptions) match(got SSHOptions) error { return nil } -// sshCertPrincipalsModifier is an SSHCertificateModifier that sets the +// sshCertPrincipalsModifier is an SSHCertModifier that sets the // principals to the SSH certificate. type sshCertPrincipalsModifier []string @@ -126,7 +126,7 @@ func (o sshCertPrincipalsModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertKeyIDModifier is an SSHCertificateModifier that sets the given +// sshCertKeyIDModifier is an SSHCertModifier that sets the given // Key ID in the SSH certificate. type sshCertKeyIDModifier string @@ -135,7 +135,7 @@ func (m sshCertKeyIDModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertTypeModifier is an SSHCertificateModifier that sets the +// sshCertTypeModifier is an SSHCertModifier that sets the // certificate type. type sshCertTypeModifier string @@ -145,7 +145,7 @@ func (m sshCertTypeModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertValidAfterModifier is an SSHCertificateModifier that sets the +// sshCertValidAfterModifier is an SSHCertModifier that sets the // ValidAfter in the SSH certificate. type sshCertValidAfterModifier uint64 @@ -154,7 +154,7 @@ func (m sshCertValidAfterModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertValidBeforeModifier is an SSHCertificateModifier that sets the +// sshCertValidBeforeModifier is an SSHCertModifier that sets the // ValidBefore in the SSH certificate. type sshCertValidBeforeModifier uint64 @@ -163,11 +163,11 @@ func (m sshCertValidBeforeModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshCertDefaultsModifier implements a SSHCertificateModifier that +// sshCertDefaultsModifier implements a SSHCertModifier that // modifies the certificate with the given options if they are not set. type sshCertDefaultsModifier SSHOptions -// Modify implements the SSHCertificateModifier interface. +// Modify implements the SSHCertModifier interface. func (m sshCertDefaultsModifier) Modify(cert *ssh.Certificate) error { if cert.CertType == 0 { cert.CertType = sshCertTypeUInt32(m.CertType) @@ -184,7 +184,7 @@ func (m sshCertDefaultsModifier) Modify(cert *ssh.Certificate) error { return nil } -// sshDefaultExtensionModifier implements an SSHCertificateModifier that sets +// sshDefaultExtensionModifier implements an SSHCertModifier that sets // the default extensions in an SSH certificate. type sshDefaultExtensionModifier struct{} @@ -208,14 +208,14 @@ func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate) error { } } -// sshDefaultDuration is an SSHCertificateModifier that sets the certificate +// sshDefaultDuration is an SSHCertModifier that sets the certificate // ValidAfter and ValidBefore if they have not been set. It will fail if a // CertType has not been set or is not valid. type sshDefaultDuration struct { *Claimer } -func (m *sshDefaultDuration) Option(o SSHOptions) SSHCertificateModifier { +func (m *sshDefaultDuration) Option(o SSHOptions) SSHCertModifier { return sshModifierFunc(func(cert *ssh.Certificate) error { d, err := m.DefaultSSHCertDuration(cert.CertType) if err != nil { @@ -248,7 +248,7 @@ type sshLimitDuration struct { NotAfter time.Time } -func (m *sshLimitDuration) Option(o SSHOptions) SSHCertificateModifier { +func (m *sshLimitDuration) Option(o SSHOptions) SSHCertModifier { if m.NotAfter.IsZero() { defaultDuration := &sshDefaultDuration{m.Claimer} return defaultDuration.Option(o) @@ -295,22 +295,22 @@ func (m *sshLimitDuration) Option(o SSHOptions) SSHCertificateModifier { }) } -// sshCertificateOptionsValidator validates the user SSHOptions with the ones +// sshCertOptionsValidator validates the user SSHOptions with the ones // usually present in the token. -type sshCertificateOptionsValidator SSHOptions +type sshCertOptionsValidator SSHOptions -// Valid implements SSHCertificateOptionsValidator and returns nil if both +// Valid implements SSHCertOptionsValidator and returns nil if both // SSHOptions match. -func (v sshCertificateOptionsValidator) Valid(got SSHOptions) error { +func (v sshCertOptionsValidator) Valid(got SSHOptions) error { want := SSHOptions(v) return want.match(got) } -type sshCertificateValidityValidator struct { +type sshCertValidityValidator struct { *Claimer } -func (v *sshCertificateValidityValidator) Valid(cert *ssh.Certificate) error { +func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate) error { switch { case cert.ValidAfter == 0: return errors.New("ssh certificate validAfter cannot be 0") @@ -355,12 +355,12 @@ func (v *sshCertificateValidityValidator) Valid(cert *ssh.Certificate) error { } } -// sshCertificateDefaultValidator implements a simple validator for all the +// sshCertDefaultValidator implements a simple validator for all the // fields in the SSH certificate. -type sshCertificateDefaultValidator struct{} +type sshCertDefaultValidator struct{} // Valid returns an error if the given certificate does not contain the necessary fields. -func (v *sshCertificateDefaultValidator) Valid(cert *ssh.Certificate) error { +func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate) error { switch { case len(cert.Nonce) == 0: return errors.New("ssh certificate nonce cannot be empty") diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index 87716e37..c13e46da 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -489,12 +489,12 @@ func Test_sshDefaultExtensionModifier_Modify(t *testing.T) { } } -func Test_sshCertificateDefaultValidator_Valid(t *testing.T) { +func Test_sshCertDefaultValidator_Valid(t *testing.T) { pub, _, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) sshPub, err := ssh.NewPublicKey(pub) assert.FatalError(t, err) - v := sshCertificateDefaultValidator{} + v := sshCertDefaultValidator{} tests := []struct { name string cert *ssh.Certificate @@ -670,10 +670,10 @@ func Test_sshCertificateDefaultValidator_Valid(t *testing.T) { } } -func Test_sshCertificateValidityValidator(t *testing.T) { +func Test_sshCertValidityValidator(t *testing.T) { p, err := generateX5C(nil) assert.FatalError(t, err) - v := sshCertificateValidityValidator{p.claimer} + v := sshCertValidityValidator{p.claimer} n := now() tests := []struct { name string @@ -992,7 +992,7 @@ func Test_sshLimitDuration_Option(t *testing.T) { name string fields fields args args - want SSHCertificateModifier + want SSHCertModifier }{ // TODO: Add test cases. } diff --git a/authority/provisioner/ssh_test.go b/authority/provisioner/ssh_test.go index 1b31f78b..84860a75 100644 --- a/authority/provisioner/ssh_test.go +++ b/authority/provisioner/ssh_test.go @@ -45,22 +45,22 @@ func signSSHCertificate(key crypto.PublicKey, opts SSHOptions, signOpts []SignOp return nil, err } - var mods []SSHCertificateModifier - var validators []SSHCertificateValidator + var mods []SSHCertModifier + var validators []SSHCertValidator for _, op := range signOpts { switch o := op.(type) { // modify the ssh.Certificate - case SSHCertificateModifier: + case SSHCertModifier: mods = append(mods, o) // modify the ssh.Certificate given the SSHOptions - case SSHCertificateOptionModifier: + case SSHCertOptionModifier: mods = append(mods, o.Option(opts)) // validate the ssh.Certificate - case SSHCertificateValidator: + case SSHCertValidator: validators = append(validators, o) // validate the given SSHOptions - case SSHCertificateOptionsValidator: + case SSHCertOptionsValidator: if err := o.Valid(opts); err != nil { return nil, err } diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index 3c55aada..db1c5a89 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -112,20 +112,20 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa return nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.authorizeToken; error checking checking sshpop cert revocation") } else if isRevoked { - return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate is revoked")) + return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop certificate is revoked") } // Check validity period of the certificate. n := time.Now() if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) { - return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate validAfter is in the future")) + return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop certificate validAfter is in the future") } if sshCert.ValidBefore != 0 && time.Unix(int64(sshCert.ValidBefore), 0).Before(n) { - return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate validBefore is in the past")) + return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop certificate validBefore is in the past") } sshCryptoPubKey, ok := sshCert.Key.(ssh.CryptoPublicKey) if !ok { - return nil, errs.InternalServerError(errors.New("sshpop.authorizeToken; sshpop public key could not be cast to ssh CryptoPublicKey")) + return nil, errs.InternalServer("sshpop.authorizeToken; sshpop public key could not be cast to ssh CryptoPublicKey") } pubKey := sshCryptoPubKey.CryptoPublicKey() @@ -146,7 +146,7 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa } } if !found { - return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate")) + return nil, errs.Unauthorized("sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate") } // Using the ssh certificates key to validate the claims accomplishes two @@ -170,12 +170,12 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errs.Unauthorized(errors.Errorf("sshpop.authorizeToken; sshpop token has invalid audience "+ - "claim (aud): expected %s, but got %s", audiences, claims.Audience)) + return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop token has invalid audience "+ + "claim (aud): expected %s, but got %s", audiences, claims.Audience) } if claims.Subject == "" { - return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop token subject cannot be empty")) + return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop token subject cannot be empty") } claims.sshCert = sshCert @@ -190,8 +190,8 @@ func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error { return errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRevoke") } if claims.Subject != strconv.FormatUint(claims.sshCert.Serial, 10) { - return errs.BadRequest(errors.New("sshpop.AuthorizeSSHRevoke; sshpop token subject " + - "must be equivalent to sshpop certificate serial number")) + return errs.BadRequest("sshpop.AuthorizeSSHRevoke; sshpop token subject " + + "must be equivalent to sshpop certificate serial number") } return nil } @@ -204,7 +204,7 @@ func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Cert return nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRenew") } if claims.sshCert.CertType != ssh.HostCert { - return nil, errs.BadRequest(errors.New("sshpop.AuthorizeSSHRenew; sshpop certificate must be a host ssh certificate")) + return nil, errs.BadRequest("sshpop.AuthorizeSSHRenew; sshpop certificate must be a host ssh certificate") } return claims.sshCert, nil @@ -219,15 +219,15 @@ func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Cert return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRekey") } if claims.sshCert.CertType != ssh.HostCert { - return nil, nil, errs.BadRequest(errors.New("sshpop.AuthorizeSSHRekey; sshpop certificate must be a host ssh certificate")) + return nil, nil, errs.BadRequest("sshpop.AuthorizeSSHRekey; sshpop certificate must be a host ssh certificate") } return claims.sshCert, []SignOption{ // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require and validate all the default fields in the SSH certificate. - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, }, nil } diff --git a/authority/provisioner/sshpop_test.go b/authority/provisioner/sshpop_test.go index 32f58879..5863b6f9 100644 --- a/authority/provisioner/sshpop_test.go +++ b/authority/provisioner/sshpop_test.go @@ -564,8 +564,8 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) { for _, o := range opts { switch v := o.(type) { case *sshDefaultPublicKeyValidator: - case *sshCertificateDefaultValidator: - case *sshCertificateValidityValidator: + case *sshCertDefaultValidator: + case *sshCertValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) default: assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 692cd963..f00a215d 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -136,7 +136,7 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err leaf := verifiedChains[0][0] if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 { - return nil, errs.Unauthorized(errors.New("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature")) + return nil, errs.Unauthorized("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature") } // Using the leaf certificates key to validate the claims accomplishes two @@ -160,12 +160,12 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err // validate audiences with the defaults if !matchesAudience(claims.Audience, audiences) { - return nil, errs.Unauthorized(errors.Errorf("x5c.authorizeToken; x5c token has invalid audience "+ - "claim (aud); expected %s, but got %s", audiences, claims.Audience)) + return nil, errs.Unauthorized("x5c.authorizeToken; x5c token has invalid audience "+ + "claim (aud); expected %s, but got %s", audiences, claims.Audience) } if claims.Subject == "" { - return nil, errs.Unauthorized(errors.New("x5c.authorizeToken; x5c token subject cannot be empty")) + return nil, errs.Unauthorized("x5c.authorizeToken; x5c token subject cannot be empty") } // Save the verified chains on the x5c payload object. @@ -213,7 +213,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // AuthorizeRenew returns an error if the renewal is disabled. func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { if p.claimer.IsDisableRenewal() { - return errs.Unauthorized(errors.Errorf("x5c.AuthorizeRenew; renew is disabled for x5c provisioner %s", p.GetID())) + return errs.Unauthorized("x5c.AuthorizeRenew; renew is disabled for x5c provisioner %s", p.GetID()) } return nil } @@ -221,7 +221,7 @@ func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error // AuthorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) { if !p.claimer.IsSSHCAEnabled() { - return nil, errs.Unauthorized(errors.Errorf("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner %s", p.GetID())) + return nil, errs.Unauthorized("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner %s", p.GetID()) } claims, err := p.authorizeToken(token, p.audiences.SSHSign) @@ -230,13 +230,13 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, } if claims.Step == nil || claims.Step.SSH == nil { - return nil, errs.Unauthorized(errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token")) + return nil, errs.Unauthorized("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token") } opts := claims.Step.SSH signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token - sshCertificateOptionsValidator(*opts), + sshCertOptionsValidator(*opts), } // Add modifiers from custom claims @@ -272,8 +272,8 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validate public key. &sshDefaultPublicKeyValidator{}, // Validate the validity period. - &sshCertificateValidityValidator{p.claimer}, + &sshCertValidityValidator{p.claimer}, // Require all the fields in the SSH certificate - &sshCertificateDefaultValidator{}, + &sshCertDefaultValidator{}, ), nil } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 775f3202..3ebaeb6b 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -548,7 +548,7 @@ func TestX5C_AuthorizeRevoke(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil { + if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") @@ -594,7 +594,7 @@ func TestX5C_AuthorizeRenew(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if err := tc.p.AuthorizeRenew(context.TODO(), nil); err != nil { + if err := tc.p.AuthorizeRenew(context.Background(), nil); err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") @@ -754,7 +754,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { tc := tt(t) - if opts, err := tc.p.AuthorizeSSHSign(context.TODO(), tc.token); err != nil { + if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") @@ -768,7 +768,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { nw := now() for _, o := range opts { switch v := o.(type) { - case sshCertificateOptionsValidator: + case sshCertOptionsValidator: tc.claims.Step.SSH.ValidAfter.t = time.Time{} tc.claims.Step.SSH.ValidBefore.t = time.Time{} assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH) @@ -787,10 +787,10 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { case *sshLimitDuration: assert.Equals(t, v.Claimer, tc.p.claimer) assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter) - case *sshCertificateValidityValidator: + case *sshCertValidityValidator: assert.Equals(t, v.Claimer, tc.p.claimer) case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator, - *sshCertificateDefaultValidator: + *sshCertDefaultValidator: case sshCertKeyIDValidator: assert.Equals(t, string(v), "foo") default: diff --git a/authority/provisioners.go b/authority/provisioners.go index 2d43571b..99a85d46 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -2,18 +2,16 @@ package authority import ( "crypto/x509" - "net/http" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" ) // GetEncryptedKey returns the JWE key corresponding to the given kid argument. func (a *Authority) GetEncryptedKey(kid string) (string, error) { key, ok := a.provisioners.LoadEncryptedKey(kid) if !ok { - return "", &apiError{errors.Errorf("encrypted key with kid %s was not found", kid), - http.StatusNotFound, apiCtx{}} + return "", errs.NotFound("encrypted key with kid %s was not found", kid) } return key, nil } @@ -30,8 +28,7 @@ func (a *Authority) GetProvisioners(cursor string, limit int) (provisioner.List, func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisioner.Interface, error) { p, ok := a.provisioners.LoadByCertificate(crt) if !ok { - return nil, &apiError{errors.Errorf("provisioner not found"), - http.StatusNotFound, apiCtx{}} + return nil, errs.NotFound("provisioner not found") } return p, nil } @@ -40,8 +37,7 @@ func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisi func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error) { p, ok := a.provisioners.Load(id) if !ok { - return nil, &apiError{errors.Errorf("provisioner not found"), - http.StatusNotFound, apiCtx{}} + return nil, errs.NotFound("provisioner not found") } return p, nil } diff --git a/authority/provisioners_test.go b/authority/provisioners_test.go index fb84a31d..1a45f209 100644 --- a/authority/provisioners_test.go +++ b/authority/provisioners_test.go @@ -7,13 +7,15 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/errs" ) func TestGetEncryptedKey(t *testing.T) { type ek struct { - a *Authority - kid string - err *apiError + a *Authority + kid string + err error + code int } tests := map[string]func(t *testing.T) *ek{ "ok": func(t *testing.T) *ek { @@ -32,10 +34,10 @@ func TestGetEncryptedKey(t *testing.T) { a, err := New(c) assert.FatalError(t, err) return &ek{ - a: a, - kid: "foo", - err: &apiError{errors.Errorf("encrypted key with kid foo was not found"), - http.StatusNotFound, apiCtx{}}, + a: a, + kid: "foo", + err: errors.New("encrypted key with kid foo was not found"), + code: http.StatusNotFound, } }, } @@ -47,14 +49,10 @@ func TestGetEncryptedKey(t *testing.T) { ek, err := tc.a.GetEncryptedKey(tc.kid) if err != nil { if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { diff --git a/authority/root.go b/authority/root.go index 3794a6c8..f391997f 100644 --- a/authority/root.go +++ b/authority/root.go @@ -2,23 +2,20 @@ package authority import ( "crypto/x509" - "net/http" - "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" ) // Root returns the certificate corresponding to the given SHA sum argument. func (a *Authority) Root(sum string) (*x509.Certificate, error) { val, ok := a.certificates.Load(sum) if !ok { - return nil, &apiError{errors.Errorf("certificate with fingerprint %s was not found", sum), - http.StatusNotFound, apiCtx{}} + return nil, errs.NotFound("certificate with fingerprint %s was not found", sum) } crt, ok := val.(*x509.Certificate) if !ok { - return nil, &apiError{errors.Errorf("stored value is not a *x509.Certificate"), - http.StatusInternalServerError, apiCtx{}} + return nil, errs.InternalServer("stored value is not a *x509.Certificate") } return crt, nil } @@ -52,8 +49,7 @@ func (a *Authority) GetFederation() (federation []*x509.Certificate, err error) crt, ok := v.(*x509.Certificate) if !ok { federation = nil - err = &apiError{errors.Errorf("stored value is not a *x509.Certificate"), - http.StatusInternalServerError, apiCtx{}} + err = errs.InternalServer("stored value is not a *x509.Certificate") return false } federation = append(federation, crt) diff --git a/authority/root_test.go b/authority/root_test.go index 4b648d78..a936b66f 100644 --- a/authority/root_test.go +++ b/authority/root_test.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" + "github.com/smallstep/certificates/errs" "github.com/smallstep/cli/crypto/pemutil" ) @@ -16,12 +17,13 @@ func TestRoot(t *testing.T) { a.certificates.Store("invaliddata", "a string") // invalid cert for testing tests := map[string]struct { - sum string - err *apiError + sum string + err error + code int }{ - "not-found": {"foo", &apiError{errors.New("certificate with fingerprint foo was not found"), http.StatusNotFound, apiCtx{}}}, - "invalid-stored-certificate": {"invaliddata", &apiError{errors.New("stored value is not a *x509.Certificate"), http.StatusInternalServerError, apiCtx{}}}, - "success": {"189f573cfa159251e445530847ef80b1b62a3a380ee670dcb49e33ed34da0616", nil}, + "not-found": {"foo", errors.New("certificate with fingerprint foo was not found"), http.StatusNotFound}, + "invalid-stored-certificate": {"invaliddata", errors.New("stored value is not a *x509.Certificate"), http.StatusInternalServerError}, + "success": {"189f573cfa159251e445530847ef80b1b62a3a380ee670dcb49e33ed34da0616", nil, http.StatusOK}, } for name, tc := range tests { @@ -29,14 +31,10 @@ func TestRoot(t *testing.T) { crt, err := a.Root(tc.sum) if err != nil { if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { diff --git a/authority/ssh.go b/authority/ssh.go index 5d80a427..28066556 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -122,7 +122,7 @@ func (a *Authority) GetSSHFederation() (*SSHKeys, error) { // GetSSHConfig returns rendered templates for clients (user) or servers (host). func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) { if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil { - return nil, errs.NotFound(errors.New("getSSHConfig: ssh is not configured")) + return nil, errs.NotFound("getSSHConfig: ssh is not configured") } var ts []templates.Template @@ -136,7 +136,7 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template ts = a.config.Templates.SSH.Host } default: - return nil, errs.BadRequest(errors.Errorf("getSSHConfig: type %s is not valid", typ)) + return nil, errs.BadRequest("getSSHConfig: type %s is not valid", typ) } // Merge user and default data @@ -177,13 +177,13 @@ func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error } return nil, nil } - return nil, errs.NotFound(errors.New("authority.GetSSHBastion; ssh is not configured")) + return nil, errs.NotFound("authority.GetSSHBastion; ssh is not configured") } // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - var mods []provisioner.SSHCertificateModifier - var validators []provisioner.SSHCertificateValidator + var mods []provisioner.SSHCertModifier + var validators []provisioner.SSHCertValidator // Set backdate with the configured value opts.Backdate = a.config.AuthorityConfig.Backdate.Duration @@ -191,27 +191,27 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign for _, op := range signOpts { switch o := op.(type) { // modify the ssh.Certificate - case provisioner.SSHCertificateModifier: + case provisioner.SSHCertModifier: mods = append(mods, o) // modify the ssh.Certificate given the SSHOptions - case provisioner.SSHCertificateOptionModifier: + case provisioner.SSHCertOptionModifier: mods = append(mods, o.Option(opts)) // validate the ssh.Certificate - case provisioner.SSHCertificateValidator: + case provisioner.SSHCertValidator: validators = append(validators, o) // validate the given SSHOptions - case provisioner.SSHCertificateOptionsValidator: + case provisioner.SSHCertOptionsValidator: if err := o.Valid(opts); err != nil { - return nil, errs.Forbidden(err) + return nil, errs.Wrap(http.StatusForbidden, err, "signSSH") } default: - return nil, errs.InternalServerError(errors.Errorf("signSSH: invalid extra option type %T", o)) + return nil, errs.InternalServer("signSSH: invalid extra option type %T", o) } } nonce, err := randutil.ASCII(32) if err != nil { - return nil, errs.InternalServerError(err) + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH") } var serial uint64 @@ -228,13 +228,13 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // Use opts to modify the certificate if err := opts.Modify(cert); err != nil { - return nil, errs.Forbidden(err) + return nil, errs.Wrap(http.StatusForbidden, err, "signSSH") } // Use provisioner modifiers for _, m := range mods { if err := m.Modify(cert); err != nil { - return nil, errs.Forbidden(err) + return nil, errs.Wrap(http.StatusForbidden, err, "signSSH") } } @@ -243,16 +243,16 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("signSSH: user certificate signing is not enabled")) + return nil, errs.NotImplemented("signSSH: user certificate signing is not enabled") } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("signSSH: host certificate signing is not enabled")) + return nil, errs.NotImplemented("signSSH: host certificate signing is not enabled") } signer = a.sshCAHostCertSignKey default: - return nil, errs.InternalServerError(errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType)) + return nil, errs.InternalServer("signSSH: unexpected ssh certificate type: %d", cert.CertType) } cert.SignatureKey = signer.PublicKey() @@ -270,7 +270,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // User provisioners validators for _, v := range validators { if err := v.Valid(cert); err != nil { - return nil, errs.Forbidden(err) + return nil, errs.Wrap(http.StatusForbidden, err, "signSSH") } } @@ -285,7 +285,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) { nonce, err := randutil.ASCII(32) if err != nil { - return nil, errs.InternalServerError(err) + return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH") } var serial uint64 @@ -294,7 +294,7 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errs.BadRequest(errors.New("rewnewSSH: cannot renew certificate without validity period")) + return nil, errs.BadRequest("rewnewSSH: cannot renew certificate without validity period") } backdate := a.config.AuthorityConfig.Backdate.Duration @@ -321,16 +321,16 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("renewSSH: user certificate signing is not enabled")) + return nil, errs.NotImplemented("renewSSH: user certificate signing is not enabled") } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("renewSSH: host certificate signing is not enabled")) + return nil, errs.NotImplemented("renewSSH: host certificate signing is not enabled") } signer = a.sshCAHostCertSignKey default: - return nil, errs.InternalServerError(errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType)) + return nil, errs.InternalServer("renewSSH: unexpected ssh certificate type: %d", cert.CertType) } cert.SignatureKey = signer.PublicKey() @@ -354,21 +354,21 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) // RekeySSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - var validators []provisioner.SSHCertificateValidator + var validators []provisioner.SSHCertValidator for _, op := range signOpts { switch o := op.(type) { // validate the ssh.Certificate - case provisioner.SSHCertificateValidator: + case provisioner.SSHCertValidator: validators = append(validators, o) default: - return nil, errs.InternalServerError(errors.Errorf("rekeySSH; invalid extra option type %T", o)) + return nil, errs.InternalServer("rekeySSH; invalid extra option type %T", o) } } nonce, err := randutil.ASCII(32) if err != nil { - return nil, errs.InternalServerError(err) + return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH") } var serial uint64 @@ -377,7 +377,7 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errs.BadRequest(errors.New("rekeySSH; cannot rekey certificate without validity period")) + return nil, errs.BadRequest("rekeySSH; cannot rekey certificate without validity period") } backdate := a.config.AuthorityConfig.Backdate.Duration @@ -404,16 +404,16 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("rekeySSH; user certificate signing is not enabled")) + return nil, errs.NotImplemented("rekeySSH; user certificate signing is not enabled") } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("rekeySSH; host certificate signing is not enabled")) + return nil, errs.NotImplemented("rekeySSH; host certificate signing is not enabled") } signer = a.sshCAHostCertSignKey default: - return nil, errs.BadRequest(errors.Errorf("rekeySSH; unexpected ssh certificate type: %d", cert.CertType)) + return nil, errs.BadRequest("rekeySSH; unexpected ssh certificate type: %d", cert.CertType) } cert.SignatureKey = signer.PublicKey() @@ -431,7 +431,7 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp // Apply validators from provisioner.. for _, v := range validators { if err := v.Valid(cert); err != nil { - return nil, errs.Forbidden(err) + return nil, errs.Wrap(http.StatusForbidden, err, "rekeySSH") } } @@ -445,18 +445,18 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp // SignSSHAddUser signs a certificate that provisions a new user in a server. func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) { if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented(errors.New("signSSHAddUser: user certificate signing is not enabled")) + return nil, errs.NotImplemented("signSSHAddUser: user certificate signing is not enabled") } if subject.CertType != ssh.UserCert { - return nil, errs.Forbidden(errors.New("signSSHAddUser: certificate is not a user certificate")) + return nil, errs.Forbidden("signSSHAddUser: certificate is not a user certificate") } if len(subject.ValidPrincipals) != 1 { - return nil, errs.Forbidden(errors.New("signSSHAddUser: certificate does not have only one principal")) + return nil, errs.Forbidden("signSSHAddUser: certificate does not have only one principal") } nonce, err := randutil.ASCII(32) if err != nil { - return nil, errs.InternalServerError(err) + return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser") } var serial uint64 diff --git a/authority/ssh_test.go b/authority/ssh_test.go index db5dc85d..cc3f164c 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -80,7 +80,7 @@ func (v sshTestOptionsValidator) Valid(opts provisioner.SSHOptions) error { type sshTestOptionsModifier string -func (m sshTestOptionsModifier) Option(opts provisioner.SSHOptions) provisioner.SSHCertificateModifier { +func (m sshTestOptionsModifier) Option(opts provisioner.SSHOptions) provisioner.SSHCertModifier { return sshTestCertModifier(string(m)) } @@ -492,12 +492,12 @@ func TestAuthority_CheckSSHHost(t *testing.T) { want bool wantErr bool }{ - {"true", fields{true, nil}, args{context.TODO(), "foo.internal.com", ""}, true, false}, - {"false", fields{false, nil}, args{context.TODO(), "foo.internal.com", ""}, false, false}, - {"notImplemented", fields{false, db.ErrNotImplemented}, args{context.TODO(), "foo.internal.com", ""}, false, true}, - {"notImplemented", fields{true, db.ErrNotImplemented}, args{context.TODO(), "foo.internal.com", ""}, false, true}, - {"internal", fields{false, fmt.Errorf("an error")}, args{context.TODO(), "foo.internal.com", ""}, false, true}, - {"internal", fields{true, fmt.Errorf("an error")}, args{context.TODO(), "foo.internal.com", ""}, false, true}, + {"true", fields{true, nil}, args{context.Background(), "foo.internal.com", ""}, true, false}, + {"false", fields{false, nil}, args{context.Background(), "foo.internal.com", ""}, false, false}, + {"notImplemented", fields{false, db.ErrNotImplemented}, args{context.Background(), "foo.internal.com", ""}, false, true}, + {"notImplemented", fields{true, db.ErrNotImplemented}, args{context.Background(), "foo.internal.com", ""}, false, true}, + {"internal", fields{false, fmt.Errorf("an error")}, args{context.Background(), "foo.internal.com", ""}, false, true}, + {"internal", fields{true, fmt.Errorf("an error")}, args{context.Background(), "foo.internal.com", ""}, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/authority/tls.go b/authority/tls.go index 9199c040..03a9ec33 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -61,7 +61,7 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption { // Sign creates a signed certificate from a certificate signing request. func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { var ( - opts = []errs.Option{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)} + opts = []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)} mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)} certValidators = []provisioner.CertificateValidator{} issIdentity = a.intermediateIdentity @@ -81,7 +81,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti case provisioner.ProfileModifier: mods = append(mods, k.Option(signOpts)) default: - return nil, errs.InternalServerError(errors.Errorf("authority.Sign; invalid extra option type %T", k), opts...) + return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...) } } @@ -131,7 +131,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti // Renew creates a new Certificate identical to the old certificate, except // with a validity window that begins 'now'. func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { - opts := []errs.Option{errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String())} + opts := []interface{}{errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String())} // Check step provisioner extensions if err := a.authorizeRenew(oldCert); err != nil { @@ -237,7 +237,7 @@ type RevokeOptions struct { // // TODO: Add OCSP and CRL support. func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error { - opts := []errs.Option{ + opts := []interface{}{ errs.WithKeyVal("serialNumber", revokeOpts.Serial), errs.WithKeyVal("reasonCode", revokeOpts.ReasonCode), errs.WithKeyVal("reason", revokeOpts.Reason), @@ -281,7 +281,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error var ok bool p, ok = a.provisioners.LoadByToken(token, &claims.Claims) if !ok { - return errs.InternalServerError(errors.Errorf("authority.Revoke; provisioner not found"), opts...) + return errs.InternalServer("authority.Revoke; provisioner not found", opts...) } rci.TokenID, err = p.GetTokenID(revokeOpts.OTT) if err != nil { @@ -309,10 +309,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error case nil: return nil case db.ErrNotImplemented: - return errs.NotImplemented(errors.New("authority.Revoke; no persistence layer configured"), opts...) + return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...) case db.ErrAlreadyExists: - return errs.BadRequest(errors.Errorf("authority.Revoke; certificate with serial "+ - "number %s has already been revoked", rci.Serial), opts...) + return errs.BadRequest("authority.Revoke; certificate with serial "+ + "number %s has already been revoked", append([]interface{}{rci.Serial}, opts...)...) default: return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) } diff --git a/ca/client.go b/ca/client.go index e6fdab92..ce936655 100644 --- a/ca/client.go +++ b/ca/client.go @@ -553,7 +553,7 @@ retry: // verify the sha256 sum := sha256.Sum256(root.RootPEM.Raw) if sha256Sum != strings.ToLower(hex.EncodeToString(sum[:])) { - return nil, errs.BadRequest(errors.New("client.Root; root certificate SHA256 fingerprint do not match")) + return nil, errs.BadRequest("client.Root; root certificate SHA256 fingerprint do not match") } return &root, nil } @@ -961,8 +961,8 @@ func (c *Client) SSHCheckHost(principal string, token string) (*api.SSHCheckPrin retry: resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body)) if err != nil { - return nil, errs.Wrapf(http.StatusInternalServerError, err, "client POST %s failed", u, - errs.WithMessage("Failed to perform POST request to %s", u)) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "client POST %s failed", + []interface{}{u, errs.WithMessage("Failed to perform POST request to %s", u)}...) } if resp.StatusCode >= 400 { if !retried && c.retryOnError(resp) { @@ -974,8 +974,8 @@ retry: } var check api.SSHCheckPrincipalResponse if err := readJSON(resp.Body, &check); err != nil { - return nil, errs.Wrapf(http.StatusInternalServerError, err, "error reading %s response", u, - errs.WithMessage("Failed to parse response from /ssh/check-host endpoint")) + return nil, errs.Wrapf(http.StatusInternalServerError, err, "error reading %s response", + []interface{}{u, errs.WithMessage("Failed to parse response from /ssh/check-host endpoint")}) } return &check, nil } diff --git a/ca/client_test.go b/ca/client_test.go index 5b74f5cb..f880c876 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -163,8 +163,8 @@ func TestClient_Version(t *testing.T) { expectedErr error }{ {"ok", ok, 200, false, nil}, - {"500", errs.InternalServerError(errors.New("force")), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)}, - {"404", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, + {"500", errs.InternalServer("force"), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)}, + {"404", errs.NotFound("force"), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -214,7 +214,7 @@ func TestClient_Health(t *testing.T) { expectedErr error }{ {"ok", ok, 200, false, nil}, - {"not ok", errs.InternalServerError(errors.New("force")), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)}, + {"not ok", errs.InternalServer("force"), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -268,7 +268,7 @@ func TestClient_Root(t *testing.T) { expectedErr error }{ {"ok", "a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d", ok, 200, false, nil}, - {"not found", "invalid", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, + {"not found", "invalid", errs.NotFound("force"), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -336,9 +336,9 @@ func TestClient_Sign(t *testing.T) { expectedErr error }{ {"ok", request, ok, 200, false, nil}, - {"unauthorized", request, errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, - {"empty request", &api.SignRequest{}, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, - {"nil request", nil, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"unauthorized", request, errs.Unauthorized("force"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"empty request", &api.SignRequest{}, errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"nil request", nil, errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -409,8 +409,8 @@ func TestClient_Revoke(t *testing.T) { expectedErr error }{ {"ok", request, ok, 200, false, nil}, - {"unauthorized", request, errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, - {"nil request", nil, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"unauthorized", request, errs.Unauthorized("force"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"nil request", nil, errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -483,9 +483,9 @@ func TestClient_Renew(t *testing.T) { err error }{ {"ok", ok, 200, false, nil}, - {"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, - {"empty request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, - {"nil request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"unauthorized", errs.Unauthorized("force"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"empty request", errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"nil request", errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -533,7 +533,7 @@ func TestClient_Provisioners(t *testing.T) { ok := &api.ProvisionersResponse{ Provisioners: provisioner.List{}, } - internalServerError := errs.InternalServerError(fmt.Errorf("Internal Server Error")) + internalServerError := errs.InternalServer("Internal Server Error") tests := []struct { name string @@ -603,7 +603,7 @@ func TestClient_ProvisionerKey(t *testing.T) { err error }{ {"ok", "kid", ok, 200, false, nil}, - {"fail", "invalid", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, + {"fail", "invalid", errs.NotFound("force"), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -665,8 +665,8 @@ func TestClient_Roots(t *testing.T) { err error }{ {"ok", ok, 200, false, nil}, - {"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, - {"bad-request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"unauthorized", errs.Unauthorized("force"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"bad-request", errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -724,7 +724,7 @@ func TestClient_Federation(t *testing.T) { err error }{ {"ok", ok, 200, false, nil}, - {"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, + {"unauthorized", errs.Unauthorized("force"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -786,7 +786,7 @@ func TestClient_SSHRoots(t *testing.T) { err error }{ {"ok", ok, 200, false, nil}, - {"not found", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)}, + {"not found", errs.NotFound("force"), 404, true, errors.New(errs.NotFoundDefaultMsg)}, } srv := httptest.NewServer(nil) @@ -869,7 +869,7 @@ func Test_parseEndpoint(t *testing.T) { func TestClient_RootFingerprint(t *testing.T) { ok := &api.HealthResponse{Status: "ok"} - nok := errs.InternalServerError(fmt.Errorf("Internal Server Error")) + nok := errs.InternalServer("Internal Server Error") httpsServer := httptest.NewTLSServer(nil) defer httpsServer.Close() @@ -947,7 +947,7 @@ func TestClient_SSHBastion(t *testing.T) { }{ {"ok", &api.SSHBastionRequest{Hostname: "host.local"}, ok, 200, false, nil}, {"bad-response", &api.SSHBastionRequest{Hostname: "host.local"}, "bad json", 200, true, nil}, - {"bad-request", &api.SSHBastionRequest{}, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)}, + {"bad-request", &api.SSHBastionRequest{}, errs.BadRequest("force"), 400, true, errors.New(errs.BadRequestDefaultMsg)}, } srv := httptest.NewServer(nil) diff --git a/errs/error.go b/errs/error.go index adae017e..2e49d8c5 100644 --- a/errs/error.go +++ b/errs/error.go @@ -62,31 +62,6 @@ type Error struct { Details map[string]interface{} } -// New returns a new Error. If the given error implements the StatusCoder -// interface we will ignore the given status. -func New(status int, err error, opts ...Option) error { - var ( - e *Error - ok bool - ) - if e, ok = err.(*Error); !ok { - if sc, ok := err.(StatusCoder); ok { - e = &Error{Status: sc.StatusCode(), Err: err} - } else { - cause := errors.Cause(err) - if sc, ok := cause.(StatusCoder); ok { - e = &Error{Status: sc.StatusCode(), Err: err} - } else { - e = &Error{Status: status, Err: err} - } - } - } - for _, o := range opts { - o(e) - } - return e -} - // ErrorResponse represents an error in JSON format. type ErrorResponse struct { Status int `json:"status"` @@ -119,10 +94,11 @@ func (e *Error) Message() string { // Wrap returns an error annotating err with a stack trace at the point Wrap is // called, and the supplied message. If err is nil, Wrap returns nil. -func Wrap(status int, e error, m string, opts ...Option) error { +func Wrap(status int, e error, m string, args ...interface{}) error { if e == nil { return nil } + _, opts := splitOptionArgs(args) if err, ok := e.(*Error); ok { err.Err = errors.Wrap(err.Err, m) e = err @@ -138,25 +114,12 @@ func Wrapf(status int, e error, format string, args ...interface{}) error { if e == nil { return nil } - var opts []Option - for i, arg := range args { - // Once we find the first Option, assume that all further arguments are Options. - if _, ok := arg.(Option); ok { - for _, a := range args[i:] { - // Ignore any arguments after the first Option that are not Options. - if opt, ok := a.(Option); ok { - opts = append(opts, opt) - } - } - args = args[:i] - break - } - } + as, opts := splitOptionArgs(args) if err, ok := e.(*Error); ok { err.Err = errors.Wrapf(err.Err, format, args...) e = err } else { - e = errors.Wrapf(e, format, args...) + e = errors.Wrapf(e, format, as...) } return StatusCodeError(status, e, opts...) } @@ -201,24 +164,24 @@ type Messenger interface { func StatusCodeError(code int, e error, opts ...Option) error { switch code { case http.StatusBadRequest: - return BadRequest(e, opts...) + return BadRequestErr(e, opts...) case http.StatusUnauthorized: - return Unauthorized(e, opts...) + return UnauthorizedErr(e, opts...) case http.StatusForbidden: - return Forbidden(e, opts...) + return ForbiddenErr(e, opts...) case http.StatusInternalServerError: - return InternalServerError(e, opts...) + return InternalServerErr(e, opts...) case http.StatusNotImplemented: - return NotImplemented(e, opts...) + return NotImplementedErr(e, opts...) default: - return UnexpectedError(code, e, opts...) + return UnexpectedErr(code, e, opts...) } } var ( seeLogs = "Please see the certificate authority logs for more info." // BadRequestDefaultMsg 400 default msg - BadRequestDefaultMsg = "The request could not be completed due to being poorly formatted or missing critical data. " + seeLogs + BadRequestDefaultMsg = "The request could not be completed; malformed or missing data" + seeLogs // UnauthorizedDefaultMsg 401 default msg UnauthorizedDefaultMsg = "The request lacked necessary authorization to be completed. " + seeLogs // ForbiddenDefaultMsg 403 default msg @@ -231,46 +194,142 @@ var ( NotImplementedDefaultMsg = "The requested method is not implemented by the certificate authority. " + seeLogs ) -// InternalServerError returns a 500 error with the given error. -func InternalServerError(err error, opts ...Option) error { +// splitOptionArgs splits the variadic length args into string formatting args +// and Option(s) to apply to an Error. +func splitOptionArgs(args []interface{}) ([]interface{}, []Option) { + indexOptionStart := -1 + for i, a := range args { + if _, ok := a.(Option); ok { + indexOptionStart = i + break + } + } + + if indexOptionStart < 0 { + return args, []Option{} + } + opts := []Option{} + // Ignore any non-Option args that come after the first Option. + for _, o := range args[indexOptionStart:] { + if opt, ok := o.(Option); ok { + opts = append(opts, opt) + } + } + return args[:indexOptionStart], opts +} + +// NewErr returns a new Error. If the given error implements the StatusCoder +// interface we will ignore the given status. +func NewErr(status int, err error, opts ...Option) error { + var ( + e *Error + ok bool + ) + if e, ok = err.(*Error); !ok { + if sc, ok := err.(StatusCoder); ok { + e = &Error{Status: sc.StatusCode(), Err: err} + } else { + cause := errors.Cause(err) + if sc, ok := cause.(StatusCoder); ok { + e = &Error{Status: sc.StatusCode(), Err: err} + } else { + e = &Error{Status: status, Err: err} + } + } + } + for _, o := range opts { + o(e) + } + return e +} + +// Errorf creates a new error using the given format and status code. +func Errorf(code int, format string, args ...interface{}) error { + as, opts := splitOptionArgs(args) + opts = append(opts, withDefaultMessage(NotImplementedDefaultMsg)) + e := &Error{Status: code, Err: fmt.Errorf(format, as...)} + for _, o := range opts { + o(e) + } + return e +} + +// InternalServer creates a 500 error with the given format and arguments. +func InternalServer(format string, args ...interface{}) error { + args = append(args, withDefaultMessage(InternalServerErrorDefaultMsg)) + return Errorf(http.StatusInternalServerError, format, args...) +} + +// InternalServerErr returns a 500 error with the given error. +func InternalServerErr(err error, opts ...Option) error { opts = append(opts, withDefaultMessage(InternalServerErrorDefaultMsg)) - return New(http.StatusInternalServerError, err, opts...) + return NewErr(http.StatusInternalServerError, err, opts...) } -// NotImplemented returns a 501 error with the given error. -func NotImplemented(err error, opts ...Option) error { +// NotImplemented creates a 501 error with the given format and arguments. +func NotImplemented(format string, args ...interface{}) error { + args = append(args, withDefaultMessage(NotImplementedDefaultMsg)) + return Errorf(http.StatusNotImplemented, format, args...) +} + +// NotImplementedErr returns a 501 error with the given error. +func NotImplementedErr(err error, opts ...Option) error { opts = append(opts, withDefaultMessage(NotImplementedDefaultMsg)) - return New(http.StatusNotImplemented, err, opts...) + return NewErr(http.StatusNotImplemented, err, opts...) +} + +// BadRequest creates a 400 error with the given format and arguments. +func BadRequest(format string, args ...interface{}) error { + args = append(args, withDefaultMessage(BadRequestDefaultMsg)) + return Errorf(http.StatusBadRequest, format, args...) } -// BadRequest returns an 400 error with the given error. -func BadRequest(err error, opts ...Option) error { +// BadRequestErr returns an 400 error with the given error. +func BadRequestErr(err error, opts ...Option) error { opts = append(opts, withDefaultMessage(BadRequestDefaultMsg)) - return New(http.StatusBadRequest, err, opts...) + return NewErr(http.StatusBadRequest, err, opts...) +} + +// Unauthorized creates a 401 error with the given format and arguments. +func Unauthorized(format string, args ...interface{}) error { + args = append(args, withDefaultMessage(UnauthorizedDefaultMsg)) + return Errorf(http.StatusUnauthorized, format, args...) } -// Unauthorized returns an 401 error with the given error. -func Unauthorized(err error, opts ...Option) error { +// UnauthorizedErr returns an 401 error with the given error. +func UnauthorizedErr(err error, opts ...Option) error { opts = append(opts, withDefaultMessage(UnauthorizedDefaultMsg)) - return New(http.StatusUnauthorized, err, opts...) + return NewErr(http.StatusUnauthorized, err, opts...) } -// Forbidden returns an 403 error with the given error. -func Forbidden(err error, opts ...Option) error { +// Forbidden creates a 403 error with the given format and arguments. +func Forbidden(format string, args ...interface{}) error { + args = append(args, withDefaultMessage(ForbiddenDefaultMsg)) + return Errorf(http.StatusForbidden, format, args...) +} + +// ForbiddenErr returns an 403 error with the given error. +func ForbiddenErr(err error, opts ...Option) error { opts = append(opts, withDefaultMessage(ForbiddenDefaultMsg)) - return New(http.StatusForbidden, err, opts...) + return NewErr(http.StatusForbidden, err, opts...) +} + +// NotFound creates a 404 error with the given format and arguments. +func NotFound(format string, args ...interface{}) error { + args = append(args, withDefaultMessage(NotFoundDefaultMsg)) + return Errorf(http.StatusNotFound, format, args...) } -// NotFound returns an 404 error with the given error. -func NotFound(err error, opts ...Option) error { +// NotFoundErr returns an 404 error with the given error. +func NotFoundErr(err error, opts ...Option) error { opts = append(opts, withDefaultMessage(NotFoundDefaultMsg)) - return New(http.StatusNotFound, err, opts...) + return NewErr(http.StatusNotFound, err, opts...) } -// UnexpectedError will be used when the certificate authority makes an outgoing +// UnexpectedErr will be used when the certificate authority makes an outgoing // request and receives an unhandled status code. -func UnexpectedError(code int, err error, opts ...Option) error { +func UnexpectedErr(code int, err error, opts ...Option) error { opts = append(opts, withDefaultMessage("The certificate authority received an "+ "unexpected HTTP status code - '%d'. "+seeLogs, code)) - return New(code, err, opts...) + return NewErr(code, err, opts...) } From 99e5bf47822e30f8cb3fca04fb07b95c200e4639 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 23 Jan 2020 22:07:29 -0800 Subject: [PATCH 150/163] Remove all references to old apiError. --- authority/error.go | 67 ---------------------------------- authority/provisioners_test.go | 17 ++++----- 2 files changed, 7 insertions(+), 77 deletions(-) delete mode 100644 authority/error.go diff --git a/authority/error.go b/authority/error.go deleted file mode 100644 index de1aa3c0..00000000 --- a/authority/error.go +++ /dev/null @@ -1,67 +0,0 @@ -package authority - -import ( - "encoding/json" - "fmt" - "net/http" -) - -type apiCtx map[string]interface{} - -// Error implements the api.Error interface and adds context to error messages. -type apiError struct { - err error - code int - context apiCtx -} - -// Cause implements the errors.Causer interface and returns the original error. -func (e *apiError) Cause() error { - return e.err -} - -// Error returns an error message with additional context. -func (e *apiError) Error() string { - ret := e.err.Error() - - /* - if len(e.context) > 0 { - ret += "\n\nContext:" - for k, v := range e.context { - ret += fmt.Sprintf("\n %s: %v", k, v) - } - } - */ - return ret -} - -// ErrorResponse represents an error in JSON format. -type ErrorResponse struct { - Status int `json:"status"` - Message string `json:"message"` -} - -// StatusCode returns an http status code indicating the type and severity of -// the error. -func (e *apiError) StatusCode() int { - if e.code == 0 { - return http.StatusInternalServerError - } - return e.code -} - -// MarshalJSON implements json.Marshaller interface for the Error struct. -func (e *apiError) MarshalJSON() ([]byte, error) { - return json.Marshal(&ErrorResponse{Status: e.code, Message: http.StatusText(e.code)}) -} - -// UnmarshalJSON implements json.Unmarshaler interface for the Error struct. -func (e *apiError) UnmarshalJSON(data []byte) error { - var er ErrorResponse - if err := json.Unmarshal(data, &er); err != nil { - return err - } - e.code = er.Status - e.err = fmt.Errorf(er.Message) - return nil -} diff --git a/authority/provisioners_test.go b/authority/provisioners_test.go index 1a45f209..94b2d715 100644 --- a/authority/provisioners_test.go +++ b/authority/provisioners_test.go @@ -69,8 +69,9 @@ func TestGetEncryptedKey(t *testing.T) { func TestGetProvisioners(t *testing.T) { type gp struct { - a *Authority - err *apiError + a *Authority + err error + code int } tests := map[string]func(t *testing.T) *gp{ "ok": func(t *testing.T) *gp { @@ -89,14 +90,10 @@ func TestGetProvisioners(t *testing.T) { ps, next, err := tc.a.GetProvisioners("", 0) if err != nil { if assert.NotNil(t, tc.err) { - switch v := err.(type) { - case *apiError: - assert.HasPrefix(t, v.err.Error(), tc.err.Error()) - assert.Equals(t, v.code, tc.err.code) - assert.Equals(t, v.context, tc.err.context) - default: - t.Errorf("unexpected error type: %T", v) - } + sc, ok := err.(errs.StatusCoder) + assert.Fatal(t, ok, "error does not implement StatusCoder interface") + assert.Equals(t, sc.StatusCode(), tc.code) + assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { From f21f07689eafd4c214b62a8d13a7e0eb1b15422b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 24 Jan 2020 12:26:27 -0800 Subject: [PATCH 151/163] Fix a couple of race conditions in the renewal of certificates. --- ca/mutable_tls_config.go | 2 +- ca/renew.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ca/mutable_tls_config.go b/ca/mutable_tls_config.go index 031a99e9..c4a1a89f 100644 --- a/ca/mutable_tls_config.go +++ b/ca/mutable_tls_config.go @@ -40,7 +40,7 @@ func (c *mutableTLSConfig) Init(base *tls.Config) { // tls.Config GetConfigForClient. func (c *mutableTLSConfig) TLSConfig() (config *tls.Config) { c.RLock() - config = c.config + config = c.config.Clone() c.RUnlock() return } diff --git a/ca/renew.go b/ca/renew.go index 6a4fd22b..13f96ff7 100644 --- a/ca/renew.go +++ b/ca/renew.go @@ -80,7 +80,9 @@ func NewTLSRenewer(cert *tls.Certificate, fn RenewFunc, opts ...tlsRenewerOption func (r *TLSRenewer) Run() { cert := r.getCertificate() next := r.nextRenewDuration(cert.Leaf.NotAfter) + r.Lock() r.timer = time.AfterFunc(next, r.renewCertificate) + r.Unlock() } // RunContext starts the certificate renewer for the given certificate. From 1e5763031b236a0cdb0e81a7b56e691bb71b4061 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 24 Jan 2020 13:42:00 -0800 Subject: [PATCH 152/163] Add backdate validation to sshCertValidityValidator. --- authority/provisioner/sign_options.go | 2 +- authority/provisioner/sign_ssh_options.go | 21 +++++------ .../provisioner/sign_ssh_options_test.go | 35 +++++++++++++++++-- authority/provisioner/ssh_test.go | 2 +- authority/ssh.go | 6 ++-- authority/ssh_test.go | 2 +- authority/tls_test.go | 2 +- 7 files changed, 47 insertions(+), 23 deletions(-) diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index ed049b6c..90c2cd40 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -290,7 +290,7 @@ func (v *validityValidator) Valid(cert *x509.Certificate, o Options) error { // apply a backdate). This is good enough. if d > v.max+o.Backdate { return errors.Errorf("requested duration of %v is more than the authorized maximum certificate duration of %v", - d, v.max) + d, v.max+o.Backdate) } return nil } diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index b0ab78ea..34bc069f 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -36,7 +36,7 @@ type SSHCertOptionModifier interface { // SSHCertValidator is the interface used to validate an SSH certificate. type SSHCertValidator interface { SignOption - Valid(cert *ssh.Certificate) error + Valid(cert *ssh.Certificate, opts SSHOptions) error } // SSHCertOptionsValidator is the interface used to validate the custom @@ -310,7 +310,7 @@ type sshCertValidityValidator struct { *Claimer } -func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate) error { +func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SSHOptions) error { switch { case cert.ValidAfter == 0: return errors.New("ssh certificate validAfter cannot be 0") @@ -336,20 +336,15 @@ func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate) error { // To not take into account the backdate, time.Now() will be used to // calculate the duration if ValidAfter is in the past. - var dur time.Duration - if t := now().Unix(); t > int64(cert.ValidAfter) { - dur = time.Duration(int64(cert.ValidBefore)-t) * time.Second - } else { - dur = time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second - } + dur := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second switch { case dur < min: return errors.Errorf("requested duration of %s is less than minimum "+ "accepted duration for selected provisioner of %s", dur, min) - case dur > max: + case dur > max+opts.Backdate: return errors.Errorf("requested duration of %s is greater than maximum "+ - "accepted duration for selected provisioner of %s", dur, max) + "accepted duration for selected provisioner of %s", dur, max+opts.Backdate) default: return nil } @@ -360,7 +355,7 @@ func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate) error { type sshCertDefaultValidator struct{} // Valid returns an error if the given certificate does not contain the necessary fields. -func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate) error { +func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate, o SSHOptions) error { switch { case len(cert.Nonce) == 0: return errors.New("ssh certificate nonce cannot be empty") @@ -395,7 +390,7 @@ func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate) error { type sshDefaultPublicKeyValidator struct{} // Valid checks that certificate request common name matches the one configured. -func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate) error { +func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SSHOptions) error { if cert.Key == nil { return errors.New("ssh certificate key cannot be nil") } @@ -425,7 +420,7 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate) error { type sshCertKeyIDValidator string // Valid returns an error if the given certificate does not contain the necessary fields. -func (v sshCertKeyIDValidator) Valid(cert *ssh.Certificate) error { +func (v sshCertKeyIDValidator) Valid(cert *ssh.Certificate, o SSHOptions) error { if string(v) != cert.KeyId { return errors.Errorf("invalid ssh certificate KeyId; want %s, but got %s", string(v), cert.KeyId) } diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index c13e46da..940fc0e2 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -659,7 +659,7 @@ func Test_sshCertDefaultValidator_Valid(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := v.Valid(tt.cert); err != nil { + if err := v.Valid(tt.cert, SSHOptions{}); err != nil { if assert.NotNil(t, tt.err) { assert.HasPrefix(t, err.Error(), tt.err.Error()) } @@ -678,26 +678,31 @@ func Test_sshCertValidityValidator(t *testing.T) { tests := []struct { name string cert *ssh.Certificate + opts SSHOptions err error }{ { "fail/validAfter-0", &ssh.Certificate{CertType: ssh.UserCert}, + SSHOptions{}, errors.New("ssh certificate validAfter cannot be 0"), }, { "fail/validBefore-in-past", &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(-time.Minute).Unix())}, + SSHOptions{}, errors.New("ssh certificate validBefore cannot be in the past"), }, { "fail/validBefore-before-validAfter", &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Add(5 * time.Minute).Unix()), ValidBefore: uint64(now().Add(3 * time.Minute).Unix())}, + SSHOptions{}, errors.New("ssh certificate validBefore cannot be before validAfter"), }, { "fail/cert-type-not-set", &ssh.Certificate{ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix())}, + SSHOptions{}, errors.New("ssh certificate type has not been set"), }, { @@ -707,6 +712,7 @@ func Test_sshCertValidityValidator(t *testing.T) { ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix()), }, + SSHOptions{}, errors.New("unknown ssh certificate type 3"), }, { @@ -716,8 +722,19 @@ func Test_sshCertValidityValidator(t *testing.T) { ValidAfter: uint64(n.Unix()), ValidBefore: uint64(n.Add(4 * time.Minute).Unix()), }, + SSHOptions{Backdate: time.Second}, errors.New("requested duration of 4m0s is less than minimum accepted duration for selected provisioner of 5m0s"), }, + { + "ok/duration-exactly-min", + &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(5 * time.Minute).Unix()), + }, + SSHOptions{Backdate: time.Second}, + nil, + }, { "fail/duration>max", &ssh.Certificate{ @@ -725,7 +742,18 @@ func Test_sshCertValidityValidator(t *testing.T) { ValidAfter: uint64(n.Unix()), ValidBefore: uint64(n.Add(48 * time.Hour).Unix()), }, - errors.New("requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m0s"), + SSHOptions{Backdate: time.Second}, + errors.New("requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m1s"), + }, + { + "ok/duration-exactly-max", + &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(24*time.Hour + time.Second).Unix()), + }, + SSHOptions{Backdate: time.Second}, + nil, }, { "ok", @@ -734,12 +762,13 @@ func Test_sshCertValidityValidator(t *testing.T) { ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(8 * time.Hour).Unix()), }, + SSHOptions{Backdate: time.Second}, nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := v.Valid(tt.cert); err != nil { + if err := v.Valid(tt.cert, tt.opts); err != nil { if assert.NotNil(t, tt.err) { assert.HasPrefix(t, err.Error(), tt.err.Error()) } diff --git a/authority/provisioner/ssh_test.go b/authority/provisioner/ssh_test.go index 84860a75..be102a1a 100644 --- a/authority/provisioner/ssh_test.go +++ b/authority/provisioner/ssh_test.go @@ -116,7 +116,7 @@ func signSSHCertificate(key crypto.PublicKey, opts SSHOptions, signOpts []SignOp // User provisioners validators for _, v := range validators { - if err := v.Valid(cert); err != nil { + if err := v.Valid(cert, opts); err != nil { return nil, err } } diff --git a/authority/ssh.go b/authority/ssh.go index 28066556..f47447d5 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -269,7 +269,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // User provisioners validators for _, v := range validators { - if err := v.Valid(cert); err != nil { + if err := v.Valid(cert, opts); err != nil { return nil, errs.Wrap(http.StatusForbidden, err, "signSSH") } } @@ -428,9 +428,9 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp } cert.Signature = sig - // Apply validators from provisioner.. + // Apply validators from provisioner. for _, v := range validators { - if err := v.Valid(cert); err != nil { + if err := v.Valid(cert, provisioner.SSHOptions{Backdate: backdate}); err != nil { return nil, errs.Wrap(http.StatusForbidden, err, "rekeySSH") } } diff --git a/authority/ssh_test.go b/authority/ssh_test.go index cc3f164c..b581740f 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -62,7 +62,7 @@ func (m sshTestCertModifier) Modify(cert *ssh.Certificate) error { type sshTestCertValidator string -func (v sshTestCertValidator) Valid(crt *ssh.Certificate) error { +func (v sshTestCertValidator) Valid(crt *ssh.Certificate, opts provisioner.SSHOptions) error { if v == "" { return nil } diff --git a/authority/tls_test.go b/authority/tls_test.go index 3fbd21bf..f946022f 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -178,7 +178,7 @@ func TestAuthority_Sign(t *testing.T) { csr: csr, extraOpts: extraOpts, signOpts: _signOpts, - err: errors.New("authority.Sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s"), + err: errors.New("authority.Sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h1m0s"), code: http.StatusUnauthorized, } }, From 92c48949d7113372b750e5efcc44f41619c01f51 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 24 Jan 2020 13:45:11 -0800 Subject: [PATCH 153/163] Remove test that is no longer implemented by the method. --- authority/config_test.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/authority/config_test.go b/authority/config_test.go index c8767dd1..d049d47e 100644 --- a/authority/config_test.go +++ b/authority/config_test.go @@ -255,19 +255,6 @@ func TestAuthConfigValidate(t *testing.T) { err: errors.New("authority cannot be undefined"), } }, - /* - "fail-invalid-claims": func(t *testing.T) AuthConfigValidateTest { - return AuthConfigValidateTest{ - ac: &AuthConfig{ - Provisioners: p, - Claims: &provisioner.Claims{ - MinTLSDur: &provisioner.Duration{Duration: -1}, - }, - }, - err: errors.New("claims: MinTLSCertDuration must be greater than 0"), - } - }, - */ "ok-empty-provisioners": func(t *testing.T) AuthConfigValidateTest { return AuthConfigValidateTest{ ac: &AuthConfig{}, From 4eaeede77ded62aa931564c55b244ccfb8345765 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 11 Feb 2020 14:05:37 -0800 Subject: [PATCH 154/163] Fix unit tests. --- authority/authority_test.go | 3 ++- authority/tls_test.go | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) 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/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) } From 43bd8113aaba57b2e64238e9478f34dbc6f5cb71 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 11 Feb 2020 14:46:18 -0800 Subject: [PATCH 155/163] Remove unnecessary comments. --- authority/tls.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index 4ea2af43..18c0b524 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 @@ -132,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) @@ -313,8 +309,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.x509Issuer, a.x509Signer, + 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") From 2d4f369db2f7c45ce580d59167d7f692a9297842 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 12 Feb 2020 15:36:24 -0800 Subject: [PATCH 156/163] Add options to set root and federated certificates using x509.Certificate --- authority/options.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/authority/options.go b/authority/options.go index 2eb62e16..2d655a2b 100644 --- a/authority/options.go +++ b/authority/options.go @@ -113,22 +113,42 @@ func WithSSHHostSigner(s crypto.Signer) Option { } } +// 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. +// 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 } - x509.NewCertPool() a.rootX509Certs = certs return nil } } // WithX509FederatedBundle is an option that allows to define the list of -// federated certificates. +// 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) From cff346e7fdf591433bf750890f2537b4545bdc60 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 13 Feb 2020 15:49:16 -0800 Subject: [PATCH 157/163] Skip test on travis, it won't fail because they have access to the KMS. --- kms/cloudkms/cloudkms_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/kms/cloudkms/cloudkms_test.go b/kms/cloudkms/cloudkms_test.go index 1776ee17..b4f92fa6 100644 --- a/kms/cloudkms/cloudkms_test.go +++ b/kms/cloudkms/cloudkms_test.go @@ -5,6 +5,7 @@ import ( "crypto" "fmt" "io/ioutil" + "os" "reflect" "testing" @@ -54,16 +55,21 @@ func TestNew(t *testing.T) { opts apiv1.Options } tests := []struct { - name string - args args - want *CloudKMS - wantErr bool + name string + skipOnCI bool + args args + want *CloudKMS + wantErr bool }{ - {"fail authentication", args{context.Background(), apiv1.Options{}}, nil, true}, - {"fail credentials", args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true}, + {"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) From 05cc1437b78cbb9f7f78f00864eab8dbc5901524 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 13 Feb 2020 17:48:43 -0800 Subject: [PATCH 158/163] Remove unnecessary parse of certificate. --- authority/tls.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index 18c0b524..4480314c 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -193,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(a.x509Issuer.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 { @@ -205,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. From b11bbd572844f442bcd61eaa1ab3e13deed74bf4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 13 Feb 2020 18:34:43 -0800 Subject: [PATCH 159/163] Fix typo. --- kms/cloudkms/cloudkms.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index afa840a2..a2332d5f 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -173,7 +173,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo // Create a new version if the key already exists. // // Note that it will have the same purpose, protection level and - // algorithm than the previous one. + // algorithm than as previous one. req := &kmspb.CreateCryptoKeyVersionRequest{ Parent: req.Name, CryptoKeyVersion: &kmspb.CryptoKeyVersion{ From 6987a46b76668a49ed72c293e09672cffad2a047 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 13 Feb 2020 18:38:00 -0800 Subject: [PATCH 160/163] Skip test on travis. --- kms/kms_test.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/kms/kms_test.go b/kms/kms_test.go index 9b041e59..f377072f 100644 --- a/kms/kms_test.go +++ b/kms/kms_test.go @@ -2,6 +2,7 @@ package kms import ( "context" + "os" "reflect" "testing" @@ -18,20 +19,25 @@ func TestNew(t *testing.T) { opts apiv1.Options } tests := []struct { - name string - args args - want KeyManager - wantErr bool + name string + skipOnCI bool + args args + want KeyManager + wantErr bool }{ - {"softkms", args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false}, - {"default", args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, false}, - {"cloudkms", args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, true}, // fails because not credentials - {"awskms", args{ctx, apiv1.Options{Type: "awskms"}}, nil, true}, // not yet supported - {"pkcs11", args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not yet supported - {"fail validation", args{ctx, apiv1.Options{Type: "foobar"}}, nil, true}, + {"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) From b724f5a33865e296dfebf4b907ff1f7a0e808dba Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 14 Feb 2020 10:24:56 -0800 Subject: [PATCH 161/163] Fix typos. --- kms/softkms/softkms.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index b5ec6468..2316e87e 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -44,10 +44,10 @@ var generateKey = func(kty, crv string, size int) (interface{}, interface{}, err return keys.GenerateKeyPair(kty, crv, size) } -// SoftKSM is a key manager that uses keys stored in disk. +// SoftKMS is a key manager that uses keys stored in disk. type SoftKMS struct{} -// New returns a new SoftKSM. +// New returns a new SoftKMS. func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { return &SoftKMS{}, nil } From dd5a96a42ec5e4dabe359585f6e4e30b7fa273ab Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 14 Feb 2020 10:42:50 -0800 Subject: [PATCH 162/163] Fix typo. --- kms/softkms/softkms.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index 2316e87e..68d2dc3f 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -52,7 +52,7 @@ func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { return &SoftKMS{}, nil } -// Closes is a noop that just returns nil. +// Close is a noop that just returns nil. func (k *SoftKMS) Close() error { return nil } From 5c8c741fabbba8ae3e8a07df706d081b3efa18c5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 14 Feb 2020 11:46:31 -0800 Subject: [PATCH 163/163] Fix linting issues. --- authority/authority.go | 16 ---------------- kms/softkms/softkms.go | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 1ea0936c..0730fe5e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -290,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/kms/softkms/softkms.go b/kms/softkms/softkms.go index 68d2dc3f..fb38a1c5 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -23,17 +23,17 @@ type algorithmAttributes struct { const DefaultRSAKeySize = 3072 var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{ - apiv1.UnspecifiedSignAlgorithm: algorithmAttributes{"EC", "P-256"}, - apiv1.SHA256WithRSA: algorithmAttributes{"RSA", ""}, - apiv1.SHA384WithRSA: algorithmAttributes{"RSA", ""}, - apiv1.SHA512WithRSA: algorithmAttributes{"RSA", ""}, - apiv1.SHA256WithRSAPSS: algorithmAttributes{"RSA", ""}, - apiv1.SHA384WithRSAPSS: algorithmAttributes{"RSA", ""}, - apiv1.SHA512WithRSAPSS: algorithmAttributes{"RSA", ""}, - apiv1.ECDSAWithSHA256: algorithmAttributes{"EC", "P-256"}, - apiv1.ECDSAWithSHA384: algorithmAttributes{"EC", "P-384"}, - apiv1.ECDSAWithSHA512: algorithmAttributes{"EC", "P-521"}, - apiv1.PureEd25519: algorithmAttributes{"OKP", "Ed25519"}, + 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.