Files
crypto/ssh/server_test.go
Akira ff03dafdb0 ssh: report verified signature format to callback
VerifiedPublicKeyCallback currently receives the public-key algorithm
declared in the outer authentication request. For RSA authentication,
that value can differ from the format of the signature that was
successfully verified because compatible RSA algorithm combinations are
accepted.

This can cause post-verification policy or audit decisions to use the
request algorithm instead of the signature format actually verified.

Pass sig.Format to VerifiedPublicKeyCallback so signatureAlgorithm
identifies the successfully verified signature format. Clarify the
callback documentation and add a regression test covering compatible
but different RSA request algorithms and signature formats.

Fixes golang/go#80411

Change-Id: Ib405378d75367a90536e0814c9d26d4dec9aaa25
GitHub-Last-Rev: 1a4acd4e639361be1f2948a4aef1013436a75ecf
GitHub-Pull-Request: golang/crypto#364
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/800740
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-07-17 15:41:46 -07:00

1283 lines
36 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 TestMaxAuthServerAttempts(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 server to disconnect after exceeding the attempts cap")
}
// The error must be the server's disconnect, not the client-side
// "too many authentication attempts (N), aborting" bound. The
// disconnect format is set by disconnectMsg.Error().
if !strings.Contains(err.Error(), "ssh: disconnect") ||
!strings.Contains(err.Error(), "too many authentication attempts") {
t.Fatalf("expected server disconnect, got: %v", err)
}
// Server-side request sequence per AuthCallback iteration:
// 1 "none" (initial, before any AuthCallback)
// 2 pubkey query (AuthCallback #k, server returns OK)
// 3 pubkey signed (AuthCallback #k, server returns partial)
// So after k complete callbacks the server has processed 1+2k requests.
// The (k+1)-th callback's query is processed as request number 2(k+1);
// the subsequent signed request is rejected by the cap check
// (authAttempts >= maxAuthServerAttempts), so 2(k+1) = maxAuthServerAttempts
// and invocations = maxAuthServerAttempts / 2.
expected := maxAuthServerAttempts / 2
if invocations != expected {
t.Errorf("AuthCallback invoked %d times; want %d", invocations, expected)
}
}
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 TestVerifiedPublicKeyCallbackSignatureFormat(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
var gotSignatureAlgorithm string
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) {
gotSignatureAlgorithm = signatureAlgorithm
return permissions, nil
},
}
serverConf.AddHostKey(testSigners["rsa"])
serverDone := make(chan error, 1)
go func() {
conn, _, _, err := NewServerConn(c1, serverConf)
if err == nil {
conn.Close()
}
serverDone <- err
}()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
// The selected algorithm and signature format differ for
// compatibility with old RSA clients.
configurablePublicKeyCallback{
signer: testSigners["rsa"].(AlgorithmSigner),
signatureAlgo: KeyAlgoRSASHA256,
signatureFormat: KeyAlgoRSA,
},
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
clientConn, _, _, clientErr := NewClientConn(c2, "", clientConf)
if clientErr == nil {
clientConn.Close()
}
serverErr := <-serverDone
if clientErr != nil || serverErr != nil {
t.Fatalf("connection failed: client error: %v; server error: %v", clientErr, serverErr)
}
if gotSignatureAlgorithm != KeyAlgoRSA {
t.Errorf("signature algorithm = %q; want verified signature format %q", gotSignatureAlgorithm, KeyAlgoRSA)
}
}
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 TestAuthCallbacksSourceAddress(t *testing.T) {
permsWithSourceAddress := func(sourceAddress string) *Permissions {
return &Permissions{
CriticalOptions: map[string]string{
sourceAddressCriticalOption: sourceAddress,
},
}
}
methods := []struct {
name string
serverConfig func(sourceAddress string) *ServerConfig
clientAuth []AuthMethod
}{
{
name: "password",
serverConfig: func(sourceAddress string) *ServerConfig {
return &ServerConfig{
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
return permsWithSourceAddress(sourceAddress), nil
},
}
},
clientAuth: []AuthMethod{Password(clientPassword)},
},
{
name: "keyboard-interactive",
serverConfig: func(sourceAddress string) *ServerConfig {
return &ServerConfig{
KeyboardInteractiveCallback: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (*Permissions, error) {
return permsWithSourceAddress(sourceAddress), nil
},
}
},
clientAuth: []AuthMethod{
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return nil, nil
}),
},
},
{
name: "none",
serverConfig: func(sourceAddress string) *ServerConfig {
return &ServerConfig{
NoClientAuth: true,
NoClientAuthCallback: func(conn ConnMetadata) (*Permissions, error) {
return permsWithSourceAddress(sourceAddress), nil
},
}
},
},
}
for _, method := range methods {
for _, tc := range []struct {
name string
sourceAddress string
wantErr bool
}{
{"mismatching", "192.168.99.99", true},
{"matching", "127.0.0.0/8,::1/128", false},
{"present but empty", "", true},
} {
t.Run(method.name+"/"+tc.name, func(t *testing.T) {
clientConf := &ClientConfig{
User: "user",
Auth: method.clientAuth,
HostKeyCallback: InsecureIgnoreHostKey(),
}
serverAuthErrors, err := doClientServerAuth(t, method.serverConfig(tc.sourceAddress), clientConf)
if tc.wantErr {
if err == nil {
t.Fatalf("client login succeeded with %s callback returning mismatching source-address", method.name)
}
var sourceAddressErrors int
for _, err := range serverAuthErrors {
if err != nil && strings.Contains(err.Error(), "source-address restriction") {
sourceAddressErrors++
}
}
if sourceAddressErrors == 0 {
t.Fatalf("no server auth error mentions the source-address restriction, got %v", serverAuthErrors)
}
} else if err != nil {
t.Fatalf("client login failed with matching source-address: %v", err)
}
})
}
}
}
func TestCheckSourceAddressNonIP(t *testing.T) {
for _, addr := range []net.Addr{
&net.UnixAddr{Name: "/run/test.sock", Net: "unix"},
&net.UnixAddr{},
nil,
} {
if err := checkSourceAddress(addr, "127.0.0.0/8,::1/128"); err == nil {
t.Errorf("source-address check succeeded for non-IP remote address %v", addr)
}
}
}
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)
}
})
}