Compare commits

...

51 commits

Author SHA1 Message Date
Mariano Cano
907c13a7be Go mod tidy. 2019-11-04 19:51:03 -08:00
Mariano Cano
191aa90ac5 Make templates public. 2019-11-04 18:30:03 -08:00
Mariano Cano
861a653769 Update dependencies. 2019-11-04 18:09:10 -08:00
Mariano Cano
bd2c764afe Add proxycommand and new lines to templates. 2019-11-04 18:07:52 -08:00
Mariano Cano
8ef9b020ed Change default user duration to 16h. 2019-10-25 12:23:52 -07:00
Mariano Cano
85218a93b0 Fix list of user ssh public keys. 2019-10-24 18:36:02 -07:00
Mariano Cano
665cf90353 Use nosql version with go mod. 2019-10-24 14:58:48 -07:00
Mariano Cano
e6b1439c20 Merge branch 'master' into ssh-config 2019-10-24 14:45:52 -07:00
Mariano Cano
7712f186e8 Upgrade cli version. 2019-10-24 14:42:38 -07:00
Mariano Cano
b49444327c Upgrade go-jose to 2.4.0. 2019-10-24 14:40:37 -07:00
Mariano Cano
519ed1ffb9 Fix lint error. 2019-10-24 14:37:51 -07:00
Mariano Cano
4669bef8c7 Use github.com/Masterminds/sprig/v3 2019-10-22 18:41:54 -07:00
Mariano Cano
9cfd84324e Merge branch 'master' into ssh-config 2019-10-22 18:35:12 -07:00
Mariano Cano
8b7415f021 Update dependencies. 2019-10-15 18:06:43 -07:00
Mariano Cano
a25d4e8b04 Add tests for templates and some fixes. 2019-10-15 18:00:46 -07:00
Mariano Cano
d35cebc1fb Add tests for ssh authority methods. 2019-10-15 12:18:29 -07:00
Mariano Cano
d6de129513 Fix known_host path. 2019-10-15 11:41:35 -07:00
Mariano Cano
325d77fce9 Add some ssh related tests. 2019-10-14 17:10:47 -07:00
Mariano Cano
8890c8edd7 Fix lint, add keys to fields. 2019-10-14 13:57:06 -07:00
Mariano Cano
c563143e6d Add tests for ssh api methods. 2019-10-14 13:07:28 -07:00
Mariano Cano
dc24f27647 Add support for federated keys. 2019-10-11 19:26:59 -07:00
Mariano Cano
15390fc636 Fix return of host configurations. 2019-10-11 19:26:09 -07:00
Mariano Cano
7a8ec397ac Add Write method to templates.Output. 2019-10-11 18:59:50 -07:00
Mariano Cano
829a86a5d7 Add first version of ssh templates. 2019-10-11 12:49:09 -07:00
Mariano Cano
74c1b4e771 Create templates path, and remove unnecessary arguments. 2019-10-11 11:25:48 -07:00
Mariano Cano
3e913d374f Add initial support for check-host endpoint. 2019-10-10 13:08:57 -07:00
Mariano Cano
962800676f Rename SSH methods. 2019-10-08 18:35:28 -07:00
Mariano Cano
9f13a92b9e Add support for multiple ssh roots.
Fixes #125
2019-10-08 18:09:41 -07:00
Mariano Cano
50c8b10a4f Add support for user data in templates. 2019-10-04 17:08:42 -07:00
Mariano Cano
eb210ccc70 Add initial support for ssh config.
Related to smallstep/cli#170
2019-10-03 19:03:38 -07:00
Mariano Cano
7b8f0327bd Fix HTTP method for /ssh/sign 2019-09-30 15:10:23 -07:00
Mariano Cano
92bbadba32 Fix merge. 2019-09-30 14:40:00 -07:00
Mariano Cano
43771b341f Fix tests. 2019-09-27 19:05:53 -07:00
Mariano Cano
b2fc1a324a Add UsageText to virtual command. 2019-09-27 18:59:27 -07:00
Mariano Cano
954db8792d Update dependencies. 2019-09-27 18:59:27 -07:00
Mariano Cano
caaba4a80d Update pki with changes in smallstep/cli 2019-09-27 18:58:52 -07:00
Mariano Cano
b9b0c2e2d6 Use production URL and add description. 2019-09-27 18:58:52 -07:00
Mariano Cano
823c190ccd Update dependencies. 2019-09-27 18:58:52 -07:00
Mariano Cano
f2ec20d53f Make dep work copying pki package from cli.
TODO: refactor and use this package from the cli.
2019-09-27 18:57:08 -07:00
Mariano Cano
1364dd9654 Improve onboard messages. 2019-09-27 18:57:07 -07:00
Mariano Cano
db68bf1081 Show error if POST fails. 2019-09-27 18:57:07 -07:00
Mariano Cano
32ae027ed9 Move ca commands to its own package. 2019-09-27 18:57:07 -07:00
Mariano Cano
9893c00e1b Generate PKI and start server using onboarding. 2019-09-27 18:57:07 -07:00
Mariano Cano
42fe92fd5c Remove url from error message. 2019-09-27 18:57:07 -07:00
Mariano Cano
6148f8f775 Create method for onboard action and clean code. 2019-09-27 18:57:07 -07:00
Alan Christopher Thomas
cba1799a43 Change onboarding bootstrap command to step-ca onboard
cc @sourishkrout @maraino
2019-09-27 18:57:07 -07:00
Alan Christopher Thomas
40f002a97d Make note about adding "admin" JWT provisioner 2019-09-27 18:57:07 -07:00
Alan Christopher Thomas
624dd0bb9d Fix linting errors and remove useless code 2019-09-27 18:57:07 -07:00
Alan Christopher Thomas
4dc2410134 Rough wiring for basics of connecting to onboarding flow 2019-09-27 18:57:07 -07:00
Mariano Cano
3d3598a1a9 Add endpoint to return the SSH public keys.
Related to smallstep/ca-component#195
2019-09-26 13:22:07 -07:00
Mariano Cano
0a61c4035b Add initial implementation of ssh config. 2019-09-24 19:12:13 -07:00
27 changed files with 2374 additions and 384 deletions

