The existing client side authentication does not handle correctly the partial success flag in SSH_MSG_USERAUTH_FAILURE authentication responses. This commit fixes two problems in ssh library: 1) RetryableAuthMethod() now breaks out from the retry loop and returns when underlying auth method fails with partial success set to true. 2) Book keeping of tried (and failed) auth methods in clientAuthenticate() does not mark an auth method failed if it fails with partial success set to true. Fixes golang/go#23461 Change-Id: Ib2e1a1d54bfe2549496199bb2f66ebbce58d130d Reviewed-on: https://go-review.googlesource.com/88035 Reviewed-by: Han-Wen Nienhuys <hanwen@google.com> Run-TryBot: Han-Wen Nienhuys <hanwen@google.com>
501 lines
13 KiB
Go
501 lines
13 KiB
Go
// Copyright 2014 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.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/dsa"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/ed25519"
|
|
"golang.org/x/crypto/ssh/testdata"
|
|
)
|
|
|
|
func rawKey(pub PublicKey) interface{} {
|
|
switch k := pub.(type) {
|
|
case *rsaPublicKey:
|
|
return (*rsa.PublicKey)(k)
|
|
case *dsaPublicKey:
|
|
return (*dsa.PublicKey)(k)
|
|
case *ecdsaPublicKey:
|
|
return (*ecdsa.PublicKey)(k)
|
|
case ed25519PublicKey:
|
|
return (ed25519.PublicKey)(k)
|
|
case *Certificate:
|
|
return k
|
|
}
|
|
panic("unknown key type")
|
|
}
|
|
|
|
func TestKeyMarshalParse(t *testing.T) {
|
|
for _, priv := range testSigners {
|
|
pub := priv.PublicKey()
|
|
roundtrip, err := ParsePublicKey(pub.Marshal())
|
|
if err != nil {
|
|
t.Errorf("ParsePublicKey(%T): %v", pub, err)
|
|
}
|
|
|
|
k1 := rawKey(pub)
|
|
k2 := rawKey(roundtrip)
|
|
|
|
if !reflect.DeepEqual(k1, k2) {
|
|
t.Errorf("got %#v in roundtrip, want %#v", k2, k1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedCurves(t *testing.T) {
|
|
raw, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey: %v", err)
|
|
}
|
|
|
|
if _, err = NewSignerFromKey(raw); err == nil || !strings.Contains(err.Error(), "only P-256") {
|
|
t.Fatalf("NewPrivateKey should not succeed with P-224, got: %v", err)
|
|
}
|
|
|
|
if _, err = NewPublicKey(&raw.PublicKey); err == nil || !strings.Contains(err.Error(), "only P-256") {
|
|
t.Fatalf("NewPublicKey should not succeed with P-224, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewPublicKey(t *testing.T) {
|
|
for _, k := range testSigners {
|
|
raw := rawKey(k.PublicKey())
|
|
// Skip certificates, as NewPublicKey does not support them.
|
|
if _, ok := raw.(*Certificate); ok {
|
|
continue
|
|
}
|
|
pub, err := NewPublicKey(raw)
|
|
if err != nil {
|
|
t.Errorf("NewPublicKey(%#v): %v", raw, err)
|
|
}
|
|
if !reflect.DeepEqual(k.PublicKey(), pub) {
|
|
t.Errorf("NewPublicKey(%#v) = %#v, want %#v", raw, pub, k.PublicKey())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKeySignVerify(t *testing.T) {
|
|
for _, priv := range testSigners {
|
|
pub := priv.PublicKey()
|
|
|
|
data := []byte("sign me")
|
|
sig, err := priv.Sign(rand.Reader, data)
|
|
if err != nil {
|
|
t.Fatalf("Sign(%T): %v", priv, err)
|
|
}
|
|
|
|
if err := pub.Verify(data, sig); err != nil {
|
|
t.Errorf("publicKey.Verify(%T): %v", priv, err)
|
|
}
|
|
sig.Blob[5]++
|
|
if err := pub.Verify(data, sig); err == nil {
|
|
t.Errorf("publicKey.Verify on broken sig did not fail")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseRSAPrivateKey(t *testing.T) {
|
|
key := testPrivateKeys["rsa"]
|
|
|
|
rsa, ok := key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
t.Fatalf("got %T, want *rsa.PrivateKey", rsa)
|
|
}
|
|
|
|
if err := rsa.Validate(); err != nil {
|
|
t.Errorf("Validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseECPrivateKey(t *testing.T) {
|
|
key := testPrivateKeys["ecdsa"]
|
|
|
|
ecKey, ok := key.(*ecdsa.PrivateKey)
|
|
if !ok {
|
|
t.Fatalf("got %T, want *ecdsa.PrivateKey", ecKey)
|
|
}
|
|
|
|
if !validateECPublicKey(ecKey.Curve, ecKey.X, ecKey.Y) {
|
|
t.Fatalf("public key does not validate.")
|
|
}
|
|
}
|
|
|
|
// See Issue https://github.com/golang/go/issues/6650.
|
|
func TestParseEncryptedPrivateKeysFails(t *testing.T) {
|
|
const wantSubstring = "encrypted"
|
|
for i, tt := range testdata.PEMEncryptedKeys {
|
|
_, err := ParsePrivateKey(tt.PEMBytes)
|
|
if err == nil {
|
|
t.Errorf("#%d key %s: ParsePrivateKey successfully parsed, expected an error", i, tt.Name)
|
|
continue
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), wantSubstring) {
|
|
t.Errorf("#%d key %s: got error %q, want substring %q", i, tt.Name, err, wantSubstring)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse encrypted private keys with passphrase
|
|
func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) {
|
|
data := []byte("sign me")
|
|
for _, tt := range testdata.PEMEncryptedKeys {
|
|
s, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey))
|
|
if err != nil {
|
|
t.Fatalf("ParsePrivateKeyWithPassphrase returned error: %s", err)
|
|
continue
|
|
}
|
|
sig, err := s.Sign(rand.Reader, data)
|
|
if err != nil {
|
|
t.Fatalf("dsa.Sign: %v", err)
|
|
}
|
|
if err := s.PublicKey().Verify(data, sig); err != nil {
|
|
t.Errorf("Verify failed: %v", err)
|
|
}
|
|
}
|
|
|
|
tt := testdata.PEMEncryptedKeys[0]
|
|
_, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte("incorrect"))
|
|
if err != x509.IncorrectPasswordError {
|
|
t.Fatalf("got %v want IncorrectPasswordError", err)
|
|
}
|
|
}
|
|
|
|
func TestParseDSA(t *testing.T) {
|
|
// We actually exercise the ParsePrivateKey codepath here, as opposed to
|
|
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
|
|
// uses.
|
|
s, err := ParsePrivateKey(testdata.PEMBytes["dsa"])
|
|
if err != nil {
|
|
t.Fatalf("ParsePrivateKey returned error: %s", err)
|
|
}
|
|
|
|
data := []byte("sign me")
|
|
sig, err := s.Sign(rand.Reader, data)
|
|
if err != nil {
|
|
t.Fatalf("dsa.Sign: %v", err)
|
|
}
|
|
|
|
if err := s.PublicKey().Verify(data, sig); err != nil {
|
|
t.Errorf("Verify failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Tests for authorized_keys parsing.
|
|
|
|
// getTestKey returns a public key, and its base64 encoding.
|
|
func getTestKey() (PublicKey, string) {
|
|
k := testPublicKeys["rsa"]
|
|
|
|
b := &bytes.Buffer{}
|
|
e := base64.NewEncoder(base64.StdEncoding, b)
|
|
e.Write(k.Marshal())
|
|
e.Close()
|
|
|
|
return k, b.String()
|
|
}
|
|
|
|
func TestMarshalParsePublicKey(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
line := fmt.Sprintf("%s %s user@host", pub.Type(), pubSerialized)
|
|
|
|
authKeys := MarshalAuthorizedKey(pub)
|
|
actualFields := strings.Fields(string(authKeys))
|
|
if len(actualFields) == 0 {
|
|
t.Fatalf("failed authKeys: %v", authKeys)
|
|
}
|
|
|
|
// drop the comment
|
|
expectedFields := strings.Fields(line)[0:2]
|
|
|
|
if !reflect.DeepEqual(actualFields, expectedFields) {
|
|
t.Errorf("got %v, expected %v", actualFields, expectedFields)
|
|
}
|
|
|
|
actPub, _, _, _, err := ParseAuthorizedKey([]byte(line))
|
|
if err != nil {
|
|
t.Fatalf("cannot parse %v: %v", line, err)
|
|
}
|
|
if !reflect.DeepEqual(actPub, pub) {
|
|
t.Errorf("got %v, expected %v", actPub, pub)
|
|
}
|
|
}
|
|
|
|
type testAuthResult struct {
|
|
pubKey PublicKey
|
|
options []string
|
|
comments string
|
|
rest string
|
|
ok bool
|
|
}
|
|
|
|
func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []testAuthResult) {
|
|
rest := authKeys
|
|
var values []testAuthResult
|
|
for len(rest) > 0 {
|
|
var r testAuthResult
|
|
var err error
|
|
r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest)
|
|
r.ok = (err == nil)
|
|
t.Log(err)
|
|
r.rest = string(rest)
|
|
values = append(values, r)
|
|
}
|
|
|
|
if !reflect.DeepEqual(values, expected) {
|
|
t.Errorf("got %#v, expected %#v", values, expected)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizedKeyBasic(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
line := "ssh-rsa " + pubSerialized + " user@host"
|
|
testAuthorizedKeys(t, []byte(line),
|
|
[]testAuthResult{
|
|
{pub, nil, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestAuth(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
authWithOptions := []string{
|
|
`# comments to ignore before any keys...`,
|
|
``,
|
|
`env="HOME=/home/root",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`,
|
|
`# comments to ignore, along with a blank line`,
|
|
``,
|
|
`env="HOME=/home/root2" ssh-rsa ` + pubSerialized + ` user2@host2`,
|
|
``,
|
|
`# more comments, plus a invalid entry`,
|
|
`ssh-rsa data-that-will-not-parse user@host3`,
|
|
}
|
|
for _, eol := range []string{"\n", "\r\n"} {
|
|
authOptions := strings.Join(authWithOptions, eol)
|
|
rest2 := strings.Join(authWithOptions[3:], eol)
|
|
rest3 := strings.Join(authWithOptions[6:], eol)
|
|
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},
|
|
})
|
|
}
|
|
}
|
|
|
|
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), []testAuthResult{
|
|
{pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
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), []testAuthResult{
|
|
{pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
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), []testAuthResult{
|
|
{pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true},
|
|
})
|
|
|
|
testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []testAuthResult{
|
|
{pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
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), []testAuthResult{
|
|
{nil, nil, "", "", false},
|
|
})
|
|
}
|
|
|
|
func TestAuthWithMissingQuote(t *testing.T) {
|
|
pub, pubSerialized := getTestKey()
|
|
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), []testAuthResult{
|
|
{pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true},
|
|
})
|
|
}
|
|
|
|
func TestInvalidEntry(t *testing.T) {
|
|
authInvalid := []byte(`ssh-rsa`)
|
|
_, _, _, _, err := ParseAuthorizedKey(authInvalid)
|
|
if err == nil {
|
|
t.Errorf("got valid entry for %q", authInvalid)
|
|
}
|
|
}
|
|
|
|
var knownHostsParseTests = []struct {
|
|
input string
|
|
err string
|
|
|
|
marker string
|
|
comment string
|
|
hosts []string
|
|
rest string
|
|
}{
|
|
{
|
|
"",
|
|
"EOF",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
{
|
|
"# Just a comment",
|
|
"EOF",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
{
|
|
" \t ",
|
|
"EOF",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
{
|
|
"localhost ssh-rsa {RSAPUB}",
|
|
"",
|
|
|
|
"", "", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}",
|
|
"",
|
|
|
|
"", "", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment\n",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment\r\n",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "",
|
|
},
|
|
{
|
|
"localhost\tssh-rsa {RSAPUB}\tcomment comment\r\nnext line",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost"}, "next line",
|
|
},
|
|
{
|
|
"localhost,[host2:123]\tssh-rsa {RSAPUB}\tcomment comment",
|
|
"",
|
|
|
|
"", "comment comment", []string{"localhost", "[host2:123]"}, "",
|
|
},
|
|
{
|
|
"@marker \tlocalhost,[host2:123]\tssh-rsa {RSAPUB}",
|
|
"",
|
|
|
|
"marker", "", []string{"localhost", "[host2:123]"}, "",
|
|
},
|
|
{
|
|
"@marker \tlocalhost,[host2:123]\tssh-rsa aabbccdd",
|
|
"short read",
|
|
|
|
"", "", nil, "",
|
|
},
|
|
}
|
|
|
|
func TestKnownHostsParsing(t *testing.T) {
|
|
rsaPub, rsaPubSerialized := getTestKey()
|
|
|
|
for i, test := range knownHostsParseTests {
|
|
var expectedKey PublicKey
|
|
const rsaKeyToken = "{RSAPUB}"
|
|
|
|
input := test.input
|
|
if strings.Contains(input, rsaKeyToken) {
|
|
expectedKey = rsaPub
|
|
input = strings.Replace(test.input, rsaKeyToken, rsaPubSerialized, -1)
|
|
}
|
|
|
|
marker, hosts, pubKey, comment, rest, err := ParseKnownHosts([]byte(input))
|
|
if err != nil {
|
|
if len(test.err) == 0 {
|
|
t.Errorf("#%d: unexpectedly failed with %q", i, err)
|
|
} else if !strings.Contains(err.Error(), test.err) {
|
|
t.Errorf("#%d: expected error containing %q, but got %q", i, test.err, err)
|
|
}
|
|
continue
|
|
} else if len(test.err) != 0 {
|
|
t.Errorf("#%d: succeeded but expected error including %q", i, test.err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(expectedKey, pubKey) {
|
|
t.Errorf("#%d: expected key %#v, but got %#v", i, expectedKey, pubKey)
|
|
}
|
|
|
|
if marker != test.marker {
|
|
t.Errorf("#%d: expected marker %q, but got %q", i, test.marker, marker)
|
|
}
|
|
|
|
if comment != test.comment {
|
|
t.Errorf("#%d: expected comment %q, but got %q", i, test.comment, comment)
|
|
}
|
|
|
|
if !reflect.DeepEqual(test.hosts, hosts) {
|
|
t.Errorf("#%d: expected hosts %#v, but got %#v", i, test.hosts, hosts)
|
|
}
|
|
|
|
if rest := string(rest); rest != test.rest {
|
|
t.Errorf("#%d: expected remaining input to be %q, but got %q", i, test.rest, rest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFingerprintLegacyMD5(t *testing.T) {
|
|
pub, _ := getTestKey()
|
|
fingerprint := FingerprintLegacyMD5(pub)
|
|
want := "fb:61:6d:1a:e3:f0:95:45:3c:a0:79:be:4a:93:63:66" // ssh-keygen -lf -E md5 rsa
|
|
if fingerprint != want {
|
|
t.Errorf("got fingerprint %q want %q", fingerprint, want)
|
|
}
|
|
}
|
|
|
|
func TestFingerprintSHA256(t *testing.T) {
|
|
pub, _ := getTestKey()
|
|
fingerprint := FingerprintSHA256(pub)
|
|
want := "SHA256:Anr3LjZK8YVpjrxu79myrW9Hrb/wpcMNpVvTq/RcBm8" // ssh-keygen -lf rsa
|
|
if fingerprint != want {
|
|
t.Errorf("got fingerprint %q want %q", fingerprint, want)
|
|
}
|
|
}
|