From a995cca418e8357440f9e62d3627c9a9fab81780 Mon Sep 17 00:00:00 2001 From: Oleksandr Kovalchuk Date: Fri, 20 Dec 2019 19:17:53 +0200 Subject: [PATCH 001/143] Perform domain normalization for wildcard domains Perform domain normalization for wildcard domains, so we do query TXT records for _acme-challenge.example.domain instead of _acme-challenge.*.example.domain when performing DNS-01 challenge. In this way the behavior is consistent with letsencrypt and records queried are in sync with the ones that are shown in certbot manual mode. --- acme/challenge.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index 4fa39668..687f3f3c 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -385,11 +385,21 @@ func (dc *dns01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validat return dc, nil } - txtRecords, err := vo.lookupTxt("_acme-challenge." + dc.Value) + // Normalize domain for wildcard DNS names + // This is done to avoid making TXT lookups for domains like + // _acme-challenge.*.example.com + // Instead perform txt lookup for _acme-challenge.example.com + domain := dc.Value + if strings.HasPrefix(domain, "*") { + domain = strings.TrimPrefix(domain, "*.") + } + + txtRecords, err := vo.lookupTxt("_acme-challenge." + domain) + fmt.Printf("Lookup TXT for _acme-challenge." + domain) if err != nil { if err = dc.storeError(db, DNSErr(errors.Wrapf(err, "error looking up TXT "+ - "records for domain %s", dc.Value))); err != nil { + "records for domain %s", domain))); err != nil { return nil, err } return dc, nil From 46832bb9b3d15c39187b60ca0d6ec0832d90bec3 Mon Sep 17 00:00:00 2001 From: Oleksandr Kovalchuk Date: Fri, 20 Dec 2019 22:22:12 +0200 Subject: [PATCH 002/143] Remove superflurous Printf statement The statement was used for debug purposes and should not be included in the final build --- acme/challenge.go | 1 - 1 file changed, 1 deletion(-) diff --git a/acme/challenge.go b/acme/challenge.go index 687f3f3c..1c040f5d 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -395,7 +395,6 @@ func (dc *dns01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validat } txtRecords, err := vo.lookupTxt("_acme-challenge." + domain) - fmt.Printf("Lookup TXT for _acme-challenge." + domain) if err != nil { if err = dc.storeError(db, DNSErr(errors.Wrapf(err, "error looking up TXT "+ From ec8ff0bcedc34c1c0457da7f60b067abef54896e Mon Sep 17 00:00:00 2001 From: Oleksandr Kovalchuk Date: Fri, 20 Dec 2019 22:54:41 +0200 Subject: [PATCH 003/143] Add testcase which ensures we pass correct domain to lookupTxt Make sure we do not pass domains with asterisk (wildcard) in the middle, like _acme-challenge.*.example.com to lookupTxt function, but preprocess domain and remove leading wildcard so we lookup for _acme-challenge.example.com. --- acme/challenge_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 6291803d..720321e5 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -930,6 +930,47 @@ func TestDNS01Validate(t *testing.T) { res: ch, } }, + "ok/lookup-txt-wildcard": func(t *testing.T) test { + ch, err := newDNSCh() + assert.FatalError(t, err) + _ch, ok := ch.(*dns01Challenge) + assert.Fatal(t, ok) + _ch.baseChallenge.Value = "*.zap.internal" + + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + + expKeyAuth, err := KeyAuthorization(ch.getToken(), jwk) + assert.FatalError(t, err) + h := sha256.Sum256([]byte(expKeyAuth)) + expected := base64.RawURLEncoding.EncodeToString(h[:]) + + baseClone := ch.clone() + baseClone.Status = StatusValid + baseClone.Error = nil + newCh := &dns01Challenge{baseClone} + + return test{ + ch: ch, + res: newCh, + vo: validateOptions{ + lookupTxt: func(url string) ([]string, error) { + assert.Equals(t, url, "_acme-challenge.zap.internal") + return []string{"foo", expected}, nil + }, + }, + jwk: jwk, + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { + dnsCh, err := unmarshalChallenge(newval) + assert.FatalError(t, err) + assert.Equals(t, dnsCh.getStatus(), StatusValid) + baseClone.Validated = dnsCh.getValidated() + return nil, true, nil + }, + }, + } + }, "fail/key-authorization-gen-error": func(t *testing.T) test { ch, err := newDNSCh() assert.FatalError(t, err) From 967e86a48bfc0fd0729c366b33b74ed083f4bcfd Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 20 Dec 2019 13:32:44 -0800 Subject: [PATCH 004/143] Simplify trimming *. prefix of domain in acme dns validation. --- acme/challenge.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index 1c040f5d..f0180f64 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -389,10 +389,7 @@ func (dc *dns01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validat // This is done to avoid making TXT lookups for domains like // _acme-challenge.*.example.com // Instead perform txt lookup for _acme-challenge.example.com - domain := dc.Value - if strings.HasPrefix(domain, "*") { - domain = strings.TrimPrefix(domain, "*.") - } + domain := strings.TrimPrefix(dc.Value, "*.") txtRecords, err := vo.lookupTxt("_acme-challenge." + domain) if err != nil { From a197158426e882a0660178770a75a392e43ed509 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 24 Sep 2019 19:12:13 -0700 Subject: [PATCH 005/143] 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 961be1fbc784f73779968ad76075e999aeefaa47 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 26 Sep 2019 13:22:07 -0700 Subject: [PATCH 006/143] 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 8f08b47a9c7612fb82b187b2bf286ad4c383b985 Mon Sep 17 00:00:00 2001 From: Alan Christopher Thomas Date: Tue, 10 Sep 2019 16:29:03 -0600 Subject: [PATCH 007/143] 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 d4c47cf3e171a072ae409c4cf9b06f8fb1045345 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 27 Sep 2019 19:05:53 -0700 Subject: [PATCH 008/143] 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 b000b59ee69721b0a94aff29320e8aecc5b89d37 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 30 Sep 2019 15:10:23 -0700 Subject: [PATCH 009/143] 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 a35988ff08dac2433aa1297c0eb73c53d97a1fd2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 3 Oct 2019 19:03:38 -0700 Subject: [PATCH 010/143] 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 91130b9c3fed97d3875b3dfa066c491d72af62e4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 4 Oct 2019 17:08:42 -0700 Subject: [PATCH 011/143] 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 b5bc249e1cf04620bad715789820c8edc727e318 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Oct 2019 18:09:41 -0700 Subject: [PATCH 012/143] Add support for multiple ssh roots. Fixes #125 --- api/api.go | 1 + api/ssh.go | 52 +++++++++++++++++++------ authority/authority.go | 48 ++++++++++++++++++----- authority/config.go | 13 +++---- authority/ssh.go | 86 ++++++++++++++++++++++++++++++++++-------- ca/client.go | 20 +++++++++- 6 files changed, 174 insertions(+), 46 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 keys.UserKey != nil { - user = &SSHPublicKey{PublicKey: keys.UserKey} + if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { + WriteError(w, NotFound(errors.New("no keys found"))) + return } - JSON(w, &SSHKeysResponse{ - HostKey: host, - UserKey: user, - }) + 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) } // 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 d08db4df2358a9710aa104b5d178908347d68d13 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Oct 2019 18:35:28 -0700 Subject: [PATCH 013/143] 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 37f17213bbd76071775b53f50d3236f80886253b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Oct 2019 13:08:57 -0700 Subject: [PATCH 014/143] 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 019f6791895e4e58a26f07ce27b09dd0ec9a483c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 11:25:48 -0700 Subject: [PATCH 015/143] 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 b792d5c07d7c777f80b7ae04dd97357f8324aa2d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 12:49:09 -0700 Subject: [PATCH 016/143] 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) } + // Generate and write templates + if err := generateTemplates(config.Templates); err != nil { + return err + } + + if config.DB != nil { + 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) - if config.DB != nil { - ui.PrintSelected("Database", config.DB.DataSource) - } 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 1b0d05097b879723afd68e89abe6f4c8e5fefa19 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 18:59:50 -0700 Subject: [PATCH 017/143] 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 a7132774538f1682ca8db6333ba47f79911cf84e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 19:26:09 -0700 Subject: [PATCH 018/143] 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 08850d5334980167f520237faf761c9332a16e4a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 11 Oct 2019 19:26:59 -0700 Subject: [PATCH 019/143] 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 d880a982956866c7dc199b173ce89b99c289e1a4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Oct 2019 13:07:28 -0700 Subject: [PATCH 020/143] 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 385bf0a14a71d769056ec9d58f6ef314d7373a3b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Oct 2019 13:57:06 -0700 Subject: [PATCH 021/143] 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 4f06f3901e60629afe9b0a797b1248ab28a7c6bf Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Oct 2019 17:10:47 -0700 Subject: [PATCH 022/143] 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 e0bfbc4b621b18b4ed869c944706757771b1ddf5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Oct 2019 11:41:35 -0700 Subject: [PATCH 023/143] 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 8939caace4f61851586aabad56aeaf68708d370a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Oct 2019 12:18:29 -0700 Subject: [PATCH 024/143] 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 4b68f1611fed6be5b999b9994e5d98a33aa44b4e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Oct 2019 18:00:46 -0700 Subject: [PATCH 025/143] 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 6489c26d4c613ef17c9cbfe7f79f1e0721517847 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 22 Oct 2019 18:41:54 -0700 Subject: [PATCH 026/143] 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 ee2277826498c042070ae26d9a117bea38239496 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:37:51 -0700 Subject: [PATCH 027/143] 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 bceb12a169f5f54907838a2050a19256056dfcfa Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:40:37 -0700 Subject: [PATCH 028/143] 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 d59a07ad89d27bdd5c40213ba71d92d390b7a184 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:42:38 -0700 Subject: [PATCH 029/143] 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 ec90c41de6b1a6773f71809a7a055be3ecdca21c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 14:58:48 -0700 Subject: [PATCH 030/143] 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 c729c5f925599766e98082a6e6ce934437cd47be Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Oct 2019 18:36:02 -0700 Subject: [PATCH 031/143] 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 c7e4cc96a4684aeafad958b0ad4e5cf93199089c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 25 Oct 2019 12:23:52 -0700 Subject: [PATCH 032/143] 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 b8817ad648b5fa511d83b1a6339b859f6d32d0d5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 18:07:52 -0800 Subject: [PATCH 033/143] 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 8e794259eb9e8228b76e235578056fe43f5c17e9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 18:09:10 -0800 Subject: [PATCH 034/143] 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 b4f02a04e13783aacdea76cd49d643cb038315b2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 18:30:03 -0800 Subject: [PATCH 035/143] 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 ded808704230e79cf2ce5efaba33ad3fb84e48f3 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 4 Nov 2019 19:51:03 -0800 Subject: [PATCH 036/143] 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 5616386eed4eae297768b55305bd2e125431aa4f Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 25 Oct 2019 13:47:49 -0700 Subject: [PATCH 037/143] 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 862d704f6bb281c6051879925fb303182d0e96e6 Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 26 Oct 2019 17:40:26 -0700 Subject: [PATCH 038/143] 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 c04f1e1bd4ea37ae3f74f5d1e9297766dc02e067 Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 26 Oct 2019 17:40:43 -0700 Subject: [PATCH 039/143] 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 29853ae01611dd15e9fd6cf4fbc42a624bd412b1 Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 28 Oct 2019 11:50:43 -0700 Subject: [PATCH 040/143] 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 | 255 +++++++++++-------------- 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, 1185 insertions(+), 338 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) { + sshCert, err := ExtractSSHPOPCert(token) + if err != nil { + return nil, errors.Wrap(err, "authorizeToken ssh-pop") + } + + // 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") + } + 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") + } + 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("ssh public key could not be cast to ssh CryptoPublicKey") + } + pubKey := sshCryptoPubKey.CryptoPublicKey() - encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"] - if !ok { - return nil, errors.New("token missing sshpop header") + var ( + found bool + data = bytesForSigning(sshCert) + keys []ssh.PublicKey + ) + if sshCert.CertType == ssh.UserCert { + keys = p.sshPubKeys.UserKeys + } else { + keys = p.sshPubKeys.HostKeys } - 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 { + 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} - } - - 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()) +// 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 } - 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. + return claims.sshCert, []SignOption{ + // Validate public key &sshDefaultPublicKeyValidator{}, // Validate the validity period. &sshCertificateValidityValidator{p.claimer}, - // Require all the fields in the SSH certificate + // Require and validate all the default fields in the SSH certificate. &sshCertificateDefaultValidator{}, - ), nil + }, nil + +} + +// 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") + } + + 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.StdEncoding.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") + } + 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 0ae9bab21e80a17f1d801dd0f38346325e3a71d9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 6 Nov 2019 13:33:23 -0800 Subject: [PATCH 041/143] 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 54e3cf732245506ba230c43c4f5d8d0d61b67481 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 6 Nov 2019 15:53:16 -0800 Subject: [PATCH 042/143] 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 5788ac3f4ff02b3706a4690484a6cb31dca09b0b Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 7 Nov 2019 21:39:36 -0800 Subject: [PATCH 043/143] 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 cf592fa0e155ed4b254c4629a62078a28d5dabb5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 8 Nov 2019 17:43:54 -0800 Subject: [PATCH 044/143] 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 5c24ca81f428512337a2974a0dab436d1f4c737c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Nov 2019 11:37:54 -0800 Subject: [PATCH 045/143] 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 a8a6d0ada39c9182a2c16dbd3cd6ac63c1d43890 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 13 Nov 2019 11:18:05 -0800 Subject: [PATCH 046/143] 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 d4627d1282952b7d4c76f9a34c22bcb57c1dd4d0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 10:48:06 -0800 Subject: [PATCH 047/143] 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 39ae5636fe7ec70da9e462c428b1352d39bbea8f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 10:49:13 -0800 Subject: [PATCH 048/143] 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 29be322b1c5be5504d827b184966e4759ae8ad47 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:18:49 -0800 Subject: [PATCH 049/143] 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 7db7b1ee4c11a720b64f9fbde11847aa98e48161 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:26:37 -0800 Subject: [PATCH 050/143] 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 a86dc78b5dcfb7b4034f6784bb9d41a75724d6aa Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:27:12 -0800 Subject: [PATCH 051/143] 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 000885dea73fa3837340308ca7b50c065bbf7131 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 15:29:04 -0800 Subject: [PATCH 052/143] 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 a6edcd0a3def7326c03726b38c5b4f08124bccad Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 18:07:16 -0800 Subject: [PATCH 053/143] 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 8bf3bf701ea4f5aa89e4b8d4e6513ffa20323fc7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 18:24:58 -0800 Subject: [PATCH 054/143] 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 f9e5b27e63dfb15c8ba458bf8f49425475b4522c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 20:32:38 -0800 Subject: [PATCH 055/143] 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 f74cd04a6ac65a59acb3e33aba5937c4c2e73a78 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 14 Nov 2019 20:38:07 -0800 Subject: [PATCH 056/143] 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 710f4252ee6245043a7fbda4562cdf8bee36d013 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Nov 2019 22:09:20 -0800 Subject: [PATCH 057/143] 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 3d970b45c898dd2d781148962bb6bb06673f3ac2 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 15 Nov 2019 11:59:04 -0800 Subject: [PATCH 058/143] 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 414a94b210c25d7c0d6a73b6fc5d129c422b783e Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 15 Nov 2019 16:57:51 -0800 Subject: [PATCH 059/143] 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 | 86 +++++++++++++++++++---- 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, 202 insertions(+), 49 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,14 +415,6 @@ func (m *MockProvisioner) Init(c Config) error { return m.Merr } -// AuthorizeRevoke mock -func (m *MockProvisioner) AuthorizeRevoke(ott string) error { - if m.MauthorizeRevoke != nil { - return m.MauthorizeRevoke(ott) - } - return m.Merr -} - // AuthorizeSign mock func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]SignOption, error) { if m.MauthorizeSign != nil { @@ -407,10 +423,50 @@ func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]Sign return m.Mret1.([]SignOption), m.Merr } -// AuthorizeRenewal mock -func (m *MockProvisioner) AuthorizeRenewal(c *x509.Certificate) error { - if m.MauthorizeRenewal != nil { - return m.MauthorizeRenewal(c) +// AuthorizeRevoke mock +func (m *MockProvisioner) AuthorizeRevoke(ctx context.Context, ott string) error { + if m.MauthorizeRevoke != nil { + return m.MauthorizeRevoke(ctx, ott) + } + return m.Merr +} + +// 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 +} + +// 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 d940ab7c2063da560345b72c92c2aa8e60f9b2a8 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 20 Nov 2019 11:32:27 -0800 Subject: [PATCH 060/143] 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 d555f310dc835fdd460ad1a8cba4d1f48f31220d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 18 Nov 2019 17:07:23 -0800 Subject: [PATCH 061/143] 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 bbaf8e106edca8ca9c58364d7b06d60a5665c607 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 11:50:46 -0800 Subject: [PATCH 062/143] 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 11c8639782b20f70ce96e58b7bd0dd604cbad828 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 11:51:25 -0800 Subject: [PATCH 063/143] 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 f92bb06b6ce3475a0db880f477acd6ed4729eff3 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 20 Nov 2019 12:59:48 -0800 Subject: [PATCH 064/143] 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 db3b795eeaae80f4eebc9b61436a1d4941a4e6ff Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 16:03:31 -0800 Subject: [PATCH 065/143] 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 c60641701bb25577ed56e986aee73247cef23bb8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 17:01:31 -0800 Subject: [PATCH 066/143] 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 03bb26fb91d99cdb0922917ec6ab6b7e2c8e1e35 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 17:02:06 -0800 Subject: [PATCH 067/143] 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 50188fc901e6c630bc77d2fbcf646b10a60dd489 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 17:15:48 -0800 Subject: [PATCH 068/143] 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 656f35e5222853d9684754471d64db0c692ff295 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 20 Nov 2019 17:23:51 -0800 Subject: [PATCH 069/143] 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 9faf3348..9d04f339 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -11,6 +11,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/pemutil" "github.com/smallstep/cli/crypto/x509util" @@ -40,7 +41,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 85d38439688b6b3ff5ca6c740d0f7f88fcd0dafa Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 Nov 2019 19:11:54 -0800 Subject: [PATCH 070/143] 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 4f08a7816f18f31a854144bd873c8b4d580fda94 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 21 Nov 2019 19:06:19 -0800 Subject: [PATCH 071/143] 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 d4071108e183b350835b8090be8eb31fac45a815 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 25 Nov 2019 19:59:53 -0800 Subject: [PATCH 072/143] 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 db1b7a7f8febb157c2e05023ae5846c5f28b30d7 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 26 Nov 2019 11:55:18 -0500 Subject: [PATCH 073/143] 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 3a16835cdd36daefb3e93404a552d12112a52c78 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 17:45:32 -0800 Subject: [PATCH 074/143] 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 b179ad3662e8d5629fc2ee821efa3306ed2a9cb6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:15:34 -0800 Subject: [PATCH 075/143] 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 557a45abfa1e94fd730a2d38b761364507dcd029 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:44:43 -0800 Subject: [PATCH 076/143] 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 f469a6bb385de82ae81cc97c239bbc782c98c3e7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:47:10 -0800 Subject: [PATCH 077/143] 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 f26103d150e75a1cb428e45c94def61ac8f771b6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:47:34 -0800 Subject: [PATCH 078/143] 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 2fe07cd79c9196eb90d2fe20182557355dfc5159 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:48:28 -0800 Subject: [PATCH 079/143] 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 e29892e9ebc41ca7d3180372ed51fdbeabdfa002 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 18:53:16 -0800 Subject: [PATCH 080/143] 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 7b81bec8aa79ccf0dfeffac9b52c5dcbce2c1760 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Nov 2019 19:09:01 -0800 Subject: [PATCH 081/143] 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 d8b3e05a3f4cad360c1e608b6d5b6ba56e6f1ed5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 12:25:40 -0800 Subject: [PATCH 082/143] 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 5d7829b198e886e6b1acc99de3081e6281a8f985 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 14:27:23 -0800 Subject: [PATCH 083/143] 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 f6ffa2cc43e9c7cec78eded4d04ab9fc84210339 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 14:48:14 -0800 Subject: [PATCH 084/143] 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 f0eb12372b410d423b8fe5602be33f62471d3934 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 14:48:34 -0800 Subject: [PATCH 085/143] 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 d2b1f1547ff19fbd3cf3ad284b3ccde062f493f0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 27 Nov 2019 17:30:06 -0800 Subject: [PATCH 086/143] 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 9caadbb3411de3c2e3e1619297e3776f322115ea Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 2 Dec 2019 19:11:27 -0500 Subject: [PATCH 087/143] 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 05cda812150a5b66dbe0f31fe8d6b654f75e1402 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 4 Dec 2019 12:04:46 -0800 Subject: [PATCH 088/143] 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 0512f6e3e50d9121542ac33d6555fb7c644feb9d Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 9 Dec 2019 12:54:32 -0800 Subject: [PATCH 089/143] 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 caa2b8dbb7619c81a7a6dbc4892e70c12ed6899a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 9 Dec 2019 16:54:48 -0800 Subject: [PATCH 090/143] 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 de3ba5845545a1f2d42dcc8a4e8e9667bb0d628b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 13:10:45 -0800 Subject: [PATCH 091/143] 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 2259f62638eb10cb56ba43a072e11b48a9c0adef Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 13:40:14 -0800 Subject: [PATCH 092/143] 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 9c3349e90c02cd5dea4402d5c822f9cc55bbf03e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 13:41:06 -0800 Subject: [PATCH 093/143] 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 08eac1b00d0d3f4ed7a170109e52071a2e364708 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 16:34:01 -0800 Subject: [PATCH 094/143] 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 ab126d64054caf949ebb40ecf9b346ba83bb2d2e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 10 Dec 2019 16:34:24 -0800 Subject: [PATCH 095/143] 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 3ac388612a5cc3b469a1444106aa869798ebfded Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 9 Dec 2019 23:14:56 -0800 Subject: [PATCH 096/143] 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 a5d88808..66e97275 100644 --- a/ca/client.go +++ b/ca/client.go @@ -952,11 +952,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 93320fd977f788e0aebf8bde86e75edae4d81fbd Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 11 Dec 2019 14:56:50 -0800 Subject: [PATCH 097/143] 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 4d423137f0db5781a595046661aca54f63564cc9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 16:26:38 -0800 Subject: [PATCH 098/143] 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 c6f6493bb72da85bfaeae5a3c031ce49c1adf1e0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 16:26:53 -0800 Subject: [PATCH 099/143] 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 dedf6b17be41219a5a43558ec3eeb045f91890b2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 18:18:13 -0800 Subject: [PATCH 100/143] 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 f4615d625857b3f30f60716ebcaa3eaf56ce64f8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 18:21:20 -0800 Subject: [PATCH 101/143] 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 9e7b86342ba9da10a26cb147c38061b0aa9a0254 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 18:24:32 -0800 Subject: [PATCH 102/143] 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 d85386d0b4a25632bced21a546b692e6e85cd1de Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Dec 2019 20:23:44 -0800 Subject: [PATCH 103/143] 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 { + if err := i.Validate(); err != nil { return nil } - if err := identity.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 25144539f8a0a41bb3ba70911416ecf7efda55b6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Dec 2019 12:23:53 -0800 Subject: [PATCH 104/143] 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 524c221c6188deece0b4315317e6a287fee189f4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Dec 2019 12:48:34 -0800 Subject: [PATCH 105/143] 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 65b4dda4200846488b76718363811a05b1b128c1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Dec 2019 13:16:17 -0800 Subject: [PATCH 106/143] 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 3029addbf6b28f75c1b3b6412d7a77ffcb48e528 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 13 Dec 2019 13:56:56 -0800 Subject: [PATCH 107/143] 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 79b408dcf71a95b502db95604c083a2caf24371e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 13 Dec 2019 13:59:11 -0800 Subject: [PATCH 108/143] 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 f033422ffa8e58cfd3305aa6834dadc7a5026664 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 16 Dec 2019 11:22:24 -0800 Subject: [PATCH 109/143] 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 b9f6aacb0f2226bcb73f3b1d61c6d187c9cb0fbe Mon Sep 17 00:00:00 2001 From: max furman Date: Sun, 15 Dec 2019 23:54:25 -0800 Subject: [PATCH 110/143] 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 af8b8584dde2eb1c837f5ad0491c5503a43aac41 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 14:30:18 -0800 Subject: [PATCH 111/143] 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 74f1c111a91d2bf40879adaf25ee7d077bbf413b Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 14:31:22 -0800 Subject: [PATCH 112/143] 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 ed7ef7229f986e5f29a5801b4ccccb656c58716c Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 14:39:08 -0800 Subject: [PATCH 113/143] 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 9aafe265d0f3b05eb9b26e9516081a1a9611669f Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 17 Dec 2019 15:53:37 -0800 Subject: [PATCH 114/143] 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 14e59775bd64828890961629a2ff189c5981c444 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 12:46:46 -0800 Subject: [PATCH 115/143] 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 47f4ac1b53fda282548628a766ac0d734ea3a332 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:39:01 -0800 Subject: [PATCH 116/143] 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 c1bd1561dda5615abf83c08bfb8d2eed3c7377b5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:43:38 -0800 Subject: [PATCH 117/143] 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 ed26e97487f424a93fd53c3495da2735adf97a1d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:44:08 -0800 Subject: [PATCH 118/143] 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 e6cafb89b6a0d03665022e826f608a9b34d0f0d6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 18 Dec 2019 14:44:59 -0800 Subject: [PATCH 119/143] 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 5565d61bf30904592b38a11529f90802561f81e9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 2 Jan 2020 17:48:28 -0800 Subject: [PATCH 120/143] 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 53334ce1e0261eebdb97e09d7a9b4e345c16cd7d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 13:27:45 -0800 Subject: [PATCH 121/143] 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 a88ba8eb3125b429cff929db281e444bcd04009e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 17:41:16 -0800 Subject: [PATCH 122/143] 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 a025f72af77146b47a116281e30d18294d67f365 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 18:16:45 -0800 Subject: [PATCH 123/143] 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 84ff17209340fa4203471c9985852c94774c18bd Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 18:22:02 -0800 Subject: [PATCH 124/143] 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 | 157 +++++++++++------- .../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, 263 insertions(+), 89 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 - - 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) - } - - hasLimit := !m.validBefore.IsZero() - - 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) - } - - if cert.ValidBefore == 0 { - certValidBefore := certValidAfter.Add(d) - if hasLimit && m.validBefore.Before(certValidBefore) { - certValidBefore = m.validBefore +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 } - 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()) } + if cert.ValidBefore == 0 { + cert.ValidBefore = cert.ValidAfter + uint64(d/time.Second) + } + // Apply backdate safely + if cert.ValidAfter > backdate { + cert.ValidAfter -= backdate + } + return nil + }) +} + +// 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 +} + +func (m *sshLimitDuration) Option(o SSHOptions) SSHCertificateModifier { + if m.NotAfter.IsZero() { + defaultDuration := &sshDefaultDuration{m.Claimer} + return defaultDuration.Option(o) } - return nil -} + return sshModifierFunc(func(cert *ssh.Certificate) error { + d, err := m.DefaultSSHCertDuration(cert.CertType) + if err != nil { + return err + } -func sshDefaultValidityModifier(c *Claimer) SSHCertificateModifier { - return &sshValidityModifier{c, time.Time{}} -} + var backdate uint64 + if cert.ValidAfter == 0 { + backdate = uint64(o.Backdate / time.Second) + cert.ValidAfter = uint64(now().Truncate(time.Second).Unix()) + } -// 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} + 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) + } + + 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) + } + } + + // 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 74b5d7f98498e1ecf2fe7a3d3a08bac4dbd729f6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 3 Jan 2020 18:30:17 -0800 Subject: [PATCH 125/143] 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 93b65bee7ce456eb4444303ab966f2b886befae1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Jan 2020 12:19:00 -0800 Subject: [PATCH 126/143] 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 8297e5c717cb0d9ede7aa5739fded30180142663 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Jan 2020 14:21:13 -0800 Subject: [PATCH 127/143] 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 06411d17155ba65164089344f756aba31490c9c6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Jan 2020 14:34:59 -0800 Subject: [PATCH 128/143] 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 144acb9ee3448c57b2adac8d7b652e594ae3a769 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 8 Jan 2020 11:46:33 -0800 Subject: [PATCH 129/143] 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 895d3054a3318843961e84fa0d6d1cb33c9f5244 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 10:58:49 -0800 Subject: [PATCH 130/143] 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 9d5b7e65e4716c0da676dc9e3c8bbeec3d591434 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 11:19:28 -0800 Subject: [PATCH 131/143] 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 74ff0513b854b85b8d3667cb5e793074d3f920cc Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 17:19:56 -0800 Subject: [PATCH 132/143] 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 549291c2ca1c18998b0872c1708d707bb597c144 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 10 Jan 2020 17:21:47 -0800 Subject: [PATCH 133/143] 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 dccbdf3a9071c1291cd7e9fa53943c43c03b5182 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 20 Dec 2019 13:30:05 -0800 Subject: [PATCH 134/143] 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 | 1059 +++++++++++++---- 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 | 237 +++- 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 | 254 +++- 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 | 533 +++++---- 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, 5292 insertions(+), 2201 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} + return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...) } - 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: + _, 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,22 +467,456 @@ 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 + token string + ctx context.Context + err error + code int + } + tests := map[string]func(t *testing.T) *authorizeTest{ + "default-to-signMethod": func(t *testing.T) *authorizeTest { + return &authorizeTest{ + auth: a, + token: "foo", + ctx: context.Background(), + err: errors.New("authority.Authorize: authority.authorizeSign: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, + } + }, + "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: "test.smallstep.com", + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: testAudiences.Sign, + ID: "1", + } + token, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + auth: a, + token: token, + ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod), + } + }, + "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: testAudiences.Revoke, + ID: "2", + } + token, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + return &authorizeTest{ + 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, + } + }, + } + + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + got, err := tc.auth.Authorize(tc.ctx, tc.token) + if err != nil { + if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { + assert.Nil(t, got) + 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 { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestAuthority_authorizeRenew(t *testing.T) { + fooCrt, err := pemutil.ReadCertificate("testdata/certs/foo.crt") + assert.FatalError(t, err) + + renewDisabledCrt, err := pemutil.ReadCertificate("testdata/certs/renew-disabled.crt") + assert.FatalError(t, err) + + otherCrt, err := pemutil.ReadCertificate("testdata/certs/provisioner-not-found.crt") + assert.FatalError(t, err) type authorizeTest struct { auth *Authority - ott string - err *apiError + cert *x509.Certificate + err error + code int } tests := map[string]func(t *testing.T) *authorizeTest{ - "fail/invalid-ott": func(t *testing.T) *authorizeTest { + "fail/db.IsRevoked-error": func(t *testing.T) *authorizeTest { + a := testAuthority(t) + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { + return false, errors.New("force") + }, + } + return &authorizeTest{ auth: a, - ott: "foo", - err: &apiError{errors.New("authorizeSign: authorizeToken: error parsing token"), - http.StatusUnauthorized, apiCtx{"ott": "foo"}}, + cert: fooCrt, + err: errors.New("authority.authorizeRenew: force"), + code: http.StatusInternalServerError, + } + }, + "fail/revoked": func(t *testing.T) *authorizeTest { + a := testAuthority(t) + a.db = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { + return true, nil + }, + } + return &authorizeTest{ + auth: a, + 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 = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { + return false, nil + }, + } + return &authorizeTest{ + auth: a, + 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 = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { + return false, nil + }, + } + + return &authorizeTest{ + auth: a, + 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 = &db.MockAuthDB{ + MIsRevoked: func(key string) (bool, error) { + return false, nil + }, + } + return &authorizeTest{ + auth: a, + cert: fooCrt, + } + }, + } + + for name, genTestCase := range tests { + t.Run(name, func(t *testing.T) { + tc := genTestCase(t) + + err := tc.auth.authorizeRenew(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()) + + 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) + } + }) + } +} + +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 { @@ -493,26 +931,18 @@ func TestAuthority_Authorize(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.authorizeSSHSign: jwk.AuthorizeSSHSign: jwk.authorizeToken; jwk token subject cannot be empty"), + code: http.StatusUnauthorized, } }, "ok": 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", - } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := generateSimpleSSHUserToken(validIssuer, validAudience[0], jwk) assert.FatalError(t, err) return &authorizeTest{ - auth: a, - ott: raw, + auth: a, + token: raw, } }, } @@ -520,113 +950,94 @@ 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.authorizeSSHSign(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) { - assert.Len(t, 8, got) + assert.Len(t, 11, got) } } }) } } -func TestAuthority_authorizeRenewal(t *testing.T) { - fooCrt, err := pemutil.ReadCertificate("testdata/certs/foo.crt") +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) - renewDisabledCrt, err := pemutil.ReadCertificate("testdata/certs/renew-disabled.crt") + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) assert.FatalError(t, err) - otherCrt, err := pemutil.ReadCertificate("testdata/certs/provisioner-not-found.crt") - assert.FatalError(t, err) + now := time.Now().UTC() + + validIssuer := "step-cli" type authorizeTest struct { - auth *Authority - crt *x509.Certificate - err *apiError + auth *Authority + token string + cert *ssh.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) { - return false, errors.New("force") - }, - } - + "fail/invalid-token": func(t *testing.T) *authorizeTest { return &authorizeTest{ - auth: a, - crt: fooCrt, - err: &apiError{errors.New("renew: force"), - http.StatusInternalServerError, apiCtx{"serialNumber": "102012593071130646873265215610956555026"}}, + auth: a, + token: "foo", + err: errors.New("authority.authorizeSSHRenew: authority.authorizeToken: error parsing token"), + code: http.StatusUnauthorized, } }, - "fail/revoked": func(t *testing.T) *authorizeTest { - a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { - return true, nil - }, + "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, - crt: fooCrt, - err: &apiError{errors.New("renew: certificate has been revoked"), - http.StatusUnauthorized, apiCtx{"serialNumber": "102012593071130646873265215610956555026"}}, - } - }, - "fail/load-provisioner": func(t *testing.T) *authorizeTest { - a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: 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"}}, - } - }, - "fail/provisioner-authorize-renewal-fail": func(t *testing.T) *authorizeTest { - a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: 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"}}, + auth: a, + token: raw, + err: errors.New("authority.authorizeSSHRenew: provisioner.AuthorizeSSHRenew not implemented"), + code: http.StatusUnauthorized, } }, "ok": func(t *testing.T) *authorizeTest { - a := testAuthority(t) - a.db = &MockAuthDB{ - isRevoked: func(key string) (bool, error) { - return false, nil - }, - } + 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, - crt: fooCrt, + auth: a, + token: tok, + cert: cert, } }, } @@ -635,17 +1046,113 @@ func TestAuthority_authorizeRenewal(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - err := tc.auth.authorizeRenew(tc.crt) + got, err := tc.auth.authorizeSSHRenew(context.Background(), 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) { + 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) @@ -653,3 +1160,99 @@ func TestAuthority_authorizeRenewal(t *testing.T) { }) } } + +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{}}, + /* + "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("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}, - }, - }, - 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 } - 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()) + 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 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}, + {"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.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}, - } - 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,21 +144,8 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { return test{ p: p, token: "foo", - err: errors.New("error parsing token"), - } - }, - "fail/ssh-unimplemented": 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) - assert.FatalError(t, err) - tok, err := generateK8sSAToken(jwk, nil) - 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"), + code: http.StatusUnauthorized, + err: errors.New("k8ssa.AuthorizeRevoke: k8ssa.authorizeToken; error parsing k8sSA token"), } }, "ok": func(t *testing.T) test { @@ -145,7 +157,6 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { assert.FatalError(t, err) return test{ p: p, - ctx: NewContextWithMethod(context.Background(), SignMethod), token: tok, } }, @@ -153,10 +164,110 @@ 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 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 { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateK8sSA(jwk.Public().Key) + assert.FatalError(t, err) + tok, err := generateK8sSAToken(jwk, nil) + 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 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.Nil(t, tc.err) { if assert.NotNil(t, opts) { @@ -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 := []struct { - name string - fields fields - args args - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v := &validityValidator{ - min: tt.fields.min, - max: tt.fields.max, + 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"), } - if err := v.Valid(tt.args.crt); (err != nil) != tt.wantErr { - t.Errorf("validityValidator.Valid() error = %v, wantErr %v", err, tt.wantErr) + }, + "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}, + } + }, + } + 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)) + } + }) + } +} + +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)) + }, + } + }, + "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,126 +509,11 @@ func TestX5C_AuthorizeSign(t *testing.T) { } } -func TestX5C_AuthorizeSSHSign(t *testing.T) { - _, fn := mockNow() - defer fn() - type test struct { - p *X5C - token string - claims *x5cPayload - err error - } - tests := map[string]func(*testing.T) test{ - "fail/no-Step-claim": func(t *testing.T) test { - p, err := generateX5C(nil) - assert.FatalError(t, err) - return test{ - p: p, - claims: new(x5cPayload), - err: errors.New("authorization 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) - return test{ - p: p, - claims: &x5cPayload{Step: new(stepPayload)}, - err: errors.New("authorization 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") - 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}, - }, - } - }, - "ok/without-claims": func(t *testing.T) test { - p, err := generateX5C(nil) - assert.FatalError(t, err) - certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") - assert.FatalError(t, err) - return test{ - p: p, - claims: &x5cPayload{ - Step: &stepPayload{SSH: &SSHOptions{}}, - Claims: jose.Claims{Subject: "foo"}, - chains: [][]*x509.Certificate{certs}, - }, - } - }, - } - 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 assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } else { - if assert.Nil(t, tc.err) { - if assert.NotNil(t, opts) { - tot := 0 - nw := now() - for _, o := range opts { - switch v := o.(type) { - case sshCertificateOptionsValidator: - 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: - 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: - assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix()) - case sshCertificateValidBeforeModifier: - assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix()) - case sshCertificateDefaultsModifier: - 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) - case *sshCertificateValidityValidator: - assert.Equals(t, v.Claimer, tc.p.claimer) - case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator, - *sshCertificateDefaultValidator: - 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) - } else { - assert.Equals(t, tot, 8) - } - } - } - } - }) - } -} - func TestX5C_AuthorizeRevoke(t *testing.T) { type test struct { p *X5C token string + code int err error } tests := map[string]func(*testing.T) test{ @@ -683,13 +523,14 @@ func TestX5C_AuthorizeRevoke(t *testing.T) { return test{ p: p, token: "foo", - err: errors.New("error parsing token"), + 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/x5c-leaf.crt") + certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt") assert.FatalError(t, err) - jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + jwk, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key") assert.FatalError(t, err) p, err := generateX5C(nil) @@ -709,6 +550,9 @@ func TestX5C_AuthorizeRevoke(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 { @@ -719,33 +563,248 @@ func TestX5C_AuthorizeRevoke(t *testing.T) { } 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 + type test struct { + p *X5C + code int + err error } - tests := []struct { - name string - prov *X5C - args args - wantErr bool - }{ - {"ok", p1, args{nil}, false}, - {"fail", p2, args{nil}, true}, + 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 _, 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) + 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, + 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, + 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) + + 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: claims, + token: tok, + } + }, + "ok/without-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{SSH: &SSHOptions{}}, + } + tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts)) + assert.FatalError(t, err) + return test{ + p: p, + claims: claims, + token: tok, + } + }, + } + 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 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) { + if assert.NotNil(t, opts) { + tot := 0 + nw := now() + for _, o := range opts { + switch v := o.(type) { + case sshCertificateOptionsValidator: + 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 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 sshCertValidAfterModifier: + assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix()) + case sshCertValidBeforeModifier: + assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix()) + 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, 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, 13) + } else { + assert.Equals(t, tot, 9) + } + } + } } }) } 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 1cb8bb3ae1487803e7a4c9da44f093cec060a35e Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 23 Jan 2020 22:04:34 -0800 Subject: [PATCH 135/143] 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 | 199 ++++++++++++------ 45 files changed, 483 insertions(+), 441 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 { - opts = append(opts, withDefaultMessage(InternalServerErrorDefaultMsg)) - return New(http.StatusInternalServerError, err, opts...) +// 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 } -// NotImplemented returns a 501 error with the given error. -func NotImplemented(err error, opts ...Option) error { +// 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)) - return New(http.StatusNotImplemented, err, opts...) + e := &Error{Status: code, Err: fmt.Errorf(format, as...)} + for _, o := range opts { + o(e) + } + return e } -// BadRequest returns an 400 error with the given error. -func BadRequest(err error, opts ...Option) error { +// 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 NewErr(http.StatusInternalServerError, err, opts...) +} + +// 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 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...) +} + +// 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 returns an 401 error with the given error. -func Unauthorized(err error, opts ...Option) error { +// 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...) +} + +// 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 returns an 404 error with the given error. -func NotFound(err error, opts ...Option) error { +// 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...) +} + +// 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 df60fe3f0db363a196216bc62e6294aede3419f9 Mon Sep 17 00:00:00 2001 From: max furman Date: Thu, 23 Jan 2020 22:07:29 -0800 Subject: [PATCH 136/143] 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 3d6a18180e7d3acf6c036506b9f4b5771799bf56 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 24 Jan 2020 12:26:27 -0800 Subject: [PATCH 137/143] 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 397a181d10608bd2a16033b10be99eb63bb90f2d Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 24 Jan 2020 13:42:00 -0800 Subject: [PATCH 138/143] 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 d482ae2fb57ec410f98d3693ba2a63ff8eb0330c Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 24 Jan 2020 13:45:11 -0800 Subject: [PATCH 139/143] 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 3fb42935b47338fe734288c22f555df4c9284a56 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 27 Jan 2020 15:52:08 -0800 Subject: [PATCH 140/143] Update cli dependency --- go.mod | 2 +- go.sum | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 66894cde..dff99712 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.20200111011727-83a91ec8e405 + github.com/smallstep/cli v0.14.0-rc.1.0.20200127233252-e55637e57819 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 86dce041..a86b7f1f 100644 --- a/go.sum +++ b/go.sum @@ -418,9 +418,11 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191213215656-d2100821138c/go. 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/certificates v0.14.0-rc.1.0.20200111012147-3ce267cdd6b7/go.mod h1:jljUh6mTvHOAqvIUvbD2L3Q/aqSpTI6HzJiNFQkj1Hc= 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/certinfo v1.1.0/go.mod h1:1gQJekdPwPvUwFWGTi7bZELmQT09cxC9wJ0VBkBNiwU= 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= @@ -441,6 +443,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20200110185014-8a0d0cd3202e h1:1aN6fvv1p 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/cli v0.14.0-rc.1.0.20200127233252-e55637e57819 h1:mcYdBrClUMvGJn2mBlJNniXC69gyOZNbBwi0GL6++qo= +github.com/smallstep/cli v0.14.0-rc.1.0.20200127233252-e55637e57819/go.mod h1:SUBVVdOk5XI7yllSupRYHzN5y4MBo89X27CN4P0d+Jw= 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 432ed0090f3d6373250f6bd78fae2506073cd42a Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 28 Jan 2020 13:17:10 -0800 Subject: [PATCH 141/143] Use _'s in table names. --- acme/authority.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/authority.go b/acme/authority.go index ddbc9213..286a7218 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -51,7 +51,7 @@ var ( challengeTable = []byte("acme_challenges") nonceTable = []byte("nonces") orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account-orders-index") + ordersByAccountIDTable = []byte("acme_account_orders_index") certTable = []byte("acme_certs") ) From c66b183783f93ba57acf53d0200387109295ebfb Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 28 Jan 2020 13:37:43 -0800 Subject: [PATCH 142/143] 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 dff99712..e3437631 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.20200127233252-e55637e57819 + github.com/smallstep/cli v0.14.0-rc.1.0.20200128213701-65805ae554f6 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 a86b7f1f..3cc1d957 100644 --- a/go.sum +++ b/go.sum @@ -419,6 +419,7 @@ github.com/smallstep/certificates v0.14.0-rc.1.0.20191217235337-aa5894058226/go. 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/certificates v0.14.0-rc.1.0.20200111012147-3ce267cdd6b7/go.mod h1:jljUh6mTvHOAqvIUvbD2L3Q/aqSpTI6HzJiNFQkj1Hc= +github.com/smallstep/certificates v0.14.0-rc.1.0.20200128212940-432ed0090f3d/go.mod h1:lWKe0ZOg45lNWtByxh82fOfzXwx93S0TeWzTCOjc19k= 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= @@ -445,6 +446,8 @@ github.com/smallstep/cli v0.14.0-rc.1.0.20200111011727-83a91ec8e405 h1:hvcnKc+fi github.com/smallstep/cli v0.14.0-rc.1.0.20200111011727-83a91ec8e405/go.mod h1:MCvJvfMNtWCi/VBfXxP1JONqLLfF9TcBj1/t5Rqme90= github.com/smallstep/cli v0.14.0-rc.1.0.20200127233252-e55637e57819 h1:mcYdBrClUMvGJn2mBlJNniXC69gyOZNbBwi0GL6++qo= github.com/smallstep/cli v0.14.0-rc.1.0.20200127233252-e55637e57819/go.mod h1:SUBVVdOk5XI7yllSupRYHzN5y4MBo89X27CN4P0d+Jw= +github.com/smallstep/cli v0.14.0-rc.1.0.20200128213701-65805ae554f6 h1:Dh+Au3z44aSzZ+nNEr+9MAdenSqTjtFVrlxlzdoXBNs= +github.com/smallstep/cli v0.14.0-rc.1.0.20200128213701-65805ae554f6/go.mod h1:50kmsPMAiR9XD0jHZYY19fkSSD3mKF9ztQjgtTLefjU= 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 0a890a5c16eea184b8b35326ae157381450760de Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 28 Jan 2020 15:34:01 -0800 Subject: [PATCH 143/143] Add the commonName as a DNSName to match RFC. Normalize names and remove the use of reflection. --- acme/order.go | 49 +++++++++++++++---- acme/order_test.go | 114 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 144 insertions(+), 19 deletions(-) diff --git a/acme/order.go b/acme/order.go index 8d22b7db..27e030e9 100644 --- a/acme/order.go +++ b/acme/order.go @@ -4,7 +4,8 @@ import ( "context" "crypto/x509" "encoding/json" - "reflect" + "sort" + "strings" "time" "github.com/pkg/errors" @@ -253,17 +254,29 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut return nil, ServerInternalErr(errors.Errorf("unexpected status %s for order %s", o.Status, o.ID)) } - // Validate identifier names against CSR alternative names // - csrNames := make(map[string]int) - for _, n := range csr.DNSNames { - csrNames[n] = 1 + // RFC8555: The CSR MUST indicate the exact same set of requested + // identifiers as the initial newOrder request. Identifiers of type "dns" + // MUST appear either in the commonName portion of the requested subject + // name or in an extensionRequest attribute [RFC2985] requesting a + // subjectAltName extension, or both. + if csr.Subject.CommonName != "" { + csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName) } - orderNames := make(map[string]int) - for _, n := range o.Identifiers { - orderNames[n.Value] = 1 + csr.DNSNames = uniqueLowerNames(csr.DNSNames) + orderNames := make([]string, len(o.Identifiers)) + for i, n := range o.Identifiers { + orderNames[i] = n.Value } - if !reflect.DeepEqual(csrNames, orderNames) { - return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly")) + orderNames = uniqueLowerNames(orderNames) + + // Validate identifier names against CSR alternative names. + if len(csr.DNSNames) != len(orderNames) { + return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames)) + } + for i := range csr.DNSNames { + if csr.DNSNames[i] != orderNames[i] { + return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames)) + } } // Get authorizations from the ACME provisioner. @@ -340,3 +353,19 @@ func (o *order) toACME(db nosql.DB, dir *directory, p provisioner.Interface) (*O } return ao, nil } + +// uniqueLowerNames returns the set of all unique names in the input after all +// of them are lowercased. The returned names will be in their lowercased form +// and sorted alphabetically. +func uniqueLowerNames(names []string) (unique []string) { + nameMap := make(map[string]int, len(names)) + for _, name := range names { + nameMap[strings.ToLower(name)] = 1 + } + unique = make([]string, 0, len(nameMap)) + for name := range nameMap { + unique = append(unique, name) + } + sort.Strings(unique) + return +} diff --git a/acme/order_test.go b/acme/order_test.go index 18a46589..77c21e24 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -906,9 +906,9 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "foo", + CommonName: "acme.example.com", }, - DNSNames: []string{"bar", "baz"}, + DNSNames: []string{"acme.example.com", "fail.smallstep.com"}, } return test{ o: o, @@ -923,9 +923,9 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "acme.example.com", + CommonName: "", }, - DNSNames: []string{"step.example.com"}, + DNSNames: []string{"acme.example.com"}, } return test{ o: o, @@ -940,7 +940,7 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "foo", + CommonName: "acme.example.com", }, DNSNames: []string{"step.example.com", "acme.example.com"}, } @@ -962,7 +962,7 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "foo", + CommonName: "acme.example.com", }, DNSNames: []string{"step.example.com", "acme.example.com"}, } @@ -982,7 +982,7 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "foo", + CommonName: "acme.example.com", }, DNSNames: []string{"step.example.com", "acme.example.com"}, } @@ -1017,7 +1017,7 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "acme", + CommonName: "acme.example.com", }, DNSNames: []string{"acme.example.com", "step.example.com"}, } @@ -1057,7 +1057,7 @@ func TestOrderFinalize(t *testing.T) { csr := &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: "foo", + CommonName: "acme.example.com", }, DNSNames: []string{"acme.example.com", "step.example.com"}, } @@ -1098,6 +1098,102 @@ func TestOrderFinalize(t *testing.T) { }, } }, + "ok/ready/no-sans": func(t *testing.T) test { + o, err := newO() + assert.FatalError(t, err) + o.Status = StatusReady + o.Identifiers = []Identifier{ + {Type: "dns", Value: "step.example.com"}, + } + + csr := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "step.example.com", + }, + } + crt := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "step.example.com", + }, + DNSNames: []string{"step.example.com"}, + } + inter := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "intermediate", + }, + } + + clone := *o + clone.Status = StatusValid + count := 0 + return test{ + o: o, + res: &clone, + csr: csr, + sa: &mockSignAuth{ + sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) { + assert.Equals(t, len(signOps), 4) + return []*x509.Certificate{crt, inter}, nil + }, + }, + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { + if count == 0 { + clone.Certificate = string(key) + } + count++ + return nil, true, nil + }, + }, + } + }, + "ok/ready/sans-and-name": func(t *testing.T) test { + o, err := newO() + assert.FatalError(t, err) + o.Status = StatusReady + + csr := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "acme.example.com", + }, + DNSNames: []string{"step.example.com"}, + } + crt := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "acme.example.com", + }, + DNSNames: []string{"acme.example.com", "step.example.com"}, + } + inter := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "intermediate", + }, + } + + clone := *o + clone.Status = StatusValid + count := 0 + return test{ + o: o, + res: &clone, + csr: csr, + sa: &mockSignAuth{ + sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) { + assert.Equals(t, len(signOps), 4) + return []*x509.Certificate{crt, inter}, nil + }, + }, + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { + if count == 0 { + clone.Certificate = string(key) + } + count++ + return nil, true, nil + }, + }, + } + }, } for name, run := range tests { t.Run(name, func(t *testing.T) {