View file

@ -250,10 +250,17 @@ 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("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)
r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost)
// For compatibility with old code:
r.MethodFunc("POST", "/re-sign", h.Renew)
// SSH CA
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.

View file

@ -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"
@ -512,6 +513,10 @@ type mockAuthority struct {
getEncryptedKey func(kid string) (string, error)
getRoots func() ([]*x509.Certificate, error)
getFederation func() ([]*x509.Certificate, 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)
}
// TODO: remove once Authorize is deprecated.
@ -617,6 +622,34 @@ func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
return m.ret1.([]*x509.Certificate), m.err
}
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
}
func (m *mockAuthority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) {
if m.getSSHConfig != nil {
return m.getSSHConfig(typ, data)
}
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

View file

@ -7,7 +7,9 @@ import (
"net/http"
"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"
)
@ -15,10 +17,14 @@ 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)
GetSSHRoots() (*authority.SSHKeys, error)
GetSSHFederation() (*authority.SSHKeys, error)
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
CheckSSHHost(principal string) (bool, 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"`
@ -28,12 +34,33 @@ type SignSSHRequest struct {
AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"`
}
// SignSSHResponse is the response object that returns the SSH certificate.
type SignSSHResponse struct {
// 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)
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
}
}
// SSHSignResponse is the response object that returns the SSH certificate.
type SSHSignResponse struct {
Certificate SSHCertificate `json:"crt"`
AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"`
}
// SSHRootsResponse represents the response object that returns the SSH user and
// host keys.
type SSHRootsResponse struct {
UserKeys []SSHPublicKey `json:"userKey,omitempty"`
HostKeys []SSHPublicKey `json:"hostKey,omitempty"`
}
// SSHCertificate represents the response SSH certificate.
type SSHCertificate struct {
*ssh.Certificate `json:"omitempty"`
@ -76,25 +103,105 @@ func (c *SSHCertificate) UnmarshalJSON(data []byte) error {
return nil
}
// Validate validates the SignSSHRequest.
func (s *SignSSHRequest) Validate() error {
// 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
}
// 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"`
Data map[string]string `json:"data"`
}
// 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"`
}
// 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 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")
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
}
}
// SignSSH is an HTTP handler that reads an SignSSHRequest with a one-time-token
// 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.
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
@ -152,8 +259,113 @@ 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,
})
}
// SSHRoots is an HTTP handler that returns the SSH public keys for user and host
// certificates.
func (h *caHandler) SSHRoots(w http.ResponseWriter, r *http.Request) {
keys, err := h.Authority.GetSSHRoots()
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(SSHRootsResponse)
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)
}
// 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, InternalServerError(err))
return
}
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
WriteError(w, NotFound(errors.New("no keys found")))
return
}
resp := new(SSHRootsResponse)
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
// 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, body.Data)
if err != nil {
WriteError(w, InternalServerError(err))
return
}
var config SSHConfigResponse
switch body.Type {
case provisioner.SSHUserCert:
config.UserTemplates = ts
case provisioner.SSHHostCert:
config.HostTemplates = ts
default:
WriteError(w, InternalServerError(errors.New("it should hot get here")))
return
}
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,
})
}

