In the server's publickey authentication path, a public key blob that ParsePublicKey could not decode caused serverAuthenticate to return an error, tearing down the whole connection. A key we cannot parse is part of an individual authentication attempt, not a transport-level framing error, so it must fail only that attempt. This lets the client fall back to other keys or authentication methods. Updates golang/go#80075 Change-Id: I8d882d08bb50eb33e0054d9d9c2bd9e0b3039443 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/795420 Reviewed-by: David Chase <drchase@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com> Reviewed-by: Filippo Valsorda <filippo@golang.org> Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
2222 lines
61 KiB
Go
2222 lines
61 KiB
Go
// 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.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type keyboardInteractive map[string]string
|
|
|
|
func (cr keyboardInteractive) Challenge(user string, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
var answers []string
|
|
for _, q := range questions {
|
|
answers = append(answers, cr[q])
|
|
}
|
|
return answers, nil
|
|
}
|
|
|
|
// reused internally by tests
|
|
var clientPassword = "tiger"
|
|
|
|
// tryAuth runs a handshake with a given config against an SSH server
|
|
// with config serverConfig. Returns both client and server side errors.
|
|
func tryAuth(t *testing.T, config *ClientConfig) error {
|
|
err, _ := tryAuthBothSides(t, config, nil)
|
|
return err
|
|
}
|
|
|
|
// tryAuthWithGSSAPIWithMICConfig runs a handshake with a given config against an SSH server
|
|
// with a given GSSAPIWithMICConfig and config serverConfig. Returns both client and server side errors.
|
|
func tryAuthWithGSSAPIWithMICConfig(t *testing.T, clientConfig *ClientConfig, gssAPIWithMICConfig *GSSAPIWithMICConfig) error {
|
|
err, _ := tryAuthBothSides(t, clientConfig, gssAPIWithMICConfig)
|
|
return err
|
|
}
|
|
|
|
// tryAuthBothSides runs the handshake and returns the resulting errors from both sides of the connection.
|
|
func tryAuthBothSides(t *testing.T, config *ClientConfig, gssAPIWithMICConfig *GSSAPIWithMICConfig) (clientError error, serverAuthErrors []error) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
certChecker := CertChecker{
|
|
IsUserAuthority: func(k PublicKey) bool {
|
|
return bytes.Equal(k.Marshal(), testPublicKeys["ecdsa"].Marshal())
|
|
},
|
|
UserKeyFallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if conn.User() == "testuser" && 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
|
|
},
|
|
}
|
|
serverConfig := &ServerConfig{
|
|
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
|
|
if conn.User() == "testuser" && string(pass) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("password auth failed")
|
|
},
|
|
PublicKeyCallback: certChecker.Authenticate,
|
|
KeyboardInteractiveCallback: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
ans, err := challenge("user",
|
|
"instruction",
|
|
[]string{"question1", "question2"},
|
|
[]bool{true, true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ok := conn.User() == "testuser" && ans[0] == "answer1" && ans[1] == "answer2"
|
|
if ok {
|
|
challenge("user", "motd", nil, nil)
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("keyboard-interactive failed")
|
|
},
|
|
GSSAPIWithMICConfig: gssAPIWithMICConfig,
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
serverConfig.AuthLogCallback = func(conn ConnMetadata, method string, err error) {
|
|
serverAuthErrors = append(serverAuthErrors, err)
|
|
}
|
|
|
|
go newServer(c1, serverConfig)
|
|
_, _, _, err = NewClientConn(c2, "", config)
|
|
return err, serverAuthErrors
|
|
}
|
|
|
|
type loggingAlgorithmSigner struct {
|
|
used []string
|
|
AlgorithmSigner
|
|
}
|
|
|
|
func (l *loggingAlgorithmSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
|
|
l.used = append(l.used, "[Sign]")
|
|
return l.AlgorithmSigner.Sign(rand, data)
|
|
}
|
|
|
|
func (l *loggingAlgorithmSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) {
|
|
l.used = append(l.used, algorithm)
|
|
return l.AlgorithmSigner.SignWithAlgorithm(rand, data, algorithm)
|
|
}
|
|
|
|
func TestClientAuthPublicKey(t *testing.T) {
|
|
signer := &loggingAlgorithmSigner{AlgorithmSigner: testSigners["rsa"].(AlgorithmSigner)}
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(signer),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
if len(signer.used) != 1 || signer.used[0] != KeyAlgoRSASHA256 {
|
|
t.Errorf("unexpected Sign/SignWithAlgorithm calls: %q", signer.used)
|
|
}
|
|
}
|
|
|
|
// TestClientAuthNoSHA2 tests a ssh-rsa Signer that doesn't implement AlgorithmSigner.
|
|
func TestClientAuthNoSHA2(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(&legacyRSASigner{testSigners["rsa"]}),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClientAuthThirdKey checks that the third configured can succeed. If we
|
|
// were to do three attempts for each key (rsa-sha2-256, rsa-sha2-512, ssh-rsa),
|
|
// we'd hit the six maximum attempts before reaching it.
|
|
func TestClientAuthThirdKey(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa-openssh-format"],
|
|
testSigners["rsa-openssh-format"], testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
// invalidRSASigner offers an RSA public key the server cannot parse: its public
|
|
// exponent is even, which parseRSA rejects regardless of any key size limit.
|
|
// The key is intentionally invalid for a structural reason rather than for its
|
|
// size, so the test stays meaningful even if the accepted modulus size changes.
|
|
// Its Sign method is never reached, because the key is rejected during the
|
|
// initial public key query.
|
|
type invalidRSASigner struct{}
|
|
|
|
func (invalidRSASigner) PublicKey() PublicKey {
|
|
n := new(big.Int).Lsh(big.NewInt(1), 2048)
|
|
pub, err := NewPublicKey(&rsa.PublicKey{N: n, E: 2}) // incorrect exponent.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return pub
|
|
}
|
|
|
|
func (invalidRSASigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
|
|
return nil, errors.New("ssh: invalid test key must not be used to sign")
|
|
}
|
|
|
|
func TestClientAuthInvalidPublicKey(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(invalidRSASigner{}, testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("client auth failed but should have fallen back to a valid key: %s", err)
|
|
}
|
|
}
|
|
|
|
// partialSuccessPublicKeyAndKbdInteractiveServer returns a server config
|
|
// where the rsa key gets partial success and the server offers both publickey
|
|
// and keyboard-interactive as next methods. The VerifiedPublicKeyCallback
|
|
// tracks state so that the same key only triggers partial success once,
|
|
// avoiding an impossible auth loop.
|
|
func partialSuccessPublicKeyAndKbdInteractiveServer() *ServerConfig {
|
|
rsaDone := false
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
if !rsaDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
rsaDone = true
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid token")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
return serverConf
|
|
}
|
|
|
|
// partialSuccessMultiKeyServer returns a server config that requires two
|
|
// public keys in sequence: the first key (rsa) triggers partial success
|
|
// requiring a second key (ecdsa) to complete authentication.
|
|
func partialSuccessMultiKeyServer() *ServerConfig {
|
|
firstKeyDone := false
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
if !firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
firstKeyDone = true
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
if firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
return serverConf
|
|
}
|
|
|
|
func TestStaticMultiKeyPartialSuccess(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessMultiKeyServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestStaticPartialSuccess(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
// partialSuccessKeyAThenKeyBServer returns a server config where keyA (rsa)
|
|
// gets partial success and the server offers BOTH publickey and
|
|
// keyboard-interactive. keyB (ecdsa) then completes auth successfully.
|
|
func partialSuccessKeyAThenKeyBServer() *ServerConfig {
|
|
keyADone := false
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
if !keyADone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
keyADone = true
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid token")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
if keyADone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
return serverConf
|
|
}
|
|
|
|
func TestStaticPartialSuccessKeyAThenKeyB(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
// partialSuccessMultiKeyThenKbdInteractiveServer returns a server config
|
|
// that strictly requires keyA (rsa) AND keyB (ecdsa) as public keys before
|
|
// allowing keyboard-interactive. After keyA, ONLY publickey is offered
|
|
// (for keyB). After keyB, ONLY keyboard-interactive is offered.
|
|
func partialSuccessMultiKeyThenKbdInteractiveServer() *ServerConfig {
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid token")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
return serverConf
|
|
}
|
|
|
|
func TestStaticMultiKeyPartialThenKbdInteractive(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
|
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodDynamic(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: nil,
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if len(ctx.Metadata.SessionID()) == 0 {
|
|
t.Error("empty sessionID")
|
|
}
|
|
if ctx.Algorithms.HostKey == "" {
|
|
t.Error("empty Host Key")
|
|
}
|
|
if slices.Contains(ctx.AllowedMethods, "password") {
|
|
return Password(clientPassword), nil
|
|
}
|
|
return nil, errors.New("auth callback error")
|
|
},
|
|
}
|
|
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackError(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
return nil, errors.New("auth callback error")
|
|
},
|
|
}
|
|
|
|
if err := tryAuth(t, config); err == nil {
|
|
t.Fatal("authentication unexpectedly succeeded after AuthCallback returned an error")
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackPartialSuccess(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
|
return PublicKeys(testSigners["rsa"]), nil
|
|
}
|
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}), nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackMultiKeyPartialSuccess(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
|
return PublicKeys(testSigners["rsa"]), nil
|
|
}
|
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "publickey") {
|
|
return PublicKeys(testSigners["ecdsa"]), nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessMultiKeyServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackMultiKeyPartialThenKbdInteractive(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
partialCount := len(ctx.PartialSuccessMethods)
|
|
if partialCount == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
|
return PublicKeys(testSigners["rsa"]), nil
|
|
}
|
|
if partialCount == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
|
return PublicKeys(testSigners["ecdsa"]), nil
|
|
}
|
|
if partialCount >= 2 && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}), nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
|
|
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackPartialSuccessKeyAThenKeyB(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if len(ctx.PartialSuccessMethods) == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
|
return PublicKeys(testSigners["rsa"]), nil
|
|
}
|
|
if len(ctx.PartialSuccessMethods) == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
|
return PublicKeys(testSigners["ecdsa"]), nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v",
|
|
ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackPartialSuccessWithStaticAuth(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}), nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func partialSuccessPubKeyForbiddenAfterPartialServer(maxTries int) *ServerConfig {
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return nil, errors.New("legacy public key authentication is forbidden")
|
|
},
|
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid token")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
MaxAuthTries: maxTries,
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
return serverConf
|
|
}
|
|
|
|
func TestAuthCallbackPartialSuccessWithStaticMultiKey(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(
|
|
testSigners["rsa"],
|
|
testSigners["ecdsa"],
|
|
testSigners["ed25519"],
|
|
),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}), nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, partialSuccessPubKeyForbiddenAfterPartialServer(2))
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackPartialSuccessShortCircuit(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := partialSuccessPubKeyForbiddenAfterPartialServer(2)
|
|
|
|
pubkeyAttempts := 0
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
|
pubkeyAttempts++
|
|
return PublicKeys(testSigners["rsa"], testSigners["ecdsa"], testSigners["ed25519"]), nil
|
|
}
|
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"correct-token"}, nil
|
|
}), nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
|
|
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
if pubkeyAttempts != 1 {
|
|
t.Errorf("expected publickey to be attempted once before short-circuit, got %d", pubkeyAttempts)
|
|
}
|
|
}
|
|
|
|
func TestAuthCallbackTriedLoopBound(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
MaxAuthTries: -1,
|
|
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
|
|
return nil, errors.New("password rejected")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
invocations := 0
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
invocations++
|
|
return Password("wrong"), nil
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err == nil {
|
|
t.Fatal("expected the client to abort after exceeding the attempts cap")
|
|
}
|
|
if !strings.Contains(err.Error(), "too many authentication attempts") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if invocations != maxAuthClientTried {
|
|
t.Errorf("AuthCallback invoked %d times; expected %d",
|
|
invocations, maxAuthClientTried)
|
|
}
|
|
}
|
|
|
|
func alwaysPartialPubKeyServer() *ServerConfig {
|
|
var alwaysPartial func(ConnMetadata, PublicKey) (*Permissions, error)
|
|
alwaysPartial = func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if !bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PublicKeyCallback: alwaysPartial,
|
|
},
|
|
}
|
|
}
|
|
serverConf := &ServerConfig{
|
|
MaxAuthTries: -1,
|
|
PublicKeyCallback: alwaysPartial,
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
return serverConf
|
|
}
|
|
|
|
func TestAuthCallbackPartialSuccessLoopBound(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
invocations := 0
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
|
invocations++
|
|
return PublicKeys(testSigners["rsa"]), nil
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, alwaysPartialPubKeyServer())
|
|
|
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
|
if err == nil {
|
|
t.Fatal("expected the client to abort after exceeding the attempts cap")
|
|
}
|
|
if !strings.Contains(err.Error(), "too many authentication attempts") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if invocations != maxAuthClientTried {
|
|
t.Errorf("AuthCallback invoked %d times; expected %d",
|
|
invocations, maxAuthClientTried)
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodPassword(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodFallback(t *testing.T) {
|
|
var passwordCalled bool
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
PasswordCallback(
|
|
func() (string, error) {
|
|
passwordCalled = true
|
|
return "WRONG", nil
|
|
}),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
|
|
if passwordCalled {
|
|
t.Errorf("password auth tried before public-key auth.")
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodWrongPassword(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
Password("wrong"),
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodKeyboardInteractive(t *testing.T) {
|
|
answers := keyboardInteractive(map[string]string{
|
|
"question1": "answer1",
|
|
"question2": "answer2",
|
|
})
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
KeyboardInteractive(answers.Challenge),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodWrongKeyboardInteractive(t *testing.T) {
|
|
answers := keyboardInteractive(map[string]string{
|
|
"question1": "answer1",
|
|
"question2": "WRONG",
|
|
})
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
KeyboardInteractive(answers.Challenge),
|
|
},
|
|
}
|
|
|
|
if err := tryAuth(t, config); err == nil {
|
|
t.Fatalf("wrong answers should not have authenticated with KeyboardInteractive")
|
|
}
|
|
}
|
|
|
|
// the mock server will only authenticate ssh-rsa keys
|
|
func TestAuthMethodInvalidPublicKey(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["dsa"]),
|
|
},
|
|
}
|
|
|
|
if err := tryAuth(t, config); err == nil {
|
|
t.Fatalf("dsa private key should not have authenticated with rsa public key")
|
|
}
|
|
}
|
|
|
|
// the client should authenticate with the second key
|
|
func TestAuthMethodRSAandDSA(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["dsa"], testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("client could not authenticate with rsa key: %v", err)
|
|
}
|
|
}
|
|
|
|
type invalidAlgSigner struct {
|
|
Signer
|
|
}
|
|
|
|
func (s *invalidAlgSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
|
|
sig, err := s.Signer.Sign(rand, data)
|
|
if sig != nil {
|
|
sig.Format = "invalid"
|
|
}
|
|
return sig, err
|
|
}
|
|
|
|
func TestMethodInvalidAlgorithm(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(&invalidAlgSigner{testSigners["rsa"]}),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
err, serverErrors := tryAuthBothSides(t, config, nil)
|
|
if err == nil {
|
|
t.Fatalf("login succeeded")
|
|
}
|
|
|
|
found := false
|
|
want := "algorithm \"invalid\""
|
|
|
|
var errStrings []string
|
|
for _, err := range serverErrors {
|
|
found = found || (err != nil && strings.Contains(err.Error(), want))
|
|
errStrings = append(errStrings, err.Error())
|
|
}
|
|
if !found {
|
|
t.Errorf("server got error %q, want substring %q", errStrings, want)
|
|
}
|
|
}
|
|
|
|
func TestClientHMAC(t *testing.T) {
|
|
supportedAlgos := SupportedAlgorithms()
|
|
insecureAlgos := InsecureAlgorithms()
|
|
supportedMACs := append(supportedAlgos.MACs, insecureAlgos.MACs...)
|
|
for _, mac := range supportedMACs {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
Config: Config{
|
|
MACs: []string{mac},
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("client could not authenticate with mac algo %s: %v", mac, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// issue 4285.
|
|
func TestClientUnsupportedCipher(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(),
|
|
},
|
|
Config: Config{
|
|
Ciphers: []string{"unsupported-cipher"}, // not currently supported
|
|
},
|
|
}
|
|
if err := tryAuth(t, config); err == nil {
|
|
t.Errorf("expected no ciphers in common")
|
|
}
|
|
}
|
|
|
|
func TestClientUnsupportedKex(t *testing.T) {
|
|
if os.Getenv("GO_BUILDER_NAME") != "" {
|
|
t.Skip("skipping known-flaky test on the Go build dashboard; see golang.org/issue/15198")
|
|
}
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(),
|
|
},
|
|
Config: Config{
|
|
KeyExchanges: []string{"non-existent-kex"},
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, config); err == nil || !strings.Contains(err.Error(), "common algorithm") {
|
|
t.Errorf("got %v, expected 'common algorithm'", err)
|
|
}
|
|
}
|
|
|
|
func TestClientLoginCert(t *testing.T) {
|
|
cert := &Certificate{
|
|
Key: testPublicKeys["rsa"],
|
|
ValidBefore: CertTimeInfinity,
|
|
CertType: UserCert,
|
|
}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
|
|
if err != nil {
|
|
t.Fatalf("NewCertSigner: %v", err)
|
|
}
|
|
|
|
clientConfig := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
clientConfig.Auth = append(clientConfig.Auth, PublicKeys(certSigner))
|
|
|
|
// should succeed
|
|
if err := tryAuth(t, clientConfig); err != nil {
|
|
t.Errorf("cert login failed: %v", err)
|
|
}
|
|
|
|
// corrupted signature
|
|
cert.Signature.Blob[0]++
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("cert login passed with corrupted sig")
|
|
}
|
|
|
|
// revoked
|
|
cert.Serial = 666
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("revoked cert login succeeded")
|
|
}
|
|
cert.Serial = 1
|
|
|
|
// sign with wrong key
|
|
cert.SignCert(rand.Reader, testSigners["dsa"])
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("cert login passed with non-authoritative key")
|
|
}
|
|
|
|
// host cert
|
|
cert.CertType = HostCert
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("cert login passed with wrong type")
|
|
}
|
|
cert.CertType = UserCert
|
|
|
|
// principal specified
|
|
cert.ValidPrincipals = []string{"user"}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err != nil {
|
|
t.Errorf("cert login failed: %v", err)
|
|
}
|
|
|
|
// wrong principal specified
|
|
cert.ValidPrincipals = []string{"fred"}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("cert login passed with wrong principal")
|
|
}
|
|
cert.ValidPrincipals = nil
|
|
|
|
// added critical option
|
|
cert.CriticalOptions = map[string]string{"root-access": "yes"}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("cert login passed with unrecognized critical option")
|
|
}
|
|
|
|
// allowed source address
|
|
cert.CriticalOptions = map[string]string{"source-address": "127.0.0.42/24,::42/120"}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err != nil {
|
|
t.Errorf("cert login with source-address failed: %v", err)
|
|
}
|
|
|
|
// disallowed source address
|
|
cert.CriticalOptions = map[string]string{"source-address": "127.0.0.42,::42"}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Errorf("cert login with source-address succeeded")
|
|
}
|
|
}
|
|
|
|
func testPermissionsPassing(withPermissions bool, t *testing.T) {
|
|
serverConfig := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if conn.User() == "nopermissions" {
|
|
return nil, nil
|
|
}
|
|
return &Permissions{}, nil
|
|
},
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConfig := &ClientConfig{
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if withPermissions {
|
|
clientConfig.User = "permissions"
|
|
} else {
|
|
clientConfig.User = "nopermissions"
|
|
}
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
go NewClientConn(c2, "", clientConfig)
|
|
serverConn, err := newServer(c1, serverConfig)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p := serverConn.Permissions; (p != nil) != withPermissions {
|
|
t.Fatalf("withPermissions is %t, but Permissions object is %#v", withPermissions, p)
|
|
}
|
|
}
|
|
|
|
func TestPermissionsPassing(t *testing.T) {
|
|
testPermissionsPassing(true, t)
|
|
}
|
|
|
|
func TestNoPermissionsPassing(t *testing.T) {
|
|
testPermissionsPassing(false, t)
|
|
}
|
|
|
|
func TestRetryableAuth(t *testing.T) {
|
|
n := 0
|
|
passwords := []string{"WRONG1", "WRONG2"}
|
|
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
RetryableAuthMethod(PasswordCallback(func() (string, error) {
|
|
p := passwords[n]
|
|
n++
|
|
return p, nil
|
|
}), 2),
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
if err := tryAuth(t, config); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
if n != 2 {
|
|
t.Fatalf("Did not try all passwords")
|
|
}
|
|
}
|
|
|
|
func ExampleRetryableAuthMethod() {
|
|
user := "testuser"
|
|
NumberOfPrompts := 3
|
|
|
|
// Normally this would be a callback that prompts the user to answer the
|
|
// provided questions
|
|
Cb := func(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
|
return []string{"answer1", "answer2"}, nil
|
|
}
|
|
|
|
config := &ClientConfig{
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
User: user,
|
|
Auth: []AuthMethod{
|
|
RetryableAuthMethod(KeyboardInteractiveChallenge(Cb), NumberOfPrompts),
|
|
},
|
|
}
|
|
|
|
host := "mysshserver"
|
|
netConn, err := net.Dial("tcp", host)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
sshConn, _, _, err := NewClientConn(netConn, host, config)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
_ = sshConn
|
|
}
|
|
|
|
// Test if username is received on server side when NoClientAuth is used
|
|
func TestClientAuthNone(t *testing.T) {
|
|
user := "testuser"
|
|
serverConfig := &ServerConfig{
|
|
NoClientAuth: true,
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConfig := &ClientConfig{
|
|
User: user,
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
go NewClientConn(c2, "", clientConfig)
|
|
serverConn, err := newServer(c1, serverConfig)
|
|
if err != nil {
|
|
t.Fatalf("newServer: %v", err)
|
|
}
|
|
if serverConn.User() != user {
|
|
t.Fatalf("server: got %q, want %q", serverConn.User(), user)
|
|
}
|
|
}
|
|
|
|
// Test if authentication attempts are limited on server when MaxAuthTries is set
|
|
func TestClientAuthMaxAuthTries(t *testing.T) {
|
|
user := "testuser"
|
|
|
|
serverConfig := &ServerConfig{
|
|
MaxAuthTries: 2,
|
|
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
|
|
if conn.User() == "testuser" && string(pass) == "right" {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("password auth failed")
|
|
},
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
expectedErr := fmt.Errorf("ssh: handshake failed: %v", &disconnectMsg{
|
|
Reason: 2,
|
|
Message: "too many authentication failures",
|
|
})
|
|
|
|
for tries := 2; tries < 4; tries++ {
|
|
n := tries
|
|
clientConfig := &ClientConfig{
|
|
User: user,
|
|
Auth: []AuthMethod{
|
|
RetryableAuthMethod(PasswordCallback(func() (string, error) {
|
|
n--
|
|
if n == 0 {
|
|
return "right", nil
|
|
}
|
|
return "wrong", nil
|
|
}), tries),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
go func() {
|
|
_, err := newServer(c1, serverConfig)
|
|
errCh <- err
|
|
}()
|
|
_, _, _, cliErr := NewClientConn(c2, "", clientConfig)
|
|
srvErr := <-errCh
|
|
|
|
if tries > serverConfig.MaxAuthTries {
|
|
if cliErr == nil {
|
|
t.Fatalf("client: got no error, want %s", expectedErr)
|
|
} else if cliErr.Error() != expectedErr.Error() {
|
|
t.Fatalf("client: got %s, want %s", err, expectedErr)
|
|
}
|
|
var authErr *ServerAuthError
|
|
if !errors.As(srvErr, &authErr) {
|
|
t.Errorf("expected ServerAuthError, got: %v", srvErr)
|
|
}
|
|
} else {
|
|
if cliErr != nil {
|
|
t.Fatalf("client: got %s, want no error", cliErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test if authentication attempts are correctly limited on server
|
|
// when more public keys are provided then MaxAuthTries
|
|
func TestClientAuthMaxAuthTriesPublicKey(t *testing.T) {
|
|
signers := []Signer{}
|
|
for i := 0; i < 6; i++ {
|
|
signers = append(signers, testSigners["dsa"])
|
|
}
|
|
|
|
validConfig := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(append([]Signer{testSigners["rsa"]}, signers...)...),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, validConfig); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
|
|
expectedErr := fmt.Errorf("ssh: handshake failed: %v", &disconnectMsg{
|
|
Reason: 2,
|
|
Message: "too many authentication failures",
|
|
})
|
|
invalidConfig := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(append(signers, testSigners["rsa"])...),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
if err := tryAuth(t, invalidConfig); err == nil {
|
|
t.Fatalf("client: got no error, want %s", expectedErr)
|
|
} else if err.Error() != expectedErr.Error() {
|
|
// On Windows we can see a WSAECONNABORTED error
|
|
// if the client writes another authentication request
|
|
// before the client goroutine reads the disconnection
|
|
// message. See issue 50805.
|
|
if runtime.GOOS == "windows" && strings.Contains(err.Error(), "wsarecv: An established connection was aborted") {
|
|
// OK.
|
|
} else {
|
|
t.Fatalf("client: got %s, want %s", err, expectedErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test whether authentication errors are being properly logged if all
|
|
// authentication methods have been exhausted
|
|
func TestClientAuthErrorList(t *testing.T) {
|
|
publicKeyErr := errors.New("This is an error from PublicKeyCallback")
|
|
|
|
clientConfig := &ClientConfig{
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
serverConfig := &ServerConfig{
|
|
PublicKeyCallback: func(_ ConnMetadata, _ PublicKey) (*Permissions, error) {
|
|
return nil, publicKeyErr
|
|
},
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
go NewClientConn(c2, "", clientConfig)
|
|
_, err = newServer(c1, serverConfig)
|
|
if err == nil {
|
|
t.Fatal("newServer: got nil, expected errors")
|
|
}
|
|
|
|
authErrs, ok := err.(*ServerAuthError)
|
|
if !ok {
|
|
t.Fatalf("errors: got %T, want *ssh.ServerAuthError", err)
|
|
}
|
|
for i, e := range authErrs.Errors {
|
|
switch i {
|
|
case 0:
|
|
if e != ErrNoAuth {
|
|
t.Fatalf("errors: got error %v, want ErrNoAuth", e)
|
|
}
|
|
case 1:
|
|
if e != publicKeyErr {
|
|
t.Fatalf("errors: got %v, want %v", e, publicKeyErr)
|
|
}
|
|
default:
|
|
t.Fatalf("errors: got %v, expected 2 errors", authErrs.Errors)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
|
|
type testcase struct {
|
|
config *ClientConfig
|
|
gssConfig *GSSAPIWithMICConfig
|
|
clientWantErr string
|
|
serverWantErr string
|
|
}
|
|
testcases := []*testcase{
|
|
{
|
|
config: &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
GSSAPIWithMICAuthMethod(
|
|
&FakeClient{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "client-valid-token-1",
|
|
},
|
|
{
|
|
expectedToken: "server-valid-token-1",
|
|
},
|
|
},
|
|
mic: []byte("valid-mic"),
|
|
maxRound: 2,
|
|
}, "testtarget",
|
|
),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
},
|
|
gssConfig: &GSSAPIWithMICConfig{
|
|
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
|
|
if srcName != conn.User()+"@DOMAIN" {
|
|
return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
|
|
}
|
|
return nil, nil
|
|
},
|
|
Server: &FakeServer{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "server-valid-token-1",
|
|
expectedToken: "client-valid-token-1",
|
|
},
|
|
},
|
|
maxRound: 1,
|
|
expectedMIC: []byte("valid-mic"),
|
|
srcName: "testuser@DOMAIN",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
GSSAPIWithMICAuthMethod(
|
|
&FakeClient{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "client-valid-token-1",
|
|
},
|
|
{
|
|
expectedToken: "server-valid-token-1",
|
|
},
|
|
},
|
|
mic: []byte("valid-mic"),
|
|
maxRound: 2,
|
|
}, "testtarget",
|
|
),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
},
|
|
gssConfig: &GSSAPIWithMICConfig{
|
|
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
|
|
return nil, fmt.Errorf("user is not allowed to login")
|
|
},
|
|
Server: &FakeServer{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "server-valid-token-1",
|
|
expectedToken: "client-valid-token-1",
|
|
},
|
|
},
|
|
maxRound: 1,
|
|
expectedMIC: []byte("valid-mic"),
|
|
srcName: "testuser@DOMAIN",
|
|
},
|
|
},
|
|
serverWantErr: "user is not allowed to login",
|
|
clientWantErr: "ssh: handshake failed: ssh: unable to authenticate",
|
|
},
|
|
{
|
|
config: &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
GSSAPIWithMICAuthMethod(
|
|
&FakeClient{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "client-valid-token-1",
|
|
},
|
|
{
|
|
expectedToken: "server-valid-token-1",
|
|
},
|
|
},
|
|
mic: []byte("valid-mic"),
|
|
maxRound: 2,
|
|
}, "testtarget",
|
|
),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
},
|
|
gssConfig: &GSSAPIWithMICConfig{
|
|
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
|
|
if srcName != conn.User() {
|
|
return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
|
|
}
|
|
return nil, nil
|
|
},
|
|
Server: &FakeServer{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "server-invalid-token-1",
|
|
expectedToken: "client-valid-token-1",
|
|
},
|
|
},
|
|
maxRound: 1,
|
|
expectedMIC: []byte("valid-mic"),
|
|
srcName: "testuser@DOMAIN",
|
|
},
|
|
},
|
|
clientWantErr: "ssh: handshake failed: got \"server-invalid-token-1\", want token \"server-valid-token-1\"",
|
|
},
|
|
{
|
|
config: &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
GSSAPIWithMICAuthMethod(
|
|
&FakeClient{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "client-valid-token-1",
|
|
},
|
|
{
|
|
expectedToken: "server-valid-token-1",
|
|
},
|
|
},
|
|
mic: []byte("invalid-mic"),
|
|
maxRound: 2,
|
|
}, "testtarget",
|
|
),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
},
|
|
gssConfig: &GSSAPIWithMICConfig{
|
|
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
|
|
if srcName != conn.User() {
|
|
return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
|
|
}
|
|
return nil, nil
|
|
},
|
|
Server: &FakeServer{
|
|
exchanges: []*exchange{
|
|
{
|
|
outToken: "server-valid-token-1",
|
|
expectedToken: "client-valid-token-1",
|
|
},
|
|
},
|
|
maxRound: 1,
|
|
expectedMIC: []byte("valid-mic"),
|
|
srcName: "testuser@DOMAIN",
|
|
},
|
|
},
|
|
serverWantErr: "got MICToken \"invalid-mic\", want \"valid-mic\"",
|
|
clientWantErr: "ssh: handshake failed: ssh: unable to authenticate",
|
|
},
|
|
}
|
|
|
|
for i, c := range testcases {
|
|
clientErr, serverErrs := tryAuthBothSides(t, c.config, c.gssConfig)
|
|
if (c.clientWantErr == "") != (clientErr == nil) {
|
|
t.Fatalf("client got %v, want %s, case %d", clientErr, c.clientWantErr, i)
|
|
}
|
|
if (c.serverWantErr == "") != (len(serverErrs) == 2 && serverErrs[1] == nil || len(serverErrs) == 1) {
|
|
t.Fatalf("server got err %v, want %s", serverErrs, c.serverWantErr)
|
|
}
|
|
if c.clientWantErr != "" {
|
|
if clientErr != nil && !strings.Contains(clientErr.Error(), c.clientWantErr) {
|
|
t.Fatalf("client got %v, want %s, case %d", clientErr, c.clientWantErr, i)
|
|
}
|
|
}
|
|
found := false
|
|
var errStrings []string
|
|
if c.serverWantErr != "" {
|
|
for _, err := range serverErrs {
|
|
found = found || (err != nil && strings.Contains(err.Error(), c.serverWantErr))
|
|
errStrings = append(errStrings, err.Error())
|
|
}
|
|
if !found {
|
|
t.Errorf("server got error %q, want substring %q, case %d", errStrings, c.serverWantErr, i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type maliciousGSSAPIWithMICAuthMethod struct{}
|
|
|
|
func (maliciousGSSAPIWithMICAuthMethod) method() string {
|
|
// Reuse an advertised auth method name so the client attempts this
|
|
// AuthMethod even when gssapi-with-mic is not advertised.
|
|
return "password"
|
|
}
|
|
|
|
func (maliciousGSSAPIWithMICAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
|
|
req := &userAuthRequestMsg{
|
|
User: user,
|
|
Service: serviceSSH,
|
|
Method: "gssapi-with-mic",
|
|
}
|
|
req.Payload = appendU32(req.Payload, 1)
|
|
req.Payload = appendString(req.Payload, string(krb5OID))
|
|
if err := c.writePacket(Marshal(req)); err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
switch packet[0] {
|
|
case msgUserAuthFailure:
|
|
var msg userAuthFailureMsg
|
|
if err := Unmarshal(packet, &msg); err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
if msg.PartialSuccess {
|
|
return authPartialSuccess, msg.Methods, nil
|
|
}
|
|
return authFailure, msg.Methods, nil
|
|
default:
|
|
return authFailure, nil, unexpectedMessageError(msgUserAuthFailure, packet[0])
|
|
}
|
|
}
|
|
|
|
func TestGSSAPIWithMICPartialConfigRejectedAtRuntime(t *testing.T) {
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
maliciousGSSAPIWithMICAuthMethod{},
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
clientErr, serverErrs := tryAuthBothSides(t, config, &GSSAPIWithMICConfig{
|
|
Server: &FakeServer{},
|
|
})
|
|
|
|
if clientErr == nil || !strings.Contains(clientErr.Error(), "ssh: unable to authenticate") {
|
|
t.Fatalf("client got %v, want authentication failure", clientErr)
|
|
}
|
|
|
|
found := false
|
|
for _, err := range serverErrs {
|
|
if err != nil && strings.Contains(err.Error(), "ssh: gssapi-with-mic auth not configured") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("server auth errors = %v, want gssapi-with-mic auth not configured", serverErrs)
|
|
}
|
|
}
|
|
|
|
func TestCompatibleAlgoAndSignatures(t *testing.T) {
|
|
type testcase struct {
|
|
algo string
|
|
sigFormat string
|
|
compatible bool
|
|
}
|
|
testcases := []*testcase{
|
|
{
|
|
KeyAlgoRSA,
|
|
KeyAlgoRSA,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSA,
|
|
KeyAlgoRSASHA256,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSA,
|
|
KeyAlgoRSASHA512,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSASHA256,
|
|
KeyAlgoRSA,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSASHA512,
|
|
KeyAlgoRSA,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSASHA512,
|
|
KeyAlgoRSASHA256,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSASHA256,
|
|
KeyAlgoRSASHA512,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoRSASHA512,
|
|
KeyAlgoRSASHA512,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSAv01,
|
|
KeyAlgoRSA,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSAv01,
|
|
KeyAlgoRSASHA256,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSAv01,
|
|
KeyAlgoRSASHA512,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSASHA256v01,
|
|
KeyAlgoRSASHA512,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSASHA512v01,
|
|
KeyAlgoRSASHA512,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSASHA512v01,
|
|
KeyAlgoRSASHA256,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSASHA256v01,
|
|
CertAlgoRSAv01,
|
|
true,
|
|
},
|
|
{
|
|
CertAlgoRSAv01,
|
|
CertAlgoRSASHA512v01,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoECDSA256,
|
|
KeyAlgoRSA,
|
|
false,
|
|
},
|
|
{
|
|
KeyAlgoECDSA256,
|
|
KeyAlgoECDSA521,
|
|
false,
|
|
},
|
|
{
|
|
KeyAlgoECDSA256,
|
|
KeyAlgoECDSA256,
|
|
true,
|
|
},
|
|
{
|
|
KeyAlgoECDSA256,
|
|
KeyAlgoED25519,
|
|
false,
|
|
},
|
|
{
|
|
KeyAlgoED25519,
|
|
KeyAlgoED25519,
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, c := range testcases {
|
|
if isAlgoCompatible(c.algo, c.sigFormat) != c.compatible {
|
|
t.Errorf("algorithm %q, signature format %q, expected compatible to be %t", c.algo, c.sigFormat, c.compatible)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPickSignatureAlgorithm(t *testing.T) {
|
|
type testcase struct {
|
|
name string
|
|
extensions map[string][]byte
|
|
}
|
|
cases := []testcase{
|
|
{
|
|
name: "server with empty server-sig-algs",
|
|
extensions: map[string][]byte{
|
|
"server-sig-algs": []byte(``),
|
|
},
|
|
},
|
|
{
|
|
name: "server with no server-sig-algs",
|
|
extensions: nil,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
signer, ok := testSigners["rsa"].(MultiAlgorithmSigner)
|
|
if !ok {
|
|
t.Fatalf("rsa test signer does not implement the MultiAlgorithmSigner interface")
|
|
}
|
|
// The signer supports the public key algorithm which is then returned.
|
|
_, algo, err := pickSignatureAlgorithm(signer, c.extensions)
|
|
if err != nil {
|
|
t.Fatalf("got %v, want no error", err)
|
|
}
|
|
if algo != signer.PublicKey().Type() {
|
|
t.Fatalf("got algo %q, want %q", algo, signer.PublicKey().Type())
|
|
}
|
|
// Test a signer that uses a certificate algorithm as the public key
|
|
// type.
|
|
cert := &Certificate{
|
|
CertType: UserCert,
|
|
Key: signer.PublicKey(),
|
|
}
|
|
cert.SignCert(rand.Reader, signer)
|
|
|
|
certSigner, err := NewCertSigner(cert, signer)
|
|
if err != nil {
|
|
t.Fatalf("error generating cert signer: %v", err)
|
|
}
|
|
// The signer supports the public key algorithm and the
|
|
// public key format is a certificate type so the certificate
|
|
// algorithm matching the key format must be returned
|
|
_, algo, err = pickSignatureAlgorithm(certSigner, c.extensions)
|
|
if err != nil {
|
|
t.Fatalf("got %v, want no error", err)
|
|
}
|
|
if algo != certSigner.PublicKey().Type() {
|
|
t.Fatalf("got algo %q, want %q", algo, certSigner.PublicKey().Type())
|
|
}
|
|
signer, err = NewSignerWithAlgorithms(signer.(AlgorithmSigner), []string{KeyAlgoRSASHA512, KeyAlgoRSASHA256})
|
|
if err != nil {
|
|
t.Fatalf("unable to create signer with algorithms: %v", err)
|
|
}
|
|
// The signer does not support the public key algorithm so an error
|
|
// is returned.
|
|
_, _, err = pickSignatureAlgorithm(signer, c.extensions)
|
|
if err == nil {
|
|
t.Fatal("got no error, no common public key signature algorithm error expected")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPickSignatureAlgorithmRespectsSignerPreference(t *testing.T) {
|
|
algoSigner, ok := testSigners["rsa"].(AlgorithmSigner)
|
|
if !ok {
|
|
t.Fatalf("rsa test signer does not implement the AlgorithmSigner interface")
|
|
}
|
|
|
|
serverExtensions := map[string][]byte{
|
|
"server-sig-algs": []byte(KeyAlgoRSASHA256 + "," + KeyAlgoRSASHA512),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
signerPrefs []string
|
|
expectedAlgo string
|
|
}{
|
|
{
|
|
name: "Signer prefers SHA512 then SHA256",
|
|
signerPrefs: []string{KeyAlgoRSASHA512, KeyAlgoRSASHA256},
|
|
expectedAlgo: KeyAlgoRSASHA512,
|
|
},
|
|
{
|
|
name: "Signer prefers SHA256 then SHA512",
|
|
signerPrefs: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512},
|
|
expectedAlgo: KeyAlgoRSASHA256,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
orderedSigner, err := NewSignerWithAlgorithms(algoSigner, tc.signerPrefs)
|
|
if err != nil {
|
|
t.Fatalf("failed to create ordered signer: %v", err)
|
|
}
|
|
|
|
_, selectedAlgo, err := pickSignatureAlgorithm(orderedSigner, serverExtensions)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if selectedAlgo != tc.expectedAlgo {
|
|
t.Errorf("Algorithm mismatch; got %q want %q", selectedAlgo, tc.expectedAlgo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// configurablePublicKeyCallback is a public key callback that allows to
|
|
// configure the signature algorithm and format. This way we can emulate the
|
|
// behavior of buggy clients.
|
|
type configurablePublicKeyCallback struct {
|
|
signer AlgorithmSigner
|
|
signatureAlgo string
|
|
signatureFormat string
|
|
}
|
|
|
|
func (cb configurablePublicKeyCallback) method() string {
|
|
return "publickey"
|
|
}
|
|
|
|
func (cb configurablePublicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error) {
|
|
pub := cb.signer.PublicKey()
|
|
|
|
ok, err := validateKey(pub, cb.signatureAlgo, user, c)
|
|
if err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
if !ok {
|
|
return authFailure, nil, fmt.Errorf("invalid public key")
|
|
}
|
|
|
|
pubKey := pub.Marshal()
|
|
data := buildDataSignedForAuth(session, userAuthRequestMsg{
|
|
User: user,
|
|
Service: serviceSSH,
|
|
Method: cb.method(),
|
|
}, cb.signatureAlgo, pubKey)
|
|
sign, err := cb.signer.SignWithAlgorithm(rand, data, underlyingAlgo(cb.signatureFormat))
|
|
if err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
|
|
s := Marshal(sign)
|
|
sig := make([]byte, stringLength(len(s)))
|
|
marshalString(sig, s)
|
|
msg := publickeyAuthMsg{
|
|
User: user,
|
|
Service: serviceSSH,
|
|
Method: cb.method(),
|
|
HasSig: true,
|
|
Algoname: cb.signatureAlgo,
|
|
PubKey: pubKey,
|
|
Sig: sig,
|
|
}
|
|
p := Marshal(&msg)
|
|
if err := c.writePacket(p); err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
var success authResult
|
|
success, methods, err := handleAuthResponse(c)
|
|
if err != nil {
|
|
return authFailure, nil, err
|
|
}
|
|
if success == authSuccess || !slices.Contains(methods, cb.method()) {
|
|
return success, methods, err
|
|
}
|
|
|
|
return authFailure, methods, nil
|
|
}
|
|
|
|
func TestPublicKeyAndAlgoCompatibility(t *testing.T) {
|
|
cert := &Certificate{
|
|
Key: testPublicKeys["rsa"],
|
|
ValidBefore: CertTimeInfinity,
|
|
CertType: UserCert,
|
|
}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
|
|
if err != nil {
|
|
t.Fatalf("NewCertSigner: %v", err)
|
|
}
|
|
|
|
clientConfig := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
Auth: []AuthMethod{
|
|
configurablePublicKeyCallback{
|
|
signer: certSigner.(AlgorithmSigner),
|
|
signatureAlgo: KeyAlgoRSASHA256,
|
|
signatureFormat: KeyAlgoRSASHA256,
|
|
},
|
|
},
|
|
}
|
|
if err := tryAuth(t, clientConfig); err == nil {
|
|
t.Error("cert login passed with incompatible public key type and algorithm")
|
|
}
|
|
}
|
|
|
|
func TestClientAuthGPGAgentCompat(t *testing.T) {
|
|
clientConfig := &ClientConfig{
|
|
User: "testuser",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
Auth: []AuthMethod{
|
|
// algorithm rsa-sha2-512 and signature format ssh-rsa.
|
|
configurablePublicKeyCallback{
|
|
signer: testSigners["rsa"].(AlgorithmSigner),
|
|
signatureAlgo: KeyAlgoRSASHA512,
|
|
signatureFormat: KeyAlgoRSA,
|
|
},
|
|
},
|
|
}
|
|
if err := tryAuth(t, clientConfig); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestCertAuthOpenSSHCompat(t *testing.T) {
|
|
cert := &Certificate{
|
|
Key: testPublicKeys["rsa"],
|
|
ValidBefore: CertTimeInfinity,
|
|
CertType: UserCert,
|
|
}
|
|
cert.SignCert(rand.Reader, testSigners["ecdsa"])
|
|
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
|
|
if err != nil {
|
|
t.Fatalf("NewCertSigner: %v", err)
|
|
}
|
|
|
|
clientConfig := &ClientConfig{
|
|
User: "user",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
Auth: []AuthMethod{
|
|
// algorithm ssh-rsa-cert-v01@openssh.com and signature format
|
|
// rsa-sha2-256.
|
|
configurablePublicKeyCallback{
|
|
signer: certSigner.(AlgorithmSigner),
|
|
signatureAlgo: CertAlgoRSAv01,
|
|
signatureFormat: KeyAlgoRSASHA256,
|
|
},
|
|
},
|
|
}
|
|
if err := tryAuth(t, clientConfig); err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestKeyboardInteractiveAuthEarlyFail(t *testing.T) {
|
|
const maxAuthTries = 2
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
// Start testserver
|
|
serverConfig := &ServerConfig{
|
|
MaxAuthTries: maxAuthTries,
|
|
KeyboardInteractiveCallback: func(c ConnMetadata,
|
|
client KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
// Fail keyboard-interactive authentication early before
|
|
// any prompt is sent to client.
|
|
return nil, errors.New("keyboard-interactive auth failed")
|
|
},
|
|
PasswordCallback: func(c ConnMetadata,
|
|
pass []byte) (*Permissions, error) {
|
|
if string(pass) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("password auth failed")
|
|
},
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
serverDone := make(chan struct{})
|
|
go func() {
|
|
defer func() { serverDone <- struct{}{} }()
|
|
conn, chans, reqs, err := NewServerConn(c2, serverConfig)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
|
|
discarderDone := make(chan struct{})
|
|
go func() {
|
|
defer func() { discarderDone <- struct{}{} }()
|
|
DiscardRequests(reqs)
|
|
}()
|
|
for newChannel := range chans {
|
|
newChannel.Reject(Prohibited,
|
|
"testserver not accepting requests")
|
|
}
|
|
|
|
<-discarderDone
|
|
}()
|
|
|
|
// Connect to testserver, expect KeyboardInteractive() to be not called,
|
|
// PasswordCallback() to be called and connection to succeed.
|
|
passwordCallbackCalled := false
|
|
clientConfig := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []AuthMethod{
|
|
RetryableAuthMethod(KeyboardInteractive(func(name,
|
|
instruction string, questions []string,
|
|
echos []bool) ([]string, error) {
|
|
t.Errorf("unexpected call to KeyboardInteractive()")
|
|
return []string{clientPassword}, nil
|
|
}), maxAuthTries),
|
|
RetryableAuthMethod(PasswordCallback(func() (secret string,
|
|
err error) {
|
|
t.Logf("PasswordCallback()")
|
|
passwordCallbackCalled = true
|
|
return clientPassword, nil
|
|
}), maxAuthTries),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
conn, _, _, err := NewClientConn(c1, "", clientConfig)
|
|
if err != nil {
|
|
t.Errorf("unexpected NewClientConn() error: %v", err)
|
|
}
|
|
if conn != nil {
|
|
conn.Close()
|
|
}
|
|
|
|
// Wait for server to finish.
|
|
<-serverDone
|
|
|
|
if !passwordCallbackCalled {
|
|
t.Errorf("expected PasswordCallback() to be called")
|
|
}
|
|
}
|