From d8ba7b77ea080800ec03ad20780476ae3f550022 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Tue, 13 Feb 2018 02:27:18 +0800 Subject: [PATCH] adopt new sshpiper api --- Gopkg.lock | 7 +- sshpiperd/sshpiperd.go | 4 +- sshpiperd/workingdir.go | 28 ++- .../x/crypto/acme/autocert/autocert.go | 11 - vendor/golang.org/x/crypto/argon2/argon2.go | 79 +++++- vendor/golang.org/x/crypto/ssh/certs.go | 4 +- vendor/golang.org/x/crypto/ssh/client_auth.go | 95 ++++--- vendor/golang.org/x/crypto/ssh/keys_test.go | 24 +- vendor/golang.org/x/crypto/ssh/sshpiper.go | 228 +++++++++++++---- .../golang.org/x/crypto/ssh/sshpiper_test.go | 237 ++++++++++++++++-- .../x/crypto/ssh/test/multi_auth_test.go | 144 +++++++++++ .../x/crypto/ssh/test/sshd_test_pw.c | 173 +++++++++++++ .../x/crypto/ssh/test/test_unix_test.go | 73 +++++- 13 files changed, 936 insertions(+), 171 deletions(-) create mode 100644 vendor/golang.org/x/crypto/ssh/test/multi_auth_test.go create mode 100644 vendor/golang.org/x/crypto/ssh/test/sshd_test_pw.c diff --git a/Gopkg.lock b/Gopkg.lock index a30a354f..7624fbad 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -34,14 +34,15 @@ "ed25519/internal/edwards25519", "internal/chacha20", "poly1305", - "ssh" + "ssh", + "ssh/testdata" ] - revision = "743ed784ccc2ebde7f34698af76a6acdf4e0c251" + revision = "daf7a6932c427819b72906700cd3e9209ec7d70e" source = "https://github.com/tg123/sshpiper.crypto" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "520641d8f3577123162ab4eca84b1d5765a60d84897a760e50b5073a0e7afddd" + inputs-digest = "a45bf0b707bd655f13d5cd1ac7586dbc57f2eaee25ffc08bf9c3746189c686f3" solver-name = "gps-cdcl" solver-version = 1 diff --git a/sshpiperd/sshpiperd.go b/sshpiperd/sshpiperd.go index 7031494e..0f1825c8 100644 --- a/sshpiperd/sshpiperd.go +++ b/sshpiperd/sshpiperd.go @@ -43,9 +43,7 @@ func main() { // TODO make this pluggable piper := &ssh.SSHPiperConfig{ - FindUpstream: findUpstreamFromUserfile, - MapPublicKey: mapPublicKeyFromUserfile, - UpstreamHostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO should support by config + FindUpstream: findUpstreamFromUserfile, } if config.Challenger != "" { diff --git a/sshpiperd/workingdir.go b/sshpiperd/workingdir.go index b085c5f1..1326e7d2 100644 --- a/sshpiperd/workingdir.go +++ b/sshpiperd/workingdir.go @@ -121,37 +121,51 @@ func parseUpstreamFile(data string) (string, string) { return line, user } -func findUpstreamFromUserfile(conn ssh.ConnMetadata) (net.Conn, string, error) { +func findUpstreamFromUserfile(conn ssh.ConnMetadata) (net.Conn, *ssh.SSHPiperAuthPipe, error) { user := conn.User() if !checkUsername(user) { - return nil, "", fmt.Errorf("downstream is not using a valid username") + return nil, nil, fmt.Errorf("downstream is not using a valid username") } err := UserUpstreamFile.checkPerm(user) if err != nil { - return nil, "", err + return nil, nil, err } data, err := UserUpstreamFile.read(user) if err != nil { - return nil, "", err + return nil, nil, err } addr, mappedUser := parseUpstreamFile(string(data)) if addr == "" { - return nil, "", fmt.Errorf("empty addr") + return nil, nil, fmt.Errorf("empty addr") } logger.Printf("mapping user [%v] to [%v@%v]", user, mappedUser, addr) c, err := net.Dial("tcp", addr) if err != nil { - return nil, "", err + return nil, nil, err } - return c, mappedUser, nil + return c, &ssh.SSHPiperAuthPipe{ + User: mappedUser, + + PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) { + signer, err := mapPublicKeyFromUserfile(conn, key) + + if err != nil { + return ssh.AuthPipeTypeDiscard, nil, err + } + + return ssh.AuthPipeTypeMap, ssh.PublicKeys(signer), nil + }, + + UpstreamHostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO should support by config + }, nil } func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer ssh.Signer, err error) { diff --git a/vendor/golang.org/x/crypto/acme/autocert/autocert.go b/vendor/golang.org/x/crypto/acme/autocert/autocert.go index 453e7229..263b2913 100644 --- a/vendor/golang.org/x/crypto/acme/autocert/autocert.go +++ b/vendor/golang.org/x/crypto/acme/autocert/autocert.go @@ -27,7 +27,6 @@ import ( "net" "net/http" "path" - "strconv" "strings" "sync" "time" @@ -942,16 +941,6 @@ func validCert(domain string, der [][]byte, key crypto.Signer) (leaf *x509.Certi return leaf, nil } -func retryAfter(v string) time.Duration { - if i, err := strconv.Atoi(v); err == nil { - return time.Duration(i) * time.Second - } - if t, err := http.ParseTime(v); err == nil { - return t.Sub(timeNow()) - } - return time.Second -} - type lockedMathRand struct { sync.Mutex rnd *mathrand.Rand diff --git a/vendor/golang.org/x/crypto/argon2/argon2.go b/vendor/golang.org/x/crypto/argon2/argon2.go index 71cf8c55..798f5cbd 100644 --- a/vendor/golang.org/x/crypto/argon2/argon2.go +++ b/vendor/golang.org/x/crypto/argon2/argon2.go @@ -5,7 +5,35 @@ // Package argon2 implements the key derivation function Argon2. // Argon2 was selected as the winner of the Password Hashing Competition and can // be used to derive cryptographic keys from passwords. -// Argon2 is specfifed at https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf +// +// For a detailed specification of Argon2 see [1]. +// +// If you aren't sure which function you need, use Argon2id (IDKey) and +// the parameter recommendations for your scenario. +// +// +// Argon2i +// +// Argon2i (implemented by Key) is the side-channel resistant version of Argon2. +// It uses data-independent memory access, which is preferred for password +// hashing and password-based key derivation. Argon2i requires more passes over +// memory than Argon2id to protect from trade-off attacks. The recommended +// parameters (taken from [2]) for non-interactive operations are time=3 and to +// use the maximum available memory. +// +// +// Argon2id +// +// Argon2id (implemented by IDKey) is a hybrid version of Argon2 combining +// Argon2i and Argon2d. It uses data-independent memory access for the first +// half of the first iteration over the memory and data-dependent memory access +// for the rest. Argon2id is side-channel resistant and provides better brute- +// force cost savings due to time-memory tradeoffs than Argon2i. The recommended +// parameters for non-interactive operations (taken from [2]) are time=1 and to +// use the maximum available memory. +// +// [1] https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf +// [2] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-9.3 package argon2 import ( @@ -25,23 +53,50 @@ const ( ) // Key derives a key from the password, salt, and cost parameters using Argon2i -// returning a byte slice of length keyLen that can be used as cryptographic key. -// The CPU cost and parallism degree must be greater than zero. +// returning a byte slice of length keyLen that can be used as cryptographic +// key. The CPU cost and parallism degree must be greater than zero. // -// For example, you can get a derived key for e.g. AES-256 (which needs a 32-byte key) by doing: -// `key := argon2.Key([]byte("some password"), salt, 4, 32*1024, 4, 32)` +// For example, you can get a derived key for e.g. AES-256 (which needs a +// 32-byte key) by doing: `key := argon2.Key([]byte("some password"), salt, 3, +// 32*1024, 4, 32)` // -// The recommended parameters for interactive logins as of 2017 are time=4, memory=32*1024. -// The number of threads can be adjusted to the numbers of available CPUs. -// The time parameter specifies the number of passes over the memory and the memory -// parameter specifies the size of the memory in KiB. For example memory=32*1024 sets the -// memory cost to ~32 MB. -// The cost parameters should be increased as memory latency and CPU parallelism increases. -// Remember to get a good random salt. +// The draft RFC recommends[2] time=3, and memory=32*1024 is a sensible number. +// If using that amount of memory (32 MB) is not possible in some contexts then +// the time parameter can be increased to compensate. +// +// The time parameter specifies the number of passes over the memory and the +// memory parameter specifies the size of the memory in KiB. For example +// memory=32*1024 sets the memory cost to ~32 MB. The number of threads can be +// adjusted to the number of available CPUs. The cost parameters should be +// increased as memory latency and CPU parallelism increases. Remember to get a +// good random salt. func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte { return deriveKey(argon2i, password, salt, nil, nil, time, memory, threads, keyLen) } +// IDKey derives a key from the password, salt, and cost parameters using +// Argon2id returning a byte slice of length keyLen that can be used as +// cryptographic key. The CPU cost and parallism degree must be greater than +// zero. +// +// For example, you can get a derived key for e.g. AES-256 (which needs a +// 32-byte key) by doing: `key := argon2.IDKey([]byte("some password"), salt, 1, +// 64*1024, 4, 32)` +// +// The draft RFC recommends[2] time=1, and memory=64*1024 is a sensible number. +// If using that amount of memory (64 MB) is not possible in some contexts then +// the time parameter can be increased to compensate. +// +// The time parameter specifies the number of passes over the memory and the +// memory parameter specifies the size of the memory in KiB. For example +// memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be +// adjusted to the numbers of available CPUs. The cost parameters should be +// increased as memory latency and CPU parallelism increases. Remember to get a +// good random salt. +func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte { + return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen) +} + func deriveKey(mode int, password, salt, secret, data []byte, time, memory uint32, threads uint8, keyLen uint32) []byte { if time < 1 { panic("argon2: number of rounds too small") diff --git a/vendor/golang.org/x/crypto/ssh/certs.go b/vendor/golang.org/x/crypto/ssh/certs.go index cfc8ead1..42106f3f 100644 --- a/vendor/golang.org/x/crypto/ssh/certs.go +++ b/vendor/golang.org/x/crypto/ssh/certs.go @@ -44,7 +44,9 @@ type Signature struct { const CertTimeInfinity = 1<<64 - 1 // An Certificate represents an OpenSSH certificate as defined in -// [PROTOCOL.certkeys]?rev=1.8. +// [PROTOCOL.certkeys]?rev=1.8. The Certificate type implements the +// PublicKey interface, so it can be unmarshaled using +// ParsePublicKey. type Certificate struct { Nonce []byte Key PublicKey diff --git a/vendor/golang.org/x/crypto/ssh/client_auth.go b/vendor/golang.org/x/crypto/ssh/client_auth.go index a1252cb9..5f44b774 100644 --- a/vendor/golang.org/x/crypto/ssh/client_auth.go +++ b/vendor/golang.org/x/crypto/ssh/client_auth.go @@ -11,6 +11,14 @@ import ( "io" ) +type authResult int + +const ( + authFailure authResult = iota + authPartialSuccess + authSuccess +) + // clientAuthenticate authenticates with the remote server. See RFC 4252. func (c *connection) clientAuthenticate(config *ClientConfig) error { // initiate user auth session @@ -37,11 +45,12 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error { if err != nil { return err } - if ok { + if ok == authSuccess { // success return nil + } else if ok == authFailure { + tried[auth.method()] = true } - tried[auth.method()] = true if methods == nil { methods = lastMethods } @@ -82,7 +91,7 @@ type AuthMethod interface { // If authentication is not successful, a []string of alternative // method names is returned. If the slice is nil, it will be ignored // and the previous set of possible methods will be reused. - auth(session []byte, user string, p packetConn, rand io.Reader) (bool, []string, error) + auth(session []byte, user string, p packetConn, rand io.Reader) (authResult, []string, error) // method returns the RFC 4252 method name. method() string @@ -91,13 +100,13 @@ type AuthMethod interface { // "none" authentication, RFC 4252 section 5.2. type noneAuth int -func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) { +func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) { if err := c.writePacket(Marshal(&userAuthRequestMsg{ User: user, Service: serviceSSH, Method: "none", })); err != nil { - return false, nil, err + return authFailure, nil, err } return handleAuthResponse(c) @@ -111,7 +120,7 @@ func (n *noneAuth) method() string { // a function call, e.g. by prompting the user. type passwordCallback func() (password string, err error) -func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) { +func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) { type passwordAuthMsg struct { User string `sshtype:"50"` Service string @@ -125,7 +134,7 @@ func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand // The program may only find out that the user doesn't have a password // when prompting. if err != nil { - return false, nil, err + return authFailure, nil, err } if err := c.writePacket(Marshal(&passwordAuthMsg{ @@ -135,7 +144,7 @@ func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand Reply: false, Password: pw, })); err != nil { - return false, nil, err + return authFailure, nil, err } return handleAuthResponse(c) @@ -178,7 +187,7 @@ func (cb publicKeyCallback) method() string { return "publickey" } -func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) { +func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) { // Authentication is performed by sending an enquiry to test if a key is // acceptable to the remote. If the key is acceptable, the client will // attempt to authenticate with the valid key. If not the client will repeat @@ -186,13 +195,13 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand signers, err := cb() if err != nil { - return false, nil, err + return authFailure, nil, err } var methods []string for _, signer := range signers { ok, err := validateKey(signer.PublicKey(), user, c) if err != nil { - return false, nil, err + return authFailure, nil, err } if !ok { continue @@ -206,7 +215,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand Method: cb.method(), }, []byte(pub.Type()), pubKey)) if err != nil { - return false, nil, err + return authFailure, nil, err } // manually wrap the serialized signature in a string @@ -224,24 +233,24 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand } p := Marshal(&msg) if err := c.writePacket(p); err != nil { - return false, nil, err + return authFailure, nil, err } - var success bool + var success authResult success, methods, err = handleAuthResponse(c) if err != nil { - return false, nil, err + return authFailure, nil, err } // If authentication succeeds or the list of available methods does not // contain the "publickey" method, do not attempt to authenticate with any // other keys. According to RFC 4252 Section 7, the latter can occur when // additional authentication methods are required. - if success || !containsMethod(methods, cb.method()) { + if success == authSuccess || !containsMethod(methods, cb.method()) { return success, methods, err } } - return false, methods, nil + return authFailure, methods, nil } func containsMethod(methods []string, method string) bool { @@ -318,28 +327,31 @@ func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMet // handleAuthResponse returns whether the preceding authentication request succeeded // along with a list of remaining authentication methods to try next and // an error if an unexpected response was received. -func handleAuthResponse(c packetConn) (bool, []string, error) { +func handleAuthResponse(c packetConn) (authResult, []string, error) { for { packet, err := c.readPacket() if err != nil { - return false, nil, err + return authFailure, nil, err } switch packet[0] { case msgUserAuthBanner: if err := handleBannerResponse(c, packet); err != nil { - return false, nil, err + return authFailure, nil, err } case msgUserAuthFailure: var msg userAuthFailureMsg if err := Unmarshal(packet, &msg); err != nil { - return false, nil, err + return authFailure, nil, err } - return false, msg.Methods, nil + if msg.PartialSuccess { + return authPartialSuccess, msg.Methods, nil + } + return authFailure, msg.Methods, nil case msgUserAuthSuccess: - return true, nil, nil + return authSuccess, nil, nil default: - return false, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0]) + return authFailure, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0]) } } } @@ -381,7 +393,7 @@ func (cb KeyboardInteractiveChallenge) method() string { return "keyboard-interactive" } -func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) { +func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) { type initiateMsg struct { User string `sshtype:"50"` Service string @@ -395,20 +407,20 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe Service: serviceSSH, Method: "keyboard-interactive", })); err != nil { - return false, nil, err + return authFailure, nil, err } for { packet, err := c.readPacket() if err != nil { - return false, nil, err + return authFailure, nil, err } // like handleAuthResponse, but with less options. switch packet[0] { case msgUserAuthBanner: if err := handleBannerResponse(c, packet); err != nil { - return false, nil, err + return authFailure, nil, err } continue case msgUserAuthInfoRequest: @@ -416,18 +428,21 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe case msgUserAuthFailure: var msg userAuthFailureMsg if err := Unmarshal(packet, &msg); err != nil { - return false, nil, err + return authFailure, nil, err } - return false, msg.Methods, nil + if msg.PartialSuccess { + return authPartialSuccess, msg.Methods, nil + } + return authFailure, msg.Methods, nil case msgUserAuthSuccess: - return true, nil, nil + return authSuccess, nil, nil default: - return false, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0]) + return authFailure, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0]) } var msg userAuthInfoRequestMsg if err := Unmarshal(packet, &msg); err != nil { - return false, nil, err + return authFailure, nil, err } // Manually unpack the prompt/echo pairs. @@ -437,7 +452,7 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe for i := 0; i < int(msg.NumPrompts); i++ { prompt, r, ok := parseString(rest) if !ok || len(r) == 0 { - return false, nil, errors.New("ssh: prompt format error") + return authFailure, nil, errors.New("ssh: prompt format error") } prompts = append(prompts, string(prompt)) echos = append(echos, r[0] != 0) @@ -445,16 +460,16 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe } if len(rest) != 0 { - return false, nil, errors.New("ssh: extra data following keyboard-interactive pairs") + return authFailure, nil, errors.New("ssh: extra data following keyboard-interactive pairs") } answers, err := cb(msg.User, msg.Instruction, prompts, echos) if err != nil { - return false, nil, err + return authFailure, nil, err } if len(answers) != len(prompts) { - return false, nil, errors.New("ssh: not enough answers from keyboard-interactive callback") + return authFailure, nil, errors.New("ssh: not enough answers from keyboard-interactive callback") } responseLength := 1 + 4 for _, a := range answers { @@ -470,7 +485,7 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe } if err := c.writePacket(serialized); err != nil { - return false, nil, err + return authFailure, nil, err } } } @@ -480,10 +495,10 @@ type retryableAuthMethod struct { maxTries int } -func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader) (ok bool, methods []string, err error) { +func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader) (ok authResult, methods []string, err error) { for i := 0; r.maxTries <= 0 || i < r.maxTries; i++ { ok, methods, err = r.authMethod.auth(session, user, c, rand) - if ok || err != nil { // either success or error terminate + if ok != authFailure || err != nil { // either success, partial success or error terminate return ok, methods, err } } diff --git a/vendor/golang.org/x/crypto/ssh/keys_test.go b/vendor/golang.org/x/crypto/ssh/keys_test.go index 20ab954e..9a90abc0 100644 --- a/vendor/golang.org/x/crypto/ssh/keys_test.go +++ b/vendor/golang.org/x/crypto/ssh/keys_test.go @@ -234,7 +234,7 @@ func TestMarshalParsePublicKey(t *testing.T) { } } -type authResult struct { +type testAuthResult struct { pubKey PublicKey options []string comments string @@ -242,11 +242,11 @@ type authResult struct { ok bool } -func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []authResult) { +func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []testAuthResult) { rest := authKeys - var values []authResult + var values []testAuthResult for len(rest) > 0 { - var r authResult + var r testAuthResult var err error r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest) r.ok = (err == nil) @@ -264,7 +264,7 @@ func TestAuthorizedKeyBasic(t *testing.T) { pub, pubSerialized := getTestKey() line := "ssh-rsa " + pubSerialized + " user@host" testAuthorizedKeys(t, []byte(line), - []authResult{ + []testAuthResult{ {pub, nil, "user@host", "", true}, }) } @@ -286,7 +286,7 @@ func TestAuth(t *testing.T) { authOptions := strings.Join(authWithOptions, eol) rest2 := strings.Join(authWithOptions[3:], eol) rest3 := strings.Join(authWithOptions[6:], eol) - testAuthorizedKeys(t, []byte(authOptions), []authResult{ + testAuthorizedKeys(t, []byte(authOptions), []testAuthResult{ {pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true}, {pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true}, {nil, nil, "", "", false}, @@ -297,7 +297,7 @@ func TestAuth(t *testing.T) { func TestAuthWithQuotedSpaceInEnv(t *testing.T) { pub, pubSerialized := getTestKey() authWithQuotedSpaceInEnv := []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`) - testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []authResult{ + testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []testAuthResult{ {pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true}, }) } @@ -305,7 +305,7 @@ func TestAuthWithQuotedSpaceInEnv(t *testing.T) { func TestAuthWithQuotedCommaInEnv(t *testing.T) { pub, pubSerialized := getTestKey() authWithQuotedCommaInEnv := []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`) - testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []authResult{ + testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []testAuthResult{ {pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true}, }) } @@ -314,11 +314,11 @@ func TestAuthWithQuotedQuoteInEnv(t *testing.T) { pub, pubSerialized := getTestKey() authWithQuotedQuoteInEnv := []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + pubSerialized + ` user@host`) authWithDoubleQuotedQuote := []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + pubSerialized + "\t" + `user@host`) - testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []authResult{ + testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []testAuthResult{ {pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true}, }) - testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []authResult{ + testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []testAuthResult{ {pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true}, }) } @@ -327,7 +327,7 @@ func TestAuthWithInvalidSpace(t *testing.T) { _, pubSerialized := getTestKey() authWithInvalidSpace := []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host #more to follow but still no valid keys`) - testAuthorizedKeys(t, []byte(authWithInvalidSpace), []authResult{ + testAuthorizedKeys(t, []byte(authWithInvalidSpace), []testAuthResult{ {nil, nil, "", "", false}, }) } @@ -337,7 +337,7 @@ func TestAuthWithMissingQuote(t *testing.T) { authWithMissingQuote := []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host env="HOME=/home/root",shared-control ssh-rsa ` + pubSerialized + ` user@host`) - testAuthorizedKeys(t, []byte(authWithMissingQuote), []authResult{ + testAuthorizedKeys(t, []byte(authWithMissingQuote), []testAuthResult{ {pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true}, }) } diff --git a/vendor/golang.org/x/crypto/ssh/sshpiper.go b/vendor/golang.org/x/crypto/ssh/sshpiper.go index ac37189f..23674ce4 100644 --- a/vendor/golang.org/x/crypto/ssh/sshpiper.go +++ b/vendor/golang.org/x/crypto/ssh/sshpiper.go @@ -11,6 +11,31 @@ import ( "net" ) +type AuthPipeType int + +const ( + AuthPipeTypePassThrough AuthPipeType = iota + AuthPipeTypeMap + AuthPipeTypeDiscard + AuthPipeTypeNone +) + +type SSHPiperAuthPipe struct { + User string + + PasswordCallback func(conn ConnMetadata, password []byte) (AuthPipeType, AuthMethod, error) + + PublicKeyCallback func(conn ConnMetadata, key PublicKey) (AuthPipeType, AuthMethod, error) + + // HostKeyCallback is called during the cryptographic + // handshake to validate the uptream server's host key. The piper + // configuration must supply this callback for the connection + // to succeed. The functions InsecureIgnoreHostKey or + // FixedHostKey can be used for simplistic host key checks. + // UpstreamHostKeyCallback HostKeyCallback + UpstreamHostKeyCallback HostKeyCallback +} + // SSHPiperConfig holds SSHPiper specific configuration data. type SSHPiperConfig struct { Config @@ -28,7 +53,7 @@ type SSHPiperConfig struct { // and upstream username should be returned. // SSHPiper will use the username from downstream if empty username is returned. // If any error occurs, the piped connection will be closed. - FindUpstream func(conn ConnMetadata) (net.Conn, string, error) + FindUpstream func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) // MapPublicKey, if non-nil, is called when downstream requests a publickey auth. // SSHPiper will sign the auth packet message using the returned Signer. @@ -38,14 +63,8 @@ type SSHPiperConfig struct { // upstream ssh server instead. // // More info: https://github.com/tg123/sshpiper#publickey-sign-again - MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error) + // MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error) - // HostKeyCallback is called during the cryptographic - // handshake to validate the uptream server's host key. The piper - // configuration must supply this callback for the connection - // to succeed. The functions InsecureIgnoreHostKey or - // FixedHostKey can be used for simplistic host key checks. - UpstreamHostKeyCallback HostKeyCallback } type upstream struct{ *connection } @@ -132,9 +151,10 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn, return nil, errors.New("sshpiper: must specify FindUpstream") } - if piper.UpstreamHostKeyCallback == nil { - return nil, errors.New("sshpiper: must specify UpstreamHostKeyCallback") - } + // TODO + //if piper.UpstreamHostKeyCallback == nil { + // return nil, errors.New("sshpiper: must specify UpstreamHostKeyCallback") + //} d, err := newDownstream(conn, &ServerConfig{ Config: piper.Config, @@ -191,11 +211,16 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn, } } - upconn, mappedUser, err := piper.FindUpstream(d) + upconn, pipeauth, err := piper.FindUpstream(d) if err != nil { return nil, err } + if pipeauth.UpstreamHostKeyCallback == nil { + return nil, errors.New("sshpiper: must specify UpstreamHostKeyCallback") + } + + mappedUser := pipeauth.User addr := upconn.RemoteAddr().String() if mappedUser == "" { @@ -203,7 +228,7 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn, } u, err := newUpstream(upconn, addr, &ClientConfig{ - HostKeyCallback: piper.UpstreamHostKeyCallback, + HostKeyCallback: pipeauth.UpstreamHostKeyCallback, }) if err != nil { return nil, err @@ -221,50 +246,143 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn, p.processAuthMsg = func(msg *userAuthRequestMsg) (*userAuthRequestMsg, error) { - // only public msg need - if msg.Method != "publickey" || piper.MapPublicKey == nil { - msg.User = mappedUser + msg.User = mappedUser + + var authType AuthPipeType = AuthPipeTypePassThrough + var authMethod AuthMethod + + switch msg.Method { + case "publickey": + + if pipeauth.PublicKeyCallback == nil { + break + } + + // pubKey MAP + downKey, isQuery, sig, err := parsePublicKeyMsg(msg) + if err != nil { + return nil, err + } + + authType, authMethod, err = pipeauth.PublicKeyCallback(d, downKey) + if err != nil { + return nil, err + } + + if isQuery { + // reply for query msg + // skip query from upstream + err = p.ack(downKey) + if err != nil { + return nil, err + } + + // discard msg + return nil, nil + } else { + + ok, err := p.checkPublicKey(msg, downKey, sig) + + if err != nil { + return nil, err + } + + if !ok { + return noneAuthMsg(mappedUser), nil + } + + } + + case "password": + if pipeauth.PasswordCallback == nil { + break + } + + payload := msg.Payload + if len(payload) < 1 || payload[0] != 0 { + return nil, parseError(msgUserAuthRequest) + } + payload = payload[1:] + password, payload, ok := parseString(payload) + if !ok || len(payload) > 0 { + return nil, parseError(msgUserAuthRequest) + } + + authType, authMethod, err = pipeauth.PasswordCallback(d, password) + if err != nil { + return nil, err + } + + default: + } + + switch authType { + case AuthPipeTypePassThrough: return msg, nil - } - - // pubKey MAP - downKey, isQuery, sig, err := parsePublicKeyMsg(msg) - if err != nil { - return nil, err - } - - signer, err := piper.MapPublicKey(d, downKey) - - // no mapped user change it to none or error occur - if err != nil || signer == nil { + case AuthPipeTypeDiscard: + return msg, nil + case AuthPipeTypeNone: return noneAuthMsg(mappedUser), nil + case AuthPipeTypeMap: } - upKey := signer.PublicKey() + // map publickey, password, etc to auth - if isQuery { - // reply for query msg - msg, err = p.validAndAck(mappedUser, upKey, downKey) - if err != nil { - return nil, err - } + switch authMethod.method() { + case "publickey": - } else { - - ok, err := p.checkPublicKey(msg, downKey, sig) - - if err != nil { - return nil, err - } + f, ok := authMethod.(publicKeyCallback) if !ok { - return noneAuthMsg(mappedUser), nil + return nil, errors.New("sshpiper: publicKeyCallback type assertions failed") } - msg, err = p.signAgain(mappedUser, msg, signer, downKey) + signers, err := f() + // no mapped user change it to none or error occur + //if err != nil || len(signers) == 0 { if err != nil { return nil, err } + + for _, signer := range signers { + msg, err = p.signAgain(mappedUser, msg, signer) + if err != nil { + return nil, err + } + } + case "password": + + f, ok := authMethod.(passwordCallback) + + if !ok { + return nil, errors.New("sshpiper: passwordCallback type assertions failed") + } + + pw, err := f() + if err != nil { + return nil, err + } + + type passwordAuthMsg struct { + User string `sshtype:"50"` + Service string + Method string + Reply bool + Password string + } + + Unmarshal(Marshal(passwordAuthMsg{ + User: mappedUser, + Service: serviceSSH, + Method: "password", + Reply: false, + Password: pw, + }), msg) + + return msg, nil + + default: + } return msg, nil @@ -278,17 +396,27 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn, return &SSHPiperConn{pipedConn: p}, nil } +func (pipe *pipedConn) ack(key PublicKey) error { + okMsg := userAuthPubKeyOkMsg{ + Algo: key.Type(), + PubKey: key.Marshal(), + } + + if err := pipe.downstream.transport.writePacket(Marshal(&okMsg)); err != nil { + return err + } + + return nil +} + +// not used after method to method map enable func (pipe *pipedConn) validAndAck(user string, upKey, downKey PublicKey) (*userAuthRequestMsg, error) { ok, err := validateKey(upKey, user, pipe.upstream.transport) if ok { - okMsg := userAuthPubKeyOkMsg{ - Algo: downKey.Type(), - PubKey: downKey.Marshal(), - } - if err = pipe.downstream.transport.writePacket(Marshal(&okMsg)); err != nil { + if err = pipe.ack(downKey); err != nil { return nil, err } @@ -312,7 +440,7 @@ func (pipe *pipedConn) checkPublicKey(msg *userAuthRequestMsg, pubkey PublicKey, return true, nil } -func (pipe *pipedConn) signAgain(user string, msg *userAuthRequestMsg, signer Signer, downKey PublicKey) (*userAuthRequestMsg, error) { +func (pipe *pipedConn) signAgain(user string, msg *userAuthRequestMsg, signer Signer) (*userAuthRequestMsg, error) { rand := pipe.upstream.transport.config.Rand session := pipe.upstream.transport.getSessionID() diff --git a/vendor/golang.org/x/crypto/ssh/sshpiper_test.go b/vendor/golang.org/x/crypto/ssh/sshpiper_test.go index cc47f737..0c30ea3f 100644 --- a/vendor/golang.org/x/crypto/ssh/sshpiper_test.go +++ b/vendor/golang.org/x/crypto/ssh/sshpiper_test.go @@ -22,16 +22,18 @@ func ExampleNewSSHPiperConn() { piper := &SSHPiperConfig{ // return conn dial to serverAddr - FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) { + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { c, err := net.Dial("tcp", serverAddr) if err != nil { - return nil, "", err + return nil, nil, err } // change upstream username to root - return c, "root", nil + return c, &SSHPiperAuthPipe{ + User: "root", + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, nil }, - UpstreamHostKeyCallback: InsecureIgnoreHostKey(), } // add private key @@ -94,14 +96,14 @@ func dialPiper(piper *SSHPiperConfig) (net.Conn, error) { return c, nil } -func TestFindUpstreamCallback(t *testing.T) { +func TestPiperFindUpstreamCallback(t *testing.T) { const username = "testuser" var called bool c, err := dialPiper(&SSHPiperConfig{ - FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) { + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { if username != conn.User() { t.Errorf("different username") } @@ -114,13 +116,18 @@ func TestFindUpstreamCallback(t *testing.T) { t.Errorf("default username changed") } + if string(password) != "password" { + t.Errorf("password not equal") + } + return nil, nil }, }, t) - return s, "", err + return s, &SSHPiperAuthPipe{ + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err }, - UpstreamHostKeyCallback: InsecureIgnoreHostKey(), }) if err != nil { @@ -140,14 +147,14 @@ func TestFindUpstreamCallback(t *testing.T) { } // TODO clean up duplicate code -func TestFindUpstreamWithUserCallback(t *testing.T) { +func TestPiperFindUpstreamWithUserCallback(t *testing.T) { const username = "testuser" const mappedname = "mappedname" var called bool c, err := dialPiper(&SSHPiperConfig{ - FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) { + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { s, err := dialUpstream(simpleEchoHandler, &ServerConfig{ PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { @@ -161,9 +168,11 @@ func TestFindUpstreamWithUserCallback(t *testing.T) { }, }, t) - return s, mappedname, err + return s, &SSHPiperAuthPipe{ + User: mappedname, + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err }, - UpstreamHostKeyCallback: InsecureIgnoreHostKey(), }) if err != nil { @@ -181,7 +190,7 @@ func TestFindUpstreamWithUserCallback(t *testing.T) { } } -func TestMapPublicKey(t *testing.T) { +func TestPiperMapPublicKey(t *testing.T) { certChecker := CertChecker{ IsUserAuthority: func(k PublicKey) bool { @@ -200,17 +209,19 @@ func TestMapPublicKey(t *testing.T) { } c, err := dialPiper(&SSHPiperConfig{ - FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) { + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { s, err := dialUpstream(simpleEchoHandler, &ServerConfig{ PublicKeyCallback: certChecker.Authenticate, }, t) - return s, "", err - }, + return s, &SSHPiperAuthPipe{ - MapPublicKey: func(conn ConnMetadata, key PublicKey) (Signer, error) { - return testSigners["rsa"], nil + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (AuthPipeType, AuthMethod, error) { + return AuthPipeTypeMap, PublicKeys(testSigners["rsa"]), nil + }, + + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err }, - UpstreamHostKeyCallback: InsecureIgnoreHostKey(), }) if err != nil { @@ -230,7 +241,177 @@ func TestMapPublicKey(t *testing.T) { } } -func TestAdditionalChallenge(t *testing.T) { +func TestPiperMapPublicKeyToPassword(t *testing.T) { + certChecker := CertChecker{ + IsUserAuthority: func(k PublicKey) bool { + return bytes.Equal(k.Marshal(), testPublicKeys["ecdsa"].Marshal()) + }, + UserKeyFallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { + return nil, nil + } + + return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User()) + }, + IsRevoked: func(c *Certificate) bool { + return c.Serial == 666 + }, + } + + var called bool + + c, err := dialPiper(&SSHPiperConfig{ + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { + s, err := dialUpstream(simpleEchoHandler, &ServerConfig{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + t.Errorf("PasswordCallback should not be called") + return nil, nil + }, + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + + called = true + return certChecker.Authenticate(conn, key) + }, + }, t) + return s, &SSHPiperAuthPipe{ + PasswordCallback: func(conn ConnMetadata, password []byte) (AuthPipeType, AuthMethod, error) { + if string(password) != "mypassword" { + t.Errorf("password not equal") + } + + return AuthPipeTypeMap, PublicKeys(testSigners["rsa"]), nil + }, + + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err + }, + }) + + if err != nil { + t.Fatalf("connect dial to piper: %v", err) + } + + _, _, _, err = NewClientConn(c, "", &ClientConfig{ + User: "testuser", + Auth: []AuthMethod{ + Password("mypassword"), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + }) + + if err != nil { + t.Fatalf("can connect to piper %v", err) + } + + if !called { + t.Fatalf("PublicKeyCallback not called") + } +} + +func TestPiperPasswordToMapPublicKey(t *testing.T) { + var called bool + + c, err := dialPiper(&SSHPiperConfig{ + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { + s, err := dialUpstream(simpleEchoHandler, &ServerConfig{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + called = true + + if string(password) != "mypassword" { + t.Errorf("password not equal") + } + + return nil, nil + }, + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + + t.Errorf("PublicKeyCallback should not be called") + return nil, nil + }, + }, t) + return s, &SSHPiperAuthPipe{ + + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (AuthPipeType, AuthMethod, error) { + return AuthPipeTypeMap, Password("mypassword"), nil + }, + + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err + }, + }) + + if err != nil { + t.Fatalf("connect dial to piper: %v", err) + } + + _, _, _, err = NewClientConn(c, "", &ClientConfig{ + User: "testuser", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + }) + + if err != nil { + t.Fatalf("can connect to piper %v", err) + } + + if !called { + t.Fatalf("PasswordCallback not called") + } +} + +func TestPiperServerWithBanner(t *testing.T) { + + const username = "testuser" + + var called bool + + c, err := dialPiper(&SSHPiperConfig{ + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { + if username != conn.User() { + t.Errorf("different username") + } + + s, err := dialUpstream(simpleEchoHandler, &ServerConfig{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + return nil, nil + }, + BannerCallback: func(conn ConnMetadata) string { + called = true + return "banner" + }, + }, t) + + return s, &SSHPiperAuthPipe{ + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err + }, + }) + + if err != nil { + t.Fatalf("connect dial to piper: %v", err) + } + + NewClientConn(c, "", &ClientConfig{ + User: username, + Auth: []AuthMethod{Password("password")}, + HostKeyCallback: InsecureIgnoreHostKey(), + BannerCallback: func(message string) error { + if message != "banner" { + t.Errorf("bad banner string") + } + + return nil + }, + }) + + if !called { + t.Fatalf("FindUpstream not called") + } +} + +func TestPiperAdditionalChallenge(t *testing.T) { c, err := dialPiper(&SSHPiperConfig{ AdditionalChallenge: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (bool, error) { ans, err := challenge("user", @@ -249,11 +430,12 @@ func TestAdditionalChallenge(t *testing.T) { } return false, fmt.Errorf("keyboard-interactive failed") }, - FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) { + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { s, err := dialUpstream(simpleEchoHandler, &ServerConfig{NoClientAuth: true}, t) - return s, "", err + return s, &SSHPiperAuthPipe{ + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err }, - UpstreamHostKeyCallback: InsecureIgnoreHostKey(), }) if err != nil { @@ -320,14 +502,15 @@ func dialUpstream(handler serverType, upstream *ServerConfig, t *testing.T) (net return c, nil } -func TestPipeData(t *testing.T) { +func TestPiperPipeData(t *testing.T) { c, err := dialPiper(&SSHPiperConfig{ - FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) { + FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) { s, err := dialUpstream(simpleEchoHandler, &ServerConfig{NoClientAuth: true}, t) - return s, "", err + return s, &SSHPiperAuthPipe{ + UpstreamHostKeyCallback: InsecureIgnoreHostKey(), + }, err }, - UpstreamHostKeyCallback: InsecureIgnoreHostKey(), }) if err != nil { diff --git a/vendor/golang.org/x/crypto/ssh/test/multi_auth_test.go b/vendor/golang.org/x/crypto/ssh/test/multi_auth_test.go new file mode 100644 index 00000000..f594d36e --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/test/multi_auth_test.go @@ -0,0 +1,144 @@ +// Copyright 2017 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. + +// Tests for ssh client multi-auth +// +// These tests run a simple go ssh client against OpenSSH server +// over unix domain sockets. The tests use multiple combinations +// of password, keyboard-interactive and publickey authentication +// methods. +// +// A wrapper library for making sshd PAM authentication use test +// passwords is required in ./sshd_test_pw.so. If the library does +// not exist these tests will be skipped. See compile instructions +// (for linux) in file ./sshd_test_pw.c. + +// +build linux + +package test + +import ( + "fmt" + "strings" + "testing" + + "golang.org/x/crypto/ssh" +) + +// test cases +type multiAuthTestCase struct { + authMethods []string + expectedPasswordCbs int + expectedKbdIntCbs int +} + +// test context +type multiAuthTestCtx struct { + password string + numPasswordCbs int + numKbdIntCbs int +} + +// create test context +func newMultiAuthTestCtx(t *testing.T) *multiAuthTestCtx { + password, err := randomPassword() + if err != nil { + t.Fatalf("Failed to generate random test password: %s", err.Error()) + } + + return &multiAuthTestCtx{ + password: password, + } +} + +// password callback +func (ctx *multiAuthTestCtx) passwordCb() (secret string, err error) { + ctx.numPasswordCbs++ + return ctx.password, nil +} + +// keyboard-interactive callback +func (ctx *multiAuthTestCtx) kbdIntCb(user, instruction string, questions []string, echos []bool) (answers []string, err error) { + if len(questions) == 0 { + return nil, nil + } + + ctx.numKbdIntCbs++ + if len(questions) == 1 { + return []string{ctx.password}, nil + } + + return nil, fmt.Errorf("unsupported keyboard-interactive flow") +} + +// TestMultiAuth runs several subtests for different combinations of password, keyboard-interactive and publickey authentication methods +func TestMultiAuth(t *testing.T) { + testCases := []multiAuthTestCase{ + // Test password,publickey authentication, assert that password callback is called 1 time + multiAuthTestCase{ + authMethods: []string{"password", "publickey"}, + expectedPasswordCbs: 1, + }, + // Test keyboard-interactive,publickey authentication, assert that keyboard-interactive callback is called 1 time + multiAuthTestCase{ + authMethods: []string{"keyboard-interactive", "publickey"}, + expectedKbdIntCbs: 1, + }, + // Test publickey,password authentication, assert that password callback is called 1 time + multiAuthTestCase{ + authMethods: []string{"publickey", "password"}, + expectedPasswordCbs: 1, + }, + // Test publickey,keyboard-interactive authentication, assert that keyboard-interactive callback is called 1 time + multiAuthTestCase{ + authMethods: []string{"publickey", "keyboard-interactive"}, + expectedKbdIntCbs: 1, + }, + // Test password,password authentication, assert that password callback is called 2 times + multiAuthTestCase{ + authMethods: []string{"password", "password"}, + expectedPasswordCbs: 2, + }, + } + + for _, testCase := range testCases { + t.Run(strings.Join(testCase.authMethods, ","), func(t *testing.T) { + ctx := newMultiAuthTestCtx(t) + + server := newServerForConfig(t, "MultiAuth", map[string]string{"AuthMethods": strings.Join(testCase.authMethods, ",")}) + defer server.Shutdown() + + clientConfig := clientConfig() + server.setTestPassword(clientConfig.User, ctx.password) + + publicKeyAuthMethod := clientConfig.Auth[0] + clientConfig.Auth = nil + for _, authMethod := range testCase.authMethods { + switch authMethod { + case "publickey": + clientConfig.Auth = append(clientConfig.Auth, publicKeyAuthMethod) + case "password": + clientConfig.Auth = append(clientConfig.Auth, + ssh.RetryableAuthMethod(ssh.PasswordCallback(ctx.passwordCb), 5)) + case "keyboard-interactive": + clientConfig.Auth = append(clientConfig.Auth, + ssh.RetryableAuthMethod(ssh.KeyboardInteractive(ctx.kbdIntCb), 5)) + default: + t.Fatalf("Unknown authentication method %s", authMethod) + } + } + + conn := server.Dial(clientConfig) + defer conn.Close() + + if ctx.numPasswordCbs != testCase.expectedPasswordCbs { + t.Fatalf("passwordCallback was called %d times, expected %d times", ctx.numPasswordCbs, testCase.expectedPasswordCbs) + } + + if ctx.numKbdIntCbs != testCase.expectedKbdIntCbs { + t.Fatalf("keyboardInteractiveCallback was called %d times, expected %d times", ctx.numKbdIntCbs, testCase.expectedKbdIntCbs) + } + }) + } +} diff --git a/vendor/golang.org/x/crypto/ssh/test/sshd_test_pw.c b/vendor/golang.org/x/crypto/ssh/test/sshd_test_pw.c new file mode 100644 index 00000000..2794a563 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/test/sshd_test_pw.c @@ -0,0 +1,173 @@ +// Copyright 2017 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. + +// sshd_test_pw.c +// Wrapper to inject test password data for sshd PAM authentication +// +// This wrapper implements custom versions of getpwnam, getpwnam_r, +// getspnam and getspnam_r. These functions first call their real +// libc versions, then check if the requested user matches test user +// specified in env variable TEST_USER and if so replace the password +// with crypted() value of TEST_PASSWD env variable. +// +// Compile: +// gcc -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c +// +// Compile with debug: +// gcc -DVERBOSE -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c +// +// Run sshd: +// LD_PRELOAD="sshd_test_pw.so" TEST_USER="..." TEST_PASSWD="..." sshd ... + +// +build ignore + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#ifdef VERBOSE +#define DEBUG(X...) fprintf(stderr, X) +#else +#define DEBUG(X...) while (0) { } +#endif + +/* crypt() password */ +static char * +pwhash(char *passwd) { + return strdup(crypt(passwd, "$6$")); +} + +/* Pointers to real functions in libc */ +static struct passwd * (*real_getpwnam)(const char *) = NULL; +static int (*real_getpwnam_r)(const char *, struct passwd *, char *, size_t, struct passwd **) = NULL; +static struct spwd * (*real_getspnam)(const char *) = NULL; +static int (*real_getspnam_r)(const char *, struct spwd *, char *, size_t, struct spwd **) = NULL; + +/* Cached test user and test password */ +static char *test_user = NULL; +static char *test_passwd_hash = NULL; + +static void +init(void) { + /* Fetch real libc function pointers */ + real_getpwnam = dlsym(RTLD_NEXT, "getpwnam"); + real_getpwnam_r = dlsym(RTLD_NEXT, "getpwnam_r"); + real_getspnam = dlsym(RTLD_NEXT, "getspnam"); + real_getspnam_r = dlsym(RTLD_NEXT, "getspnam_r"); + + /* abort if env variables are not defined */ + if (getenv("TEST_USER") == NULL || getenv("TEST_PASSWD") == NULL) { + fprintf(stderr, "env variables TEST_USER and TEST_PASSWD are missing\n"); + abort(); + } + + /* Fetch test user and test password from env */ + test_user = strdup(getenv("TEST_USER")); + test_passwd_hash = pwhash(getenv("TEST_PASSWD")); + + DEBUG("sshd_test_pw init():\n"); + DEBUG("\treal_getpwnam: %p\n", real_getpwnam); + DEBUG("\treal_getpwnam_r: %p\n", real_getpwnam_r); + DEBUG("\treal_getspnam: %p\n", real_getspnam); + DEBUG("\treal_getspnam_r: %p\n", real_getspnam_r); + DEBUG("\tTEST_USER: '%s'\n", test_user); + DEBUG("\tTEST_PASSWD: '%s'\n", getenv("TEST_PASSWD")); + DEBUG("\tTEST_PASSWD_HASH: '%s'\n", test_passwd_hash); +} + +static int +is_test_user(const char *name) { + if (test_user != NULL && strcmp(test_user, name) == 0) + return 1; + return 0; +} + +/* getpwnam */ + +struct passwd * +getpwnam(const char *name) { + struct passwd *pw; + + DEBUG("sshd_test_pw getpwnam(%s)\n", name); + + if (real_getpwnam == NULL) + init(); + if ((pw = real_getpwnam(name)) == NULL) + return NULL; + + if (is_test_user(name)) + pw->pw_passwd = strdup(test_passwd_hash); + + return pw; +} + +/* getpwnam_r */ + +int +getpwnam_r(const char *name, + struct passwd *pwd, + char *buf, + size_t buflen, + struct passwd **result) { + int r; + + DEBUG("sshd_test_pw getpwnam_r(%s)\n", name); + + if (real_getpwnam_r == NULL) + init(); + if ((r = real_getpwnam_r(name, pwd, buf, buflen, result)) != 0 || *result == NULL) + return r; + + if (is_test_user(name)) + pwd->pw_passwd = strdup(test_passwd_hash); + + return 0; +} + +/* getspnam */ + +struct spwd * +getspnam(const char *name) { + struct spwd *sp; + + DEBUG("sshd_test_pw getspnam(%s)\n", name); + + if (real_getspnam == NULL) + init(); + if ((sp = real_getspnam(name)) == NULL) + return NULL; + + if (is_test_user(name)) + sp->sp_pwdp = strdup(test_passwd_hash); + + return sp; +} + +/* getspnam_r */ + +int +getspnam_r(const char *name, + struct spwd *spbuf, + char *buf, + size_t buflen, + struct spwd **spbufp) { + int r; + + DEBUG("sshd_test_pw getspnam_r(%s)\n", name); + + if (real_getspnam_r == NULL) + init(); + if ((r = real_getspnam_r(name, spbuf, buf, buflen, spbufp)) != 0) + return r; + + if (is_test_user(name)) + spbuf->sp_pwdp = strdup(test_passwd_hash); + + return r; +} diff --git a/vendor/golang.org/x/crypto/ssh/test/test_unix_test.go b/vendor/golang.org/x/crypto/ssh/test/test_unix_test.go index 15b879d3..39607868 100644 --- a/vendor/golang.org/x/crypto/ssh/test/test_unix_test.go +++ b/vendor/golang.org/x/crypto/ssh/test/test_unix_test.go @@ -10,6 +10,8 @@ package test import ( "bytes" + "crypto/rand" + "encoding/base64" "fmt" "io/ioutil" "log" @@ -25,7 +27,8 @@ import ( "golang.org/x/crypto/ssh/testdata" ) -const sshdConfig = ` +const ( + defaultSshdConfig = ` Protocol 2 Banner {{.Dir}}/banner HostKey {{.Dir}}/id_rsa @@ -50,8 +53,17 @@ RhostsRSAAuthentication no HostbasedAuthentication no PubkeyAcceptedKeyTypes=* ` + multiAuthSshdConfigTail = ` +UsePAM yes +PasswordAuthentication yes +ChallengeResponseAuthentication yes +AuthenticationMethods {{.AuthMethods}} +` +) -var configTmpl = template.Must(template.New("").Parse(sshdConfig)) +var configTmpl = map[string]*template.Template{ + "default": template.Must(template.New("").Parse(defaultSshdConfig)), + "MultiAuth": template.Must(template.New("").Parse(defaultSshdConfig + multiAuthSshdConfigTail))} type server struct { t *testing.T @@ -60,6 +72,10 @@ type server struct { cmd *exec.Cmd output bytes.Buffer // holds stderr from sshd process + testUser string // test username for sshd + testPasswd string // test password for sshd + sshdTestPwSo string // dynamic library to inject a custom password into sshd + // Client half of the network connection. clientConn net.Conn } @@ -186,6 +202,20 @@ func (s *server) TryDialWithAddr(config *ssh.ClientConfig, addr string) (*ssh.Cl s.cmd.Stdin = f s.cmd.Stdout = f s.cmd.Stderr = &s.output + + if s.sshdTestPwSo != "" { + if s.testUser == "" { + s.t.Fatal("user missing from sshd_test_pw.so config") + } + if s.testPasswd == "" { + s.t.Fatal("password missing from sshd_test_pw.so config") + } + s.cmd.Env = append(os.Environ(), + fmt.Sprintf("LD_PRELOAD=%s", s.sshdTestPwSo), + fmt.Sprintf("TEST_USER=%s", s.testUser), + fmt.Sprintf("TEST_PASSWD=%s", s.testPasswd)) + } + if err := s.cmd.Start(); err != nil { s.t.Fail() s.Shutdown() @@ -236,8 +266,39 @@ func writeFile(path string, contents []byte) { } } +// generate random password +func randomPassword() (string, error) { + b := make([]byte, 12) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// setTestPassword is used for setting user and password data for sshd_test_pw.so +// This function also checks that ./sshd_test_pw.so exists and if not calls s.t.Skip() +func (s *server) setTestPassword(user, passwd string) error { + wd, _ := os.Getwd() + wrapper := filepath.Join(wd, "sshd_test_pw.so") + if _, err := os.Stat(wrapper); err != nil { + s.t.Skip(fmt.Errorf("sshd_test_pw.so is not available")) + return err + } + + s.sshdTestPwSo = wrapper + s.testUser = user + s.testPasswd = passwd + return nil +} + // newServer returns a new mock ssh server. func newServer(t *testing.T) *server { + return newServerForConfig(t, "default", map[string]string{}) +} + +// newServerForConfig returns a new mock ssh server. +func newServerForConfig(t *testing.T, config string, configVars map[string]string) *server { if testing.Short() { t.Skip("skipping test due to -short") } @@ -249,9 +310,11 @@ func newServer(t *testing.T) *server { if err != nil { t.Fatal(err) } - err = configTmpl.Execute(f, map[string]string{ - "Dir": dir, - }) + if _, ok := configTmpl[config]; ok == false { + t.Fatal(fmt.Errorf("Invalid server config '%s'", config)) + } + configVars["Dir"] = dir + err = configTmpl[config].Execute(f, configVars) if err != nil { t.Fatal(err) }