View file

@ -12,12 +12,15 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"github.com/smallstep/assert"
"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"
)
@ -218,7 +221,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,
@ -234,7 +237,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()
@ -243,17 +246,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(),
@ -296,25 +299,303 @@ 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)
h.SSHSign(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_SSHRoots(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{
getSSHRoots: func() (*authority.SSHKeys, error) {
return tt.keys, tt.keysErr
},
}).(*caHandler)
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.SSHRoots StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("caHandler.SSHRoots unexpected error = %v", err)
}
if tt.statusCode < http.StatusBadRequest {
if !bytes.Equal(bytes.TrimSpace(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{
{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{
{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)
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)
}
})
}
}

View file

@ -8,11 +8,14 @@ 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/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/x509util"
"golang.org/x/crypto/ssh"
)
const (
@ -21,15 +24,19 @@ const (
// Authority implements the Certificate Authority internal interface.
type Authority struct {
config *Config
rootX509Certs []*x509.Certificate
intermediateIdentity *x509util.Identity
sshCAUserCertSignKey crypto.Signer
sshCAHostCertSignKey crypto.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
}
@ -125,16 +132,50 @@ 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")
}
// 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 != "" {
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")
}
// Append public key to list of user certs
a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAUserCertSignKey.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)
}
}
}
@ -145,6 +186,25 @@ func (a *Authority) init() error {
}
}
// Configure protected template variables:
if t := a.config.Templates; t != nil {
if t.Data == nil {
t.Data = make(map[string]interface{})
}
var vars templates.Step
if a.config.SSH != nil {
if a.sshCAHostCertSignKey != nil {
vars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey()
vars.SSH.HostFederatedKeys = append(vars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...)
}
if a.sshCAUserCertSignKey != nil {
vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey()
vars.SSH.UserFederatedKeys = append(vars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...)
}
}
t.Data["Step"] = vars
}
// JWT numeric dates are seconds.
a.startTime = time.Now().Truncate(time.Second)
// Set flag indicating that initialization has been completed, and should

View file

@ -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"
@ -36,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},
@ -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.
@ -101,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) {
@ -181,6 +176,16 @@ 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
}
return c.AuthorityConfig.Validate(c.getAudiences())
}

View file

@ -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()

View file

@ -8,7 +8,10 @@ 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"
"golang.org/x/crypto/ssh"
)
@ -24,6 +27,134 @@ const (
SSHAddUserCommand = "sudo useradd -m <principal>; 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 {
UserKeys []ssh.PublicKey
HostKeys []ssh.PublicKey
}
// GetSSHRoots returns the SSH User and Host public keys.
func (a *Authority) GetSSHRoots() (*SSHKeys, error) {
return &SSHKeys{
HostKeys: a.sshCAHostCerts,
UserKeys: a.sshCAUserCerts,
}, nil
}
// GetSSHFederation returns the public keys for federated SSH signers.
func (a *Authority) GetSSHFederation() (*SSHKeys, error) {
return &SSHKeys{
HostKeys: a.sshCAHostFederatedCerts,
UserKeys: a.sshCAUserFederatedCerts,
}, nil
}
// 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,
}
}
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,
}
}
// 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(mergedData)
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
@ -95,12 +226,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 +234,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),
@ -143,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
}
@ -156,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,
}
}
@ -175,19 +303,12 @@ 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"),
code: http.StatusInternalServerError,
}
}
signer, err := ssh.NewSignerFromSigner(a.sshCAUserCertSignKey)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "signSSHProxy: error creating signer"),
err: errors.Wrap(err, "signSSHAddUser: error reading random number"),
code: http.StatusInternalServerError,
}
}
signer := a.sshCAUserCertSignKey
principal := subject.ValidPrincipals[0]
addUserPrincipal := a.getAddUserPrincipal()
@ -218,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

View file

@ -1,16 +1,20 @@
package authority
import (
"crypto"
"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/db"
"github.com/smallstep/certificates/templates"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
@ -81,6 +85,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 +98,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 +119,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 +174,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 +217,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 <principal> <principal>"}, 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 <principal> <principal>"}, 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 <principal> <principal>"}, 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 <principal> <principal>"}, 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) {
@ -250,3 +258,343 @@ 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)},
}
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
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},
{"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) {
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)
}
})
}
}
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)
}
})
}
}

4
authority/testdata/templates/ca.tpl vendored Normal file
View file

@ -0,0 +1,4 @@
{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}
{{- range .Step.SSH.UserFederatedKeys}}
{{.Type}} {{.Marshal | toString | b64enc}}
{{- end}}

View file

@ -0,0 +1,3 @@
Match exec "step ssh check-host %h"
ForwardAgent yes
UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts

View file

@ -0,0 +1 @@
Missing function {{Function}}

View file

@ -0,0 +1,2 @@
Host *
Include {{.User.StepPath}}/ssh/config

View file

@ -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}}

View file

@ -0,0 +1,3 @@
TrustedUserCAKeys /etc/ssh/ca.pub
HostCertificate /etc/ssh/{{.User.Certificate}}
HostKey /etc/ssh/{{.User.Key}}

View file

@ -24,6 +24,7 @@ import (
"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"
@ -373,28 +374,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: "/sign-ssh"})
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,6 +506,111 @@ func (c *Client) Federation() (*api.FederationResponse, error) {
return &federation, nil
}
// 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)
}
if resp.StatusCode >= 400 {
return nil, readError(resp.Body)
}
var keys api.SSHRootsResponse
if err := readJSON(resp.Body, &keys); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
}
return &keys, nil
}
// SSHFederation performs the get /ssh/federation request to the CA and returns
// 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 {
return nil, errors.Wrapf(err, "client GET %s failed", u)
}
if resp.StatusCode >= 400 {
return nil, readError(resp.Body)
}
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 /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 {
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
}
// 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.

View file

@ -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_SSHRoots(t *testing.T) {
key, err := ssh.NewPublicKey(mustKey().Public())
if err != nil {
t.Fatal(err)
}
ok := &api.SSHRootsResponse{
HostKeys: []api.SSHPublicKey{{PublicKey: key}},
UserKeys: []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.SSHRoots()
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"}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

28
go.mod
View file

@ -3,35 +3,17 @@ module github.com/smallstep/certificates
go 1.13
require (
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 // indirect
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/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/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect
github.com/manifoldco/promptui v0.3.1 // 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
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.13.4-0.20191014220846-775cfe98ef76
github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61
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
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
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
)

251
go.sum
View file

@ -1,269 +1,144 @@
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/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/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=
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/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/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/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/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/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/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/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.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/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/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=
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=
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-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-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-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/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=
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/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=
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=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

View file

@ -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)
}
}
}
@ -432,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
@ -459,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
@ -468,7 +472,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)
}
@ -487,6 +491,7 @@ func (p *PKI) Save(opt ...Option) error {
}
}
// Generate and write defaults.json
defaults := &caDefaults{
Root: p.root,
CAConfig: p.config,
@ -497,15 +502,24 @@ 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)
}
// 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'.")

111
pki/templates.go Normal file
View file

@ -0,0 +1,111 @@
package pki
import (
"os"
"path/filepath"
"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: "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: "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: "#"},
},
}
// 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}}/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}}
`,
// 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}}
{{- range .Step.SSH.UserFederatedKeys}}
{{.Type}} {{.Marshal | toString | b64enc}}
{{- end}}
`,
}
// 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(config.StepAbs(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(config.StepAbs(t.TemplatePath), []byte(data), 0644); err != nil {
return err
}
}
}
return nil
}

232
templates/templates.go Normal file
View file

@ -0,0 +1,232 @@
package templates
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"github.com/smallstep/cli/config"
"github.com/smallstep/cli/utils"
)
// 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"
// Directory will mark a template as a directory.
Directory TemplateType = "directory"
)
// Templates is a collection of templates and variables.
type Templates struct {
SSH *SSHTemplates `json:"ssh,omitempty"`
Data map[string]interface{} `json:"data,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" 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
}
// 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 != 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
}
}
}
}
return
}
// 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.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")
}
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
}
// 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 && t.Type != Directory {
filename := config.StepAbs(t.TemplatePath)
b, err := ioutil.ReadFile(filename)
if err != nil {
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", filename)
}
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 t.Type == Directory {
return nil, nil
}
if err := t.Load(); err != nil {
return nil, err
}
buf := new(bytes.Buffer)
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,
Path: t.Path,
Comment: t.Comment,
Content: b,
}, nil
}
// Output represents the text representation of a rendered template.
type Output struct {
Name string `json:"name"`
Type TemplateType `json:"type"`
Path string `json:"path"`
Comment string `json:"comment"`
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
}

420
templates/templates_test.go Normal file
View file

@ -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())
}
}
}
})
}
}

17
templates/values.go Normal file
View file

@ -0,0 +1,17 @@
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
HostFederatedKeys []ssh.PublicKey
UserFederatedKeys []ssh.PublicKey
}