Add ClientAuthCallback, a hook invoked before each authentication attempt that allows the client to dynamically select an auth method based on server capabilities, partial successes, or previous failures. clientAuthenticate tracks partial successes in a slice and passes them to AuthCallback via ClientAuthContext.PartialSuccessMethods, along with the list of failed methods and currently allowed methods. When AuthCallback returns a non-nil AuthMethod, it is used for the next attempt, bypassing the static findNext selection. When it returns (nil, nil), the static config.Auth selection proceeds as before. If AuthCallback returns (nil, error) the handshake aborts immediately with that error. To bound resource use when AuthCallback keeps supplying methods, the client caps the total number of authentication attempts (failures and partial successes combined) at 64; exceeding the cap aborts the handshake with an error. Fixes golang/go#76146 Change-Id: I0d02bea7b9dd724e95e5d9d49d85306666c0df7a Reviewed-on: https://go-review.googlesource.com/c/crypto/+/717140 Reviewed-by: Cherry Mui <cherryyz@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Filippo Valsorda <filippo@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1080 lines
30 KiB
Go
1080 lines
30 KiB
Go
// Copyright 2023 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/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestClientAuthRestrictedPublicKeyAlgos(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
key Signer
|
|
wantError bool
|
|
}{
|
|
{"rsa", testSigners["rsa"], false},
|
|
{"dsa", testSigners["dsa"], true},
|
|
{"ed25519", testSigners["ed25519"], true},
|
|
} {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
serverConf := &ServerConfig{
|
|
PublicKeyAuthAlgorithms: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512},
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["ecdsap256"])
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
NewServerConn(c1, serverConf)
|
|
}()
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(tt.key),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err != nil {
|
|
if !tt.wantError {
|
|
t.Errorf("%s: got unexpected error %q", tt.name, err.Error())
|
|
}
|
|
} else if tt.wantError {
|
|
t.Errorf("%s: succeeded, but want error", tt.name)
|
|
}
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func TestMaxAuthTriesNoneMethod(t *testing.T) {
|
|
username := "testuser"
|
|
serverConfig := &ServerConfig{
|
|
MaxAuthTries: 2,
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if conn.User() == username && string(password) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
var serverAuthErrors []error
|
|
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
serverConfig.AuthLogCallback = func(conn ConnMetadata, method string, err error) {
|
|
serverAuthErrors = append(serverAuthErrors, err)
|
|
}
|
|
go newServer(c1, serverConfig)
|
|
|
|
clientConfig := ClientConfig{
|
|
User: username,
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
clientConfig.SetDefaults()
|
|
// Our client will send 'none' auth only once, so we need to send the
|
|
// requests manually.
|
|
c := &connection{
|
|
sshConn: sshConn{
|
|
conn: c2,
|
|
user: username,
|
|
clientVersion: []byte(packageVersion),
|
|
},
|
|
}
|
|
c.serverVersion, err = exchangeVersions(c.sshConn.conn, c.clientVersion)
|
|
if err != nil {
|
|
t.Fatalf("unable to exchange version: %v", err)
|
|
}
|
|
c.transport = newClientTransport(
|
|
newTransport(c.sshConn.conn, clientConfig.Rand, true /* is client */),
|
|
c.clientVersion, c.serverVersion, &clientConfig, "", c.sshConn.RemoteAddr())
|
|
if err := c.transport.waitSession(); err != nil {
|
|
t.Fatalf("unable to wait session: %v", err)
|
|
}
|
|
c.sessionID = c.transport.getSessionID()
|
|
if err := c.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil {
|
|
t.Fatalf("unable to send ssh-userauth message: %v", err)
|
|
}
|
|
packet, err := c.transport.readPacket()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(packet) > 0 && packet[0] == msgExtInfo {
|
|
packet, err = c.transport.readPacket()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var serviceAccept serviceAcceptMsg
|
|
if err := Unmarshal(packet, &serviceAccept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i <= serverConfig.MaxAuthTries; i++ {
|
|
auth := new(noneAuth)
|
|
_, _, err := auth.auth(c.sessionID, clientConfig.User, c.transport, clientConfig.Rand, nil)
|
|
if i < serverConfig.MaxAuthTries {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
continue
|
|
}
|
|
if err == nil {
|
|
t.Fatal("client: got no error")
|
|
} else if !strings.Contains(err.Error(), "too many authentication failures") {
|
|
t.Fatalf("client: got unexpected error: %v", err)
|
|
}
|
|
}
|
|
if len(serverAuthErrors) != 3 {
|
|
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
|
|
}
|
|
for _, err := range serverAuthErrors {
|
|
if !errors.Is(err, ErrNoAuth) {
|
|
t.Errorf("go error: %v; want: %v", err, ErrNoAuth)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMaxAuthTriesFirstNoneAuthErrorIgnored(t *testing.T) {
|
|
username := "testuser"
|
|
serverConfig := &ServerConfig{
|
|
MaxAuthTries: 1,
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if conn.User() == username && string(password) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
clientConfig := &ClientConfig{
|
|
User: username,
|
|
Auth: []AuthMethod{
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
serverAuthErrors, err := doClientServerAuth(t, serverConfig, clientConfig)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
if len(serverAuthErrors) != 2 {
|
|
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
|
|
}
|
|
if !errors.Is(serverAuthErrors[0], ErrNoAuth) {
|
|
t.Errorf("go error: %v; want: %v", serverAuthErrors[0], ErrNoAuth)
|
|
}
|
|
if serverAuthErrors[1] != nil {
|
|
t.Errorf("unexpected error: %v", serverAuthErrors[1])
|
|
}
|
|
}
|
|
|
|
func TestNewServerConnValidationErrors(t *testing.T) {
|
|
serverConf := &ServerConfig{
|
|
PublicKeyAuthAlgorithms: []string{CertAlgoRSAv01},
|
|
}
|
|
c := &markerConn{}
|
|
_, _, _, err := NewServerConn(c, serverConf)
|
|
if err == nil {
|
|
t.Fatal("NewServerConn with invalid public key auth algorithms succeeded")
|
|
}
|
|
if !c.isClosed() {
|
|
t.Fatal("NewServerConn with invalid public key auth algorithms left connection open")
|
|
}
|
|
if c.isUsed() {
|
|
t.Fatal("NewServerConn with invalid public key auth algorithms used connection")
|
|
}
|
|
|
|
serverConf = &ServerConfig{
|
|
Config: Config{
|
|
KeyExchanges: []string{KeyExchangeDHGEXSHA256},
|
|
},
|
|
}
|
|
c = &markerConn{}
|
|
_, _, _, err = NewServerConn(c, serverConf)
|
|
if err == nil {
|
|
t.Fatal("NewServerConn with unsupported key exchange succeeded")
|
|
}
|
|
if !c.isClosed() {
|
|
t.Fatal("NewServerConn with unsupported key exchange left connection open")
|
|
}
|
|
if c.isUsed() {
|
|
t.Fatal("NewServerConn with unsupported key exchange used connection")
|
|
}
|
|
}
|
|
|
|
func TestBannerError(t *testing.T) {
|
|
serverConfig := &ServerConfig{
|
|
BannerCallback: func(ConnMetadata) string {
|
|
return "banner from BannerCallback"
|
|
},
|
|
NoClientAuth: true,
|
|
NoClientAuthCallback: func(ConnMetadata) (*Permissions, error) {
|
|
err := &BannerError{
|
|
Err: errors.New("error from NoClientAuthCallback"),
|
|
Message: "banner from NoClientAuthCallback",
|
|
}
|
|
return nil, fmt.Errorf("wrapped: %w", err)
|
|
},
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
return &Permissions{}, nil
|
|
},
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return nil, &BannerError{
|
|
Err: errors.New("error from PublicKeyCallback"),
|
|
Message: "banner from PublicKeyCallback",
|
|
}
|
|
},
|
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
|
return nil, &BannerError{
|
|
Err: nil, // make sure that a nil inner error is allowed
|
|
Message: "banner from KeyboardInteractiveCallback",
|
|
}
|
|
},
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
var banners []string
|
|
clientConfig := &ClientConfig{
|
|
User: "test",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
|
return []string{"letmein"}, nil
|
|
}),
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
BannerCallback: func(msg string) error {
|
|
banners = append(banners, msg)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
go newServer(c1, serverConfig)
|
|
c, _, _, err := NewClientConn(c2, "", clientConfig)
|
|
if err != nil {
|
|
t.Fatalf("client connection failed: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
wantBanners := []string{
|
|
"banner from BannerCallback",
|
|
"banner from NoClientAuthCallback",
|
|
"banner from PublicKeyCallback",
|
|
"banner from KeyboardInteractiveCallback",
|
|
}
|
|
if !reflect.DeepEqual(banners, wantBanners) {
|
|
t.Errorf("got banners:\n%q\nwant banners:\n%q", banners, wantBanners)
|
|
}
|
|
}
|
|
|
|
func TestPublicKeyCallbackLastSeen(t *testing.T) {
|
|
var lastSeenKey PublicKey
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
lastSeenKey = key
|
|
fmt.Printf("seen %#v\n", key)
|
|
if _, ok := key.(*dsaPublicKey); !ok {
|
|
return nil, errors.New("nope")
|
|
}
|
|
return nil, nil
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["ecdsap256"])
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
NewServerConn(c1, serverConf)
|
|
}()
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"], testSigners["dsa"], testSigners["ed25519"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-done
|
|
|
|
expectedPublicKey := testSigners["dsa"].PublicKey().Marshal()
|
|
lastSeenMarshalled := lastSeenKey.Marshal()
|
|
if !bytes.Equal(lastSeenMarshalled, expectedPublicKey) {
|
|
t.Errorf("unexpected key: got %#v, want %#v", lastSeenKey, testSigners["dsa"].PublicKey())
|
|
}
|
|
}
|
|
|
|
func TestPreAuthConnAndBanners(t *testing.T) {
|
|
testDone := make(chan struct{})
|
|
defer close(testDone)
|
|
|
|
authConnc := make(chan ServerPreAuthConn, 1)
|
|
serverConfig := &ServerConfig{
|
|
PreAuthConnCallback: func(c ServerPreAuthConn) {
|
|
t.Logf("got ServerPreAuthConn: %v", c)
|
|
authConnc <- c // for use later in the test
|
|
for _, s := range []string{"hello1", "hello2"} {
|
|
if err := c.SendAuthBanner(s); err != nil {
|
|
t.Errorf("failed to send banner %q: %v", s, err)
|
|
}
|
|
}
|
|
// Now start a goroutine to spam SendAuthBanner in hopes
|
|
// of hitting a race.
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-testDone:
|
|
return
|
|
default:
|
|
if err := c.SendAuthBanner("attempted-race"); err != nil && err != errSendBannerPhase {
|
|
t.Errorf("unexpected error from SendAuthBanner: %v", err)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
},
|
|
NoClientAuth: true,
|
|
NoClientAuthCallback: func(ConnMetadata) (*Permissions, error) {
|
|
t.Logf("got NoClientAuthCallback")
|
|
return &Permissions{}, nil
|
|
},
|
|
}
|
|
serverConfig.AddHostKey(testSigners["rsa"])
|
|
|
|
var banners []string
|
|
clientConfig := &ClientConfig{
|
|
User: "test",
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
BannerCallback: func(msg string) error {
|
|
if msg != "attempted-race" {
|
|
banners = append(banners, msg)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
go newServer(c1, serverConfig)
|
|
c, _, _, err := NewClientConn(c2, "", clientConfig)
|
|
if err != nil {
|
|
t.Fatalf("client connection failed: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
wantBanners := []string{
|
|
"hello1",
|
|
"hello2",
|
|
}
|
|
if !reflect.DeepEqual(banners, wantBanners) {
|
|
t.Errorf("got banners:\n%q\nwant banners:\n%q", banners, wantBanners)
|
|
}
|
|
|
|
// Now that we're authenticated, verify that use of SendBanner
|
|
// is an error.
|
|
var bc ServerPreAuthConn
|
|
select {
|
|
case bc = <-authConnc:
|
|
default:
|
|
t.Fatal("expected ServerPreAuthConn")
|
|
}
|
|
if err := bc.SendAuthBanner("wrong-phase"); err == nil {
|
|
t.Error("unexpected success of SendAuthBanner after authentication")
|
|
} else if err != errSendBannerPhase {
|
|
t.Errorf("unexpected error: %v; want %v", err, errSendBannerPhase)
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPublicKeyCallback(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
extraKey := "extra"
|
|
extraDataString := "just a string"
|
|
|
|
serverConf := &ServerConfig{
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
if permissions != nil && permissions.ExtraData != nil {
|
|
if !reflect.DeepEqual(map[any]any{extraKey: extraDataString}, permissions.ExtraData) {
|
|
t.Errorf("expected extra data: %v; got: %v", extraDataString, permissions.ExtraData)
|
|
}
|
|
} else {
|
|
t.Error("expected extra data is missing")
|
|
}
|
|
if signatureAlgorithm != KeyAlgoRSASHA256 {
|
|
t.Errorf("expected signature algorithm: %q; got: %q", KeyAlgoRSASHA256, signatureAlgorithm)
|
|
}
|
|
return permissions, nil
|
|
},
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return &Permissions{ExtraData: map[any]any{extraKey: extraDataString}}, nil
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
conn, _, _, err := NewServerConn(c1, serverConf)
|
|
if err != nil {
|
|
t.Errorf("unexpected server error: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(map[any]any{extraKey: extraDataString}, conn.Permissions.ExtraData) {
|
|
t.Errorf("expected extra data: %v; got: %v", extraDataString, conn.Permissions.ExtraData)
|
|
}
|
|
}()
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-done
|
|
}
|
|
|
|
func TestVerifiedPublicCallbackPartialSuccess(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
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{
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if string(password) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: nil,
|
|
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.AllowedMethods, "password") && slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
|
return Password(clientPassword), nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPublicKeyCallbackPwdAndKey(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if string(password) == clientPassword {
|
|
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")
|
|
},
|
|
},
|
|
}
|
|
}
|
|
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, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
Password(clientPassword),
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err != nil {
|
|
t.Fatalf("client login error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPubKeyCallbackAuthMethods(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
return nil, nil
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("client login succeed with only VerifiedPublicKeyCallback defined")
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPubKeyCallbackError(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return nil, nil
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("client login succeed with VerifiedPublicKeyCallback returning an error")
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPubKeyCallbackSourceAddress(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return nil, nil
|
|
},
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
return &Permissions{
|
|
CriticalOptions: map[string]string{
|
|
sourceAddressCriticalOption: "192.168.99.99",
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("client login succeed with VerifiedPublicKeyCallback returning mismatching source-address")
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
|
// Returning PartialSuccessError is not permitted when
|
|
// VerifiedPublicKeyCallback is defined. This callback is
|
|
// invoked for both query requests and real authentications,
|
|
// while VerifiedPublicKeyCallback is only triggered if the
|
|
// client has proven control of the key.
|
|
return nil, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if string(password) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
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{
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if string(password) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("authentication succeeded with PartialSuccess returned from PublicKeyCallback and VerifiedPublicKeyCallback defined")
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPublicKeyCallbackOnError(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
var verifiedCallbackCalled bool
|
|
|
|
serverConf := &ServerConfig{
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
verifiedCallbackCalled = true
|
|
return nil, nil
|
|
},
|
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
|
return nil, errors.New("invalid key")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
NewServerConn(c1, serverConf)
|
|
}()
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("authentication should fail")
|
|
}
|
|
<-done
|
|
if verifiedCallbackCalled {
|
|
t.Error("VerifiedPublicKeyCallback called after PublicKeyCallback returned an error")
|
|
}
|
|
}
|
|
|
|
func TestVerifiedPublicKeyCallbackOnly(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
NewServerConn(c1, serverConf)
|
|
}()
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("authentication succeeded with only VerifiedPublicKeyCallback defined")
|
|
}
|
|
<-done
|
|
}
|
|
|
|
func TestPartialSuccessWithNonNilPerms(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
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()) {
|
|
// Intentionally return non-nil Permissions along with a
|
|
// PartialSuccessError. Since permissions are reset between
|
|
// authentication steps, this constitutes invalid library usage
|
|
// and the server is expected to reject the connection.
|
|
return &Permissions{Extensions: map[string]string{"permit-port-forwarding": ""}}, &PartialSuccessError{
|
|
Next: ServerAuthCallbacks{
|
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
|
if string(password) == clientPassword {
|
|
return nil, nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return nil, errors.New("invalid credentials")
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
|
|
clientConf := ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
go NewServerConn(c1, serverConf)
|
|
|
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
|
if err == nil {
|
|
t.Fatal("authentication succeeded unexpectedly; server should have rejected non-nil Permissions combined with PartialSuccessError")
|
|
}
|
|
}
|
|
|
|
type markerConn struct {
|
|
closed uint32
|
|
used uint32
|
|
}
|
|
|
|
func (c *markerConn) isClosed() bool {
|
|
return atomic.LoadUint32(&c.closed) != 0
|
|
}
|
|
|
|
func (c *markerConn) isUsed() bool {
|
|
return atomic.LoadUint32(&c.used) != 0
|
|
}
|
|
|
|
func (c *markerConn) Close() error {
|
|
atomic.StoreUint32(&c.closed, 1)
|
|
return nil
|
|
}
|
|
|
|
func (c *markerConn) Read(b []byte) (n int, err error) {
|
|
atomic.StoreUint32(&c.used, 1)
|
|
if atomic.LoadUint32(&c.closed) != 0 {
|
|
return 0, net.ErrClosed
|
|
} else {
|
|
return 0, io.EOF
|
|
}
|
|
}
|
|
|
|
func (c *markerConn) Write(b []byte) (n int, err error) {
|
|
atomic.StoreUint32(&c.used, 1)
|
|
if atomic.LoadUint32(&c.closed) != 0 {
|
|
return 0, net.ErrClosed
|
|
} else {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
}
|
|
|
|
func (*markerConn) LocalAddr() net.Addr { return nil }
|
|
func (*markerConn) RemoteAddr() net.Addr { return nil }
|
|
|
|
func (*markerConn) SetDeadline(t time.Time) error { return nil }
|
|
func (*markerConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (*markerConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
// skTestSigner is a Signer that produces SK-ECDSA signatures over
|
|
// user-auth data, simulating a FIDO/U2F authenticator. The flags
|
|
// byte (UP and other bits) is caller-controlled so tests can exercise
|
|
// the server's user-presence enforcement and its opt-out paths. This
|
|
// is test-only: the real Signer is hardware-backed.
|
|
type skTestSigner struct {
|
|
priv *ecdsa.PrivateKey
|
|
pub PublicKey
|
|
flags byte
|
|
application string
|
|
}
|
|
|
|
func (s *skTestSigner) PublicKey() PublicKey { return s.pub }
|
|
|
|
func (s *skTestSigner) Sign(r io.Reader, data []byte) (*Signature, error) {
|
|
h := sha256.New()
|
|
h.Write([]byte(s.application))
|
|
appDigest := h.Sum(nil)
|
|
h.Reset()
|
|
h.Write(data)
|
|
dataDigest := h.Sum(nil)
|
|
var counter uint32 = 1
|
|
blob := struct {
|
|
ApplicationDigest []byte `ssh:"rest"`
|
|
Flags byte
|
|
Counter uint32
|
|
MessageDigest []byte `ssh:"rest"`
|
|
}{appDigest, s.flags, counter, dataDigest}
|
|
h.Reset()
|
|
h.Write(Marshal(blob))
|
|
digest := h.Sum(nil)
|
|
x, y, err := ecdsa.Sign(r, s.priv, digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Signature{
|
|
Format: KeyAlgoSKECDSA256,
|
|
Blob: Marshal(struct{ R, S *big.Int }{x, y}),
|
|
Rest: Marshal(struct {
|
|
Flags byte
|
|
Counter uint32
|
|
}{s.flags, counter}),
|
|
}, nil
|
|
}
|
|
|
|
// TestServerAuthSKUserPresence drives the full userAuthLoop with an SK
|
|
// public-key client and verifies the server's wiring of the UP check
|
|
// and its two opt-out paths: the per-key Permissions.Extensions route
|
|
// and (via cert) the cert-level route. It also confirms that non-SK
|
|
// clients are unaffected by the new code path.
|
|
func TestServerAuthSKUserPresence(t *testing.T) {
|
|
userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
skPub := &skECDSAPublicKey{application: "ssh:", PublicKey: userKey.PublicKey}
|
|
|
|
runAuth := func(t *testing.T, signer Signer, perms *Permissions) error {
|
|
t.Helper()
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
serverConf := &ServerConfig{
|
|
PublicKeyCallback: func(ConnMetadata, PublicKey) (*Permissions, error) {
|
|
return perms, nil
|
|
},
|
|
}
|
|
serverConf.AddHostKey(testSigners["ecdsa"])
|
|
|
|
serverErr := make(chan error, 1)
|
|
go func() {
|
|
_, _, _, err := NewServerConn(c1, serverConf)
|
|
serverErr <- err
|
|
}()
|
|
|
|
clientConf := &ClientConfig{
|
|
User: "user",
|
|
Auth: []AuthMethod{PublicKeys(signer)},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
_, _, _, clientErr := NewClientConn(c2, "", clientConf)
|
|
<-serverErr
|
|
return clientErr
|
|
}
|
|
|
|
optOut := &Permissions{Extensions: map[string]string{noTouchRequiredExtension: ""}}
|
|
|
|
t.Run("UP=1, default perms accepts", func(t *testing.T) {
|
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: flagUserPresence, application: "ssh:"}
|
|
if err := runAuth(t, s, nil); err != nil {
|
|
t.Errorf("expected auth to succeed: %v", err)
|
|
}
|
|
})
|
|
t.Run("UP=0, default perms rejects", func(t *testing.T) {
|
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"}
|
|
if err := runAuth(t, s, nil); err == nil {
|
|
t.Error("expected auth to fail with UP=0")
|
|
}
|
|
})
|
|
t.Run("UP=0, perms opt-out accepts", func(t *testing.T) {
|
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"}
|
|
if err := runAuth(t, s, optOut); err != nil {
|
|
t.Errorf("expected auth to succeed with opt-out: %v", err)
|
|
}
|
|
})
|
|
t.Run("UP=0, perms CriticalOptions does NOT opt out", func(t *testing.T) {
|
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"}
|
|
critOnly := &Permissions{CriticalOptions: map[string]string{noTouchRequiredExtension: ""}}
|
|
if err := runAuth(t, s, critOnly); err == nil {
|
|
t.Error("no-touch-required in CriticalOptions must not waive UP")
|
|
}
|
|
})
|
|
t.Run("non-SK RSA signer unaffected", func(t *testing.T) {
|
|
if err := runAuth(t, testSigners["rsa"], nil); err != nil {
|
|
t.Errorf("plain RSA auth must still work: %v", err)
|
|
}
|
|
})
|
|
}
|