diff --git a/Gopkg.lock b/Gopkg.lock index fb7d80cc..9d7a9339 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -373,9 +373,12 @@ version = "v2.4" [[projects]] - digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976" + digest = "1:31538f8d774e8bf2fb9380d2d2a82d69e206a7d0603946586926f1819c546c26" name = "github.com/sirupsen/logrus" - packages = ["."] + packages = [ + ".", + "hooks/test", + ] pruneopts = "UT" revision = "839c75faf7f98a33d445d181f3018b5c3409a45e" version = "v1.4.2" @@ -607,6 +610,7 @@ "github.com/prometheus/client_golang/prometheus/promhttp", "github.com/rifflock/lfshook", "github.com/sirupsen/logrus", + "github.com/sirupsen/logrus/hooks/test", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "golang.org/x/crypto/nacl/box", diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 64e82978..4191ccbf 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -338,7 +338,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan logger.Infof("ssh-server set") sshServerAddress := "127.0.0.1:" + c.String("local-ssh-port") - server, err := sshserver.New(logger, sshServerAddress, shutdownC) + server, err := sshserver.New(logger, sshServerAddress, shutdownC, c.Bool("short-lived-certs")) if err != nil { logger.WithError(err).Error("Cannot create new SSH Server") return errors.Wrap(err, "Cannot create new SSH Server") @@ -915,5 +915,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag { EnvVars: []string{"LOCAL_SSH_PORT"}, Hidden: true, }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "short-lived-certs", + Usage: "Enable short lived cert authentication for SSH server", + EnvVars: []string{"SHORT_LIVED_CERTS"}, + Hidden: true, + }), } } diff --git a/sshserver/authentication.go b/sshserver/authentication.go new file mode 100644 index 00000000..217c700d --- /dev/null +++ b/sshserver/authentication.go @@ -0,0 +1,97 @@ +//+build !windows + +package sshserver + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + "github.com/gliderlabs/ssh" + "github.com/pkg/errors" + gossh "golang.org/x/crypto/ssh" +) + +var ( + systemConfigPath = "/etc/cloudflared/" + authorizeKeysPath = ".cloudflared/authorized_keys" +) + +func (s *SSHServer) authorizedKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { + sshUser, err := s.getUserFunc(ctx.User()) + if err != nil { + s.logger.Debugf("Invalid user: %s", ctx.User()) + return false + } + + authorizedKeysPath := path.Join(sshUser.HomeDir, authorizeKeysPath) + if _, err := os.Stat(authorizedKeysPath); os.IsNotExist(err) { + s.logger.Debugf("authorized_keys file %s not found", authorizeKeysPath) + return false + } + + authorizedKeysBytes, err := ioutil.ReadFile(authorizedKeysPath) + if err != nil { + s.logger.WithError(err).Errorf("Failed to load authorized_keys %s", authorizedKeysPath) + return false + } + + for len(authorizedKeysBytes) > 0 { + // Skips invalid keys. Returns error if no valid keys remain. + pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes) + authorizedKeysBytes = rest + if err != nil { + s.logger.WithError(err).Errorf("No valid keys found in %s", authorizeKeysPath) + return false + } + + if ssh.KeysEqual(pubKey, key) { + ctx.SetValue("sshUser", sshUser) + return true + } + } + s.logger.Debugf("Matching public key not found in %s", authorizeKeysPath) + return false +} + +func (s *SSHServer) shortLivedCertHandler(ctx ssh.Context, key ssh.PublicKey) bool { + userCert, ok := key.(*gossh.Certificate) + if !ok { + s.logger.Debug("Received key is not an SSH certificate") + return false + } + + if !ssh.KeysEqual(s.caCert, userCert.SignatureKey) { + s.logger.Debug("CA certificate does not match user certificate signer") + return false + } + + checker := gossh.CertChecker{} + if err := checker.CheckCert(ctx.User(), userCert); err != nil { + s.logger.Debug(err) + return false + } else { + sshUser, err := s.getUserFunc(ctx.User()) + if err != nil { + s.logger.Debugf("Invalid user: %s", ctx.User()) + return false + } + ctx.SetValue("sshUser", sshUser) + } + return true +} + +func getCACert() (ssh.PublicKey, error) { + caCertPath := path.Join(systemConfigPath, "ca.pub") + caCertBytes, err := ioutil.ReadFile(caCertPath) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("Failed to load CA certertificate %s", caCertPath)) + } + caCert, _, _, _, err := ssh.ParseAuthorizedKey(caCertBytes) + if err != nil { + return nil, errors.Wrap(err, "Failed to parse CA Certificate") + } + + return caCert, nil +} diff --git a/sshserver/authentication_test.go b/sshserver/authentication_test.go new file mode 100644 index 00000000..2773709c --- /dev/null +++ b/sshserver/authentication_test.go @@ -0,0 +1,192 @@ +//+build !windows + +package sshserver + +import ( + "context" + "io/ioutil" + "net" + "os" + "os/user" + "path" + "sync" + "testing" + + "github.com/cloudflare/cloudflared/log" + "github.com/gliderlabs/ssh" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + validPrincipal = "testUser" + testDir = "testdata" + testUserKeyFilename = "id_rsa.pub" + testCAFilename = "ca.pub" + testOtherCAFilename = "other_ca.pub" + testUserCertFilename = "id_rsa-cert.pub" +) + +var logger, hook = test.NewNullLogger() + +func TestMain(m *testing.M) { + authorizeKeysPath = testUserKeyFilename + logger.SetLevel(logrus.DebugLevel) + code := m.Run() + os.Exit(code) +} + +func TestPublicKeyAuth_Success(t *testing.T) { + context, cancel := newMockContext(validPrincipal) + defer cancel() + + sshServer := SSHServer{getUserFunc: getMockUser} + + pubKey := getKey(t, testUserKeyFilename) + assert.True(t, sshServer.authorizedKeyHandler(context, pubKey)) +} + +func TestPublicKeyAuth_MissingKey(t *testing.T) { + context, cancel := newMockContext(validPrincipal) + defer cancel() + + sshServer := SSHServer{logger: logger, getUserFunc: getMockUser} + + pubKey := getKey(t, testOtherCAFilename) + assert.False(t, sshServer.authorizedKeyHandler(context, pubKey)) + assert.Contains(t, hook.LastEntry().Message, "Matching public key not found in") +} + +func TestPublicKeyAuth_InvalidUser(t *testing.T) { + context, cancel := newMockContext("notAUser") + defer cancel() + + sshServer := SSHServer{logger: logger, getUserFunc: lookupUser} + + pubKey := getKey(t, testUserKeyFilename) + assert.False(t, sshServer.authorizedKeyHandler(context, pubKey)) + assert.Contains(t, hook.LastEntry().Message, "Invalid user") +} + +func TestPublicKeyAuth_MissingFile(t *testing.T) { + currentUser, err := user.Current() + require.Nil(t, err) + context, cancel := newMockContext(currentUser.Username) + defer cancel() + + sshServer := SSHServer{Server: ssh.Server{}, logger: logger, getUserFunc: lookupUser} + + pubKey := getKey(t, testUserKeyFilename) + assert.False(t, sshServer.authorizedKeyHandler(context, pubKey)) + assert.Contains(t, hook.LastEntry().Message, "not found") +} + +func TestShortLivedCerts_Success(t *testing.T) { + context, cancel := newMockContext(validPrincipal) + defer cancel() + + caCert := getKey(t, testCAFilename) + sshServer := SSHServer{logger: log.CreateLogger(), caCert: caCert, getUserFunc: getMockUser} + + userCert := getKey(t, testUserCertFilename) + assert.True(t, sshServer.shortLivedCertHandler(context, userCert)) +} + +func TestShortLivedCerts_CAsDontMatch(t *testing.T) { + context, cancel := newMockContext(validPrincipal) + defer cancel() + + caCert := getKey(t, testOtherCAFilename) + sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: getMockUser} + + userCert := getKey(t, testUserCertFilename) + assert.False(t, sshServer.shortLivedCertHandler(context, userCert)) + assert.Equal(t, "CA certificate does not match user certificate signer", hook.LastEntry().Message) +} + +func TestShortLivedCerts_UserDoesNotExist(t *testing.T) { + context, cancel := newMockContext(validPrincipal) + defer cancel() + + caCert := getKey(t, testCAFilename) + sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: lookupUser} + + userCert := getKey(t, testUserCertFilename) + assert.False(t, sshServer.shortLivedCertHandler(context, userCert)) + assert.Contains(t, hook.LastEntry().Message, "Invalid user") +} + +func TestShortLivedCerts_InvalidPrincipal(t *testing.T) { + context, cancel := newMockContext("notAUser") + defer cancel() + + caCert := getKey(t, testCAFilename) + sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: lookupUser} + + userCert := getKey(t, testUserCertFilename) + assert.False(t, sshServer.shortLivedCertHandler(context, userCert)) + assert.Contains(t, hook.LastEntry().Message, "not in the set of valid principals for given certificate") +} + +func getMockUser(_ string) (*User, error) { + return &User{ + Username: validPrincipal, + HomeDir: testDir, + }, nil + +} + +func getKey(t *testing.T, filename string) ssh.PublicKey { + path := path.Join(testDir, filename) + bytes, err := ioutil.ReadFile(path) + require.Nil(t, err) + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(bytes) + require.Nil(t, err) + return pubKey +} + +type mockSSHContext struct { + context.Context + *sync.Mutex +} + +func newMockContext(user string) (*mockSSHContext, context.CancelFunc) { + innerCtx, cancel := context.WithCancel(context.Background()) + mockCtx := &mockSSHContext{innerCtx, &sync.Mutex{}} + mockCtx.SetValue("user", user) + return mockCtx, cancel +} + +func (ctx *mockSSHContext) SetValue(key, value interface{}) { + ctx.Context = context.WithValue(ctx.Context, key, value) +} + +func (ctx *mockSSHContext) User() string { + return ctx.Value("user").(string) +} + +func (ctx *mockSSHContext) SessionID() string { + return "" +} + +func (ctx *mockSSHContext) ClientVersion() string { + return "" +} + +func (ctx *mockSSHContext) ServerVersion() string { + return "" +} + +func (ctx *mockSSHContext) RemoteAddr() net.Addr { + return nil +} + +func (ctx *mockSSHContext) LocalAddr() net.Addr { + return nil +} + +func (ctx *mockSSHContext) Permissions() *ssh.Permissions { + return nil +} diff --git a/sshserver/get_user.go b/sshserver/get_user.go index d5fde3fd..5446e003 100644 --- a/sshserver/get_user.go +++ b/sshserver/get_user.go @@ -1,7 +1,6 @@ // Taken from https://github.com/golang/go/blob/ad644d2e86bab85787879d41c2d2aebbd7c57db8/src/os/user/user.go // and modified to return login shell in User struct. cloudflared requires cgo for compilation because of this addition. - // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -145,7 +144,7 @@ func buildUser(pwd *C.struct_passwd) *User { Name: C.GoString(pwd.pw_gecos), HomeDir: C.GoString(pwd.pw_dir), /****************** Begin added code ******************/ - Shell: C.GoString(pwd.pw_shell), + Shell: C.GoString(pwd.pw_shell), /****************** End added code ******************/ } // The pw_gecos field isn't quite standardized. Some docs diff --git a/sshserver/host_keys.go b/sshserver/host_keys.go index a2d22bc4..c04b0ac7 100644 --- a/sshserver/host_keys.go +++ b/sshserver/host_keys.go @@ -19,14 +19,14 @@ import ( ) const ( - rsaFilename = "ssh_host_rsa_key" + rsaFilename = "ssh_host_rsa_key" ecdsaFilename = "ssh_host_ecdsa_key" ) func (s *SSHServer) configureHostKeys() error { - if _, err := os.Stat(configDir); os.IsNotExist(err) { - if err := os.MkdirAll(configDir, 0755); err != nil { - return errors.Wrap(err, fmt.Sprintf("Error creating %s directory", configDir)) + if _, err := os.Stat(systemConfigPath); os.IsNotExist(err) { + if err := os.MkdirAll(systemConfigPath, 0755); err != nil { + return errors.Wrap(err, fmt.Sprintf("Error creating %s directory", systemConfigPath)) } } @@ -54,7 +54,7 @@ func (s *SSHServer) configureHostKey(keyFunc func() (string, error)) error { } func (s *SSHServer) ensureRSAKeyExists() (string, error) { - keyPath := filepath.Join(configDir, rsaFilename) + keyPath := filepath.Join(systemConfigPath, rsaFilename) if _, err := os.Stat(keyPath); os.IsNotExist(err) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -76,7 +76,7 @@ func (s *SSHServer) ensureRSAKeyExists() (string, error) { } func (s *SSHServer) ensureECDSAKeyExists() (string, error) { - keyPath := filepath.Join(configDir, ecdsaFilename) + keyPath := filepath.Join(systemConfigPath, ecdsaFilename) if _, err := os.Stat(keyPath); os.IsNotExist(err) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { diff --git a/sshserver/sshserver_unix.go b/sshserver/sshserver_unix.go index 7ee3a66c..ae948922 100644 --- a/sshserver/sshserver_unix.go +++ b/sshserver/sshserver_unix.go @@ -4,8 +4,8 @@ package sshserver import ( "bufio" + "errors" "fmt" - "github.com/pkg/errors" "io" "os" "os/exec" @@ -19,18 +19,15 @@ import ( "github.com/sirupsen/logrus" ) -const ( - defaultShellPrompt = `\e[0;31m[\u@\h \W]\$ \e[m ` - configDir = "/etc/cloudflared/" -) - type SSHServer struct { ssh.Server - logger *logrus.Logger - shutdownC chan struct{} + logger *logrus.Logger + shutdownC chan struct{} + caCert ssh.PublicKey + getUserFunc func(string) (*User, error) } -func New(logger *logrus.Logger, address string, shutdownC chan struct{}) (*SSHServer, error) { +func New(logger *logrus.Logger, address string, shutdownC chan struct{}, shortLivedCertAuth bool) (*SSHServer, error) { currentUser, err := user.Current() if err != nil { return nil, err @@ -39,11 +36,28 @@ func New(logger *logrus.Logger, address string, shutdownC chan struct{}) (*SSHSe return nil, errors.New("cloudflared SSH server needs to run as root") } - sshServer := SSHServer{ssh.Server{Addr: address}, logger, shutdownC} + sshServer := SSHServer{ + Server: ssh.Server{Addr: address}, + logger: logger, + shutdownC: shutdownC, + getUserFunc: lookupUser, + } + if err := sshServer.configureHostKeys(); err != nil { return nil, err } + if shortLivedCertAuth { + caCert, err := getCACert() + if err != nil { + return nil, err + } + sshServer.caCert = caCert + sshServer.PublicKeyHandler = sshServer.shortLivedCertHandler + } else { + sshServer.PublicKeyHandler = sshServer.authorizedKeyHandler + } + return &sshServer, nil } @@ -64,11 +78,9 @@ func (s *SSHServer) Start() error { func (s *SSHServer) connectionHandler(session ssh.Session) { // Get uid and gid of user attempting to login - sshUser, err := lookupUser(session.User()) - if err != nil { - if _, err := io.WriteString(session, "Invalid credentials\n"); err != nil { - s.logger.WithError(err).Error("Invalid credentials: Failed to write to SSH session") - } + sshUser, ok := session.Context().Value("sshUser").(*User) + if !ok || sshUser == nil { + s.logger.Error("Error retrieving credentials from session") s.CloseSession(session) return } @@ -86,17 +98,22 @@ func (s *SSHServer) connectionHandler(session ssh.Session) { return } - uidInt, uidErr := stringToUint32(sshUser.Uid) - gidInt, gidErr := stringToUint32(sshUser.Gid) - if uidErr != nil || gidErr != nil { + uidInt, err := stringToUint32(sshUser.Uid) + if err != nil { s.logger.WithError(err).Error("Invalid user") s.CloseSession(session) return } + gidInt, err := stringToUint32(sshUser.Gid) + if err != nil { + s.logger.WithError(err).Error("Invalid user group") + s.CloseSession(session) + return + } cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uidInt, Gid: gidInt}} cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) - cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser.Name)) + cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser.Username)) cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", sshUser.HomeDir)) cmd.Dir = sshUser.HomeDir psuedoTTY, err := pty.Start(cmd) diff --git a/sshserver/sshserver_windows.go b/sshserver/sshserver_windows.go index e0780802..a2002e01 100644 --- a/sshserver/sshserver_windows.go +++ b/sshserver/sshserver_windows.go @@ -3,15 +3,17 @@ package sshserver import ( + "errors" + "github.com/sirupsen/logrus" ) type SSHServer struct{} -func New(_ *logrus.Logger, _ string, _ chan struct{}) (*SSHServer, error) { - return nil, nil +func New(_ *logrus.Logger, _ string, _ chan struct{}, _ bool) (*SSHServer, error) { + return nil, errors.New("cloudflared ssh server is not supported on windows") } func (s *SSHServer) Start() error { - return nil + return errors.New("cloudflared ssh server is not supported on windows") } diff --git a/sshserver/testdata/ca b/sshserver/testdata/ca new file mode 100644 index 00000000..5e3f830c --- /dev/null +++ b/sshserver/testdata/ca @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV6 +8IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hB +oYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UC +Zt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvb +HZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV +/ZKJ0AyhhQAAA8gciO8XHIjvFwAAAAdzc2gtcnNhAAABAQDRzoSSVi8L0HTjWoQZYM26i3 +obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9y +CEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKH +P1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/ +fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETO +HgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGFAAAAAwEAAQAAAQEApVzGdKhk8ETevAst +rurze6JPHcKUbr3NQE1EJi2fBvCtF0oQrtxTx54h2GAB8Q0MO6bQfsiL1ojm0ZQCfUBJBs +jxxb9zoccS98Vilo7ybm5SdBcMjkZX1am1jCMdQCZfCpk4/kGi7yvyOe1IhG01UBodpX5X +mwTjhN+fdjW7LSiW6cKPClN49CZKgmtvI27FCt+/TtMzdCXOiJxJ4yZCzCRhSgssV0gWI1 +0VJr/MHirKUvv/qCLAuOBxIr9UgdduRZUpNX+KS2rfhFEbjnUqc/57aAakpQmuPB5I+s9G +DnrF0HSHpq7u1XC1SvYlnFBN/0A7Hw/MX2SaBFH7mc9AAQAAAIAFuTHr6O8tCvWEawfxC0 +qiAPQ+Yy1vthq5uewmuQujMutUnc9JAUl32PdU1DbS7APC1Dg9XL7SyAB6A+ZpRJRAKgCY +SneAKE6hOytH+yM206aekrz6VuZiSpBqpfEqDibVAaZIO8sv/9dtZd6kWemxNErPQoKJey +Z7/cuWUWQovAAAAIEA6ugIlVj1irPmElyCCt5YfPv2x8Dl54ELoP/WsffsrPHNQog64hFd +ahD7Wq63TA566bN85fkx8OVU5TbbEQmkHgOEV6nDRY2YsBSqIOblA/KehtfdUIqZB0iNBh +Gn6TV/z6HwnSR3gKv4b66Gveek6LfRAG3mbsLCgyRAbYgn6YUAAACBAOSlf+n1eh6yjtvF +Zecq3Zslj7O8cUs17PQx4vQ7sXNCFrIZdevWPIn9sVrt7/hsTrXunDz6eXCeclB35KZe3H +WPVjRoD+xnr5+sXx2qXOnKCR0LdFybso6IR5bXAI6DNSNfP7D9LPEQ+R73Jk0jPuLYzocS +iM89KZiuGpzr01gBAAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/sshserver/testdata/ca.pub b/sshserver/testdata/ca.pub new file mode 100644 index 00000000..18c338d5 --- /dev/null +++ b/sshserver/testdata/ca.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRzoSSVi8L0HTjWoQZYM26i3obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9yCEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKHP1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETOHgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGF mike@C02Y50TGJGH8 diff --git a/sshserver/testdata/id_rsa b/sshserver/testdata/id_rsa new file mode 100644 index 00000000..a8923737 --- /dev/null +++ b/sshserver/testdata/id_rsa @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/sshserver/testdata/id_rsa-cert.pub b/sshserver/testdata/id_rsa-cert.pub new file mode 100644 index 00000000..961868a2 --- /dev/null +++ b/sshserver/testdata/id_rsa-cert.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgOsuFqKdzp/nC3wQfKVJBdHa8axtGryKplPkDjdSXT4kAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2wAAAAAAAAAAAAAAAQAAAA10ZXN0VXNlckB0ZXN0AAAADAAAAAh0ZXN0VXNlcgAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV68IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hBoYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UCZt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvbHZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV/ZKJ0AyhhQAAAQ8AAAAHc3NoLXJzYQAAAQC2lL+6JYTGOdz1zNnck6onrFcVpO2onCVAKP8HdLoCeH0/upIugaCocPKuzoURYEfiHQotviNeprE/2CyAroJ5VBdqWftEeHn3FFvBCQ1gwRQ7oci4C5n72t0vjWWE6WBylS0RqpJjr6EQ8a1vuwIqAQrEJPp2yNLjRH2WD7eicBh5f43VKOMr73DtyTh4xoF0C2sNBROudt58npTaYqRHQgoI25V/aCmuYBgM3wdAGcoEZGoSerMfhID7GcWkvemq2hF8mQsspG3zgnyQXk+ahagmefzxutDnr3KdrZ637La0/XwABvBZ9L4l5RiEilVI1Shl96F2qbBW2YZ64pUQ test@cloudflare.com diff --git a/sshserver/testdata/id_rsa.pub b/sshserver/testdata/id_rsa.pub new file mode 100644 index 00000000..842a259f --- /dev/null +++ b/sshserver/testdata/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2w== test@cloudflare.com diff --git a/sshserver/testdata/other_ca b/sshserver/testdata/other_ca new file mode 100644 index 00000000..c79d6a2e --- /dev/null +++ b/sshserver/testdata/other_ca @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAzBO7TXxbpk7sGQm/Wa29N/NFe5uuoEQGC5hxfihmcvVgeKeNKiSS +snxzCE1Y6SmNMoE4aQs92wtcn48GmxRwZSXbCqLq2CJrHfe9B2k3aPkJZpQkFMshcJGo7p +G0Vlo7dWAbYf99/YKddf290uLK7vxw9ty0pM1hXSXHNShv1b+bTQm/COMZ5jNsncjc1yBH +KGkFVHee9Dh4Z0xLlHipIyyNXXzI0RFYuHSNJz9GD310XQLIIroptr7+/7g6+sPPGsNlI+ +95OScba1/PQ2b/qy+KyIwNIMSd9ziJy5xnO7Vo3LrqQrza1Pkn2i29PljUcbc/F0hhXNIq +ITdNWwVqsQAAA8iKllTIipZUyAAAAAdzc2gtcnNhAAABAQDME7tNfFumTuwZCb9Zrb0380 +V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourY +Imsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJc +c1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YP +fXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuu +pCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqxAAAAAwEAAQAAAQAKEtNFEOVpQS4QUlXa +tGPJtj1wy4+EI7d0rRK1GoNsG0amzgZ+1Q1UuCXpe//uinmIy64gKUjlXhs1WRcHYqvlok +e8r6wN/Szybr8q9Xuht+FJ6fgZ+qjs6JPBKvoO5SdYNOVFIhpzABaLs3nCRiWkRFvDI8Pa ++rRap7m8mwFiOJtmdiIZYFxzw6xXwTsGCrWPKgTv3FKGZzXnCB9i7jC2vwT1MDYbcnzEH4 +Ba4dxI8bp6WWEX0biRIXj3jCtLb5gisNTSxdZs254Syh75HEXunSh2YO+yVSWQtZj19ewW +6Rb1Z3x5rVfXcgSkg7gZd9EpbckIIg6+MFSH3wdGW6atAAAAgQDFXiMuNd4ZYwdyhjlM5n +nFqQDXGgnwyNdiIqAapoqTdF5aZwNnbTU0fCFaDMLCQAHgntcgCEsW9A4HzDzYhOABKElv +j973vXWF165wFiZwuKSfroq/6JH6CiIcjiqpszbnqSOzy1hq913RWILS6e9yMjxRv8PUjm +E+IkcnfcFUwAAAAIEA+jwI3ICe8PGEIezV2tvQFeQy2Z2wGslu1yvqfTYEztSmtygns3wn +ZBb+cBXCnpqUCtznG7hZhq7I4m1I47BYznULwwFiBTVtBASG5wNP7zeVKTVZ4SKprze+Fe +I/nUZDJ5Q26um7eDbhvZ/n95GY+fucMVHoSBfX1wE16XBfp88AAACBANDHcgC4qP2oyOw/ ++p9HineMQd/ppG3fePe07jyZXLHLf0rByFveFgRAQ1m77O7FtP3fFKy3Y9nNy18LGq35ZK +Blsz2B23bO8NuffgAhchDG7KzKFXCo+AraIj5znp/znK5zIkaiiSOQaYywJ36EooYVpRtj +ep5ap6bBFDZ2e+V/AAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/sshserver/testdata/other_ca.pub b/sshserver/testdata/other_ca.pub new file mode 100644 index 00000000..7ba45831 --- /dev/null +++ b/sshserver/testdata/other_ca.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDME7tNfFumTuwZCb9Zrb0380V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourYImsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJcc1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YPfXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuupCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqx mike@C02Y50TGJGH8 diff --git a/vendor/github.com/sirupsen/logrus/hooks/test/test.go b/vendor/github.com/sirupsen/logrus/hooks/test/test.go new file mode 100644 index 00000000..234a17df --- /dev/null +++ b/vendor/github.com/sirupsen/logrus/hooks/test/test.go @@ -0,0 +1,92 @@ +// The Test package is used for testing logrus. It is here for backwards +// compatibility from when logrus' organization was upper-case. Please use +// lower-case logrus and the `null` package instead of this one. +package test + +import ( + "io/ioutil" + "sync" + + "github.com/sirupsen/logrus" +) + +// Hook is a hook designed for dealing with logs in test scenarios. +type Hook struct { + // Entries is an array of all entries that have been received by this hook. + // For safe access, use the AllEntries() method, rather than reading this + // value directly. + Entries []logrus.Entry + mu sync.RWMutex +} + +// NewGlobal installs a test hook for the global logger. +func NewGlobal() *Hook { + + hook := new(Hook) + logrus.AddHook(hook) + + return hook + +} + +// NewLocal installs a test hook for a given local logger. +func NewLocal(logger *logrus.Logger) *Hook { + + hook := new(Hook) + logger.Hooks.Add(hook) + + return hook + +} + +// NewNullLogger creates a discarding logger and installs the test hook. +func NewNullLogger() (*logrus.Logger, *Hook) { + + logger := logrus.New() + logger.Out = ioutil.Discard + + return logger, NewLocal(logger) + +} + +func (t *Hook) Fire(e *logrus.Entry) error { + t.mu.Lock() + defer t.mu.Unlock() + t.Entries = append(t.Entries, *e) + return nil +} + +func (t *Hook) Levels() []logrus.Level { + return logrus.AllLevels +} + +// LastEntry returns the last entry that was logged or nil. +func (t *Hook) LastEntry() *logrus.Entry { + t.mu.RLock() + defer t.mu.RUnlock() + i := len(t.Entries) - 1 + if i < 0 { + return nil + } + return &t.Entries[i] +} + +// AllEntries returns all entries that were logged. +func (t *Hook) AllEntries() []*logrus.Entry { + t.mu.RLock() + defer t.mu.RUnlock() + // Make a copy so the returned value won't race with future log requests + entries := make([]*logrus.Entry, len(t.Entries)) + for i := 0; i < len(t.Entries); i++ { + // Make a copy, for safety + entries[i] = &t.Entries[i] + } + return entries +} + +// Reset removes all Entries from this test hook. +func (t *Hook) Reset() { + t.mu.Lock() + defer t.mu.Unlock() + t.Entries = make([]logrus.Entry, 0) +}