forked from TrueCloudLab/certificates
Compare commits
51 commits
tcl/master
...
ssh-config
Author | SHA1 | Date | |
---|---|---|---|
|
907c13a7be | ||
|
191aa90ac5 | ||
|
861a653769 | ||
|
bd2c764afe | ||
|
8ef9b020ed | ||
|
85218a93b0 | ||
|
665cf90353 | ||
|
e6b1439c20 | ||
|
7712f186e8 | ||
|
b49444327c | ||
|
519ed1ffb9 | ||
|
4669bef8c7 | ||
|
9cfd84324e | ||
|
8b7415f021 | ||
|
a25d4e8b04 | ||
|
d35cebc1fb | ||
|
d6de129513 | ||
|
325d77fce9 | ||
|
8890c8edd7 | ||
|
c563143e6d | ||
|
dc24f27647 | ||
|
15390fc636 | ||
|
7a8ec397ac | ||
|
829a86a5d7 | ||
|
74c1b4e771 | ||
|
3e913d374f | ||
|
962800676f | ||
|
9f13a92b9e | ||
|
50c8b10a4f | ||
|
eb210ccc70 | ||
|
7b8f0327bd | ||
|
92bbadba32 | ||
|
43771b341f | ||
|
b2fc1a324a | ||
|
954db8792d | ||
|
caaba4a80d | ||
|
b9b0c2e2d6 | ||
|
823c190ccd | ||
|
f2ec20d53f | ||
|
1364dd9654 | ||
|
db68bf1081 | ||
|
32ae027ed9 | ||
|
9893c00e1b | ||
|
42fe92fd5c | ||
|
6148f8f775 | ||
|
cba1799a43 | ||
|
40f002a97d | ||
|
624dd0bb9d | ||
|
4dc2410134 | ||
|
3d3598a1a9 | ||
|
0a61c4035b |
27 changed files with 2374 additions and 384 deletions
11
api/api.go
11
api/api.go
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
244
api/ssh.go
244
api/ssh.go
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
301
api/ssh_test.go
301
api/ssh_test.go
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
194
authority/ssh.go
194
authority/ssh.go
|
@ -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
|
||||
|
|
|
@ -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
4
authority/testdata/templates/ca.tpl
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}
|
||||
{{- range .Step.SSH.UserFederatedKeys}}
|
||||
{{.Type}} {{.Marshal | toString | b64enc}}
|
||||
{{- end}}
|
3
authority/testdata/templates/config.tpl
vendored
Normal file
3
authority/testdata/templates/config.tpl
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
Match exec "step ssh check-host %h"
|
||||
ForwardAgent yes
|
||||
UserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts
|
1
authority/testdata/templates/error.tpl
vendored
Normal file
1
authority/testdata/templates/error.tpl
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Missing function {{Function}}
|
2
authority/testdata/templates/include.tpl
vendored
Normal file
2
authority/testdata/templates/include.tpl
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
Host *
|
||||
Include {{.User.StepPath}}/ssh/config
|
4
authority/testdata/templates/known_hosts.tpl
vendored
Normal file
4
authority/testdata/templates/known_hosts.tpl
vendored
Normal 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}}
|
3
authority/testdata/templates/sshd_config.tpl
vendored
Normal file
3
authority/testdata/templates/sshd_config.tpl
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
TrustedUserCAKeys /etc/ssh/ca.pub
|
||||
HostCertificate /etc/ssh/{{.User.Certificate}}
|
||||
HostKey /etc/ssh/{{.User.Key}}
|
128
ca/client.go
128
ca/client.go
|
@ -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.
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
45
db/db.go
45
db/db.go
|
@ -3,17 +3,23 @@ package db
|
|||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/smallstep/nosql/database"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
certsTable = []byte("x509_certs")
|
||||
revokedCertsTable = []byte("revoked_x509_certs")
|
||||
usedOTTTable = []byte("used_ott")
|
||||
sshCertsTable = []byte("ssh_certs")
|
||||
sshHostsTable = []byte("ssh_hosts")
|
||||
sshUsersTable = []byte("ssh_users")
|
||||
)
|
||||
|
||||
// ErrAlreadyExists can be returned if the DB attempts to set a key that has
|
||||
|
@ -34,6 +40,8 @@ type AuthDB interface {
|
|||
Revoke(rci *RevokedCertificateInfo) error
|
||||
StoreCertificate(crt *x509.Certificate) error
|
||||
UseToken(id, tok string) (bool, error)
|
||||
IsSSHHost(name string) (bool, error)
|
||||
StoreSSHCertificate(crt *ssh.Certificate) error
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
|
@ -55,7 +63,10 @@ func New(c *Config) (AuthDB, error) {
|
|||
return nil, errors.Wrapf(err, "Error opening database of Type %s with source %s", c.Type, c.DataSource)
|
||||
}
|
||||
|
||||
tables := [][]byte{revokedCertsTable, certsTable, usedOTTTable}
|
||||
tables := [][]byte{
|
||||
revokedCertsTable, certsTable, usedOTTTable,
|
||||
sshCertsTable, sshHostsTable, sshUsersTable,
|
||||
}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating table %s",
|
||||
|
@ -138,6 +149,38 @@ func (db *DB) UseToken(id, tok string) (bool, error) {
|
|||
return swapped, nil
|
||||
}
|
||||
|
||||
// IsSSHHost returns if a principal is present in the ssh hosts table.
|
||||
func (db *DB) IsSSHHost(principal string) (bool, error) {
|
||||
if _, err := db.Get(sshHostsTable, []byte(strings.ToLower(principal))); err != nil {
|
||||
if database.IsErrNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.Wrap(err, "database Get error")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// StoreSSHCertificate stores an SSH certificate.
|
||||
func (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error {
|
||||
var table []byte
|
||||
serial := strconv.FormatUint(crt.Serial, 10)
|
||||
tx := new(database.Tx)
|
||||
tx.Set(sshCertsTable, []byte(serial), crt.Marshal())
|
||||
if crt.CertType == ssh.HostCert {
|
||||
table = sshHostsTable
|
||||
} else {
|
||||
table = sshUsersTable
|
||||
}
|
||||
for _, p := range crt.ValidPrincipals {
|
||||
tx.Set(table, []byte(strings.ToLower(p)), []byte(serial))
|
||||
}
|
||||
if err := db.Update(tx); err != nil {
|
||||
return errors.Wrap(err, "database Update error")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Shutdown sends a shutdown message to the database.
|
||||
func (db *DB) Shutdown() error {
|
||||
if db.isUp {
|
||||
|
|
11
db/simple.go
11
db/simple.go
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/nosql/database"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ErrNotImplemented is an error returned when an operation is Not Implemented.
|
||||
|
@ -58,6 +59,16 @@ func (s *SimpleDB) UseToken(id, tok string) (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// IsSSHHost returns a "NotImplemented" error.
|
||||
func (s *SimpleDB) IsSSHHost(principal string) (bool, error) {
|
||||
return false, ErrNotImplemented
|
||||
}
|
||||
|
||||
// StoreSSHCertificate returns a "NotImplemented" error.
|
||||
func (s *SimpleDB) StoreSSHCertificate(crt *ssh.Certificate) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// Shutdown returns nil
|
||||
func (s *SimpleDB) Shutdown() error {
|
||||
return nil
|
||||
|
|
28
go.mod
28
go.mod
|
@ -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
251
go.sum
|
@ -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=
|
||||
|
|
58
pki/pki.go
58
pki/pki.go
|
@ -15,8 +15,6 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
|
@ -31,6 +29,7 @@ import (
|
|||
"github.com/smallstep/cli/jose"
|
||||
"github.com/smallstep/cli/ui"
|
||||
"github.com/smallstep/cli/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -46,6 +45,8 @@ const (
|
|||
// DBPath is the directory name under the step path where the private keys
|
||||
// will be stored.
|
||||
dbPath = "db"
|
||||
// templatesPath is the directory to store templates
|
||||
templatesPath = "templates"
|
||||
)
|
||||
|
||||
// GetDBPath returns the path where the file-system persistence is stored
|
||||
|
@ -84,6 +85,11 @@ func GetOTTKeyPath() string {
|
|||
return filepath.Join(config.StepPath(), privatePath, "ott_key")
|
||||
}
|
||||
|
||||
// GetTemplatesPath returns the path where the templates are stored.
|
||||
func GetTemplatesPath() string {
|
||||
return filepath.Join(config.StepPath(), templatesPath)
|
||||
}
|
||||
|
||||
// GetProvisioners returns the map of provisioners on the given CA.
|
||||
func GetProvisioners(caURL, rootFile string) (provisioner.List, error) {
|
||||
if len(rootFile) == 0 {
|
||||
|
@ -142,21 +148,17 @@ type PKI struct {
|
|||
}
|
||||
|
||||
// New creates a new PKI configuration.
|
||||
func New(public, private, config string) (*PKI, error) {
|
||||
if _, err := os.Stat(public); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(public, 0700); err != nil {
|
||||
return nil, errs.FileError(err, public)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(private); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(private, 0700); err != nil {
|
||||
return nil, errs.FileError(err, private)
|
||||
}
|
||||
}
|
||||
if len(config) > 0 {
|
||||
if _, err := os.Stat(config); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(config, 0700); err != nil {
|
||||
return nil, errs.FileError(err, config)
|
||||
func New() (*PKI, error) {
|
||||
public := GetPublicPath()
|
||||
private := GetSecretsPath()
|
||||
config := GetConfigPath()
|
||||
|
||||
// Create directories
|
||||
dirs := []string{public, private, config, GetTemplatesPath()}
|
||||
for _, name := range dirs {
|
||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(name, 0700); err != nil {
|
||||
return nil, errs.FileError(err, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
111
pki/templates.go
Normal 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
232
templates/templates.go
Normal 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
420
templates/templates_test.go
Normal 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
17
templates/values.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue