Previously the library did not verify the "User Presence" (UP) bit in signatures generated by FIDO/U2F security keys (sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com). This allowed signatures without physical interaction to be accepted if the underlying hardware produced them, deviating from the default secure behavior expected by the FIDO standards and OpenSSH. skECDSAPublicKey.Verify and skEd25519PublicKey.Verify now enforce the user-presence bit (0x01, constant flagUserPresence) by default. Signatures whose flags byte has UP clear fail with the sentinel errSKMissingUserPresence. The server public-key authentication path honors the OpenSSH "no-touch-required" extension as an opt-out. noTouchAllowed reports true when the extension is present either in the Permissions returned by PublicKeyCallback (authorized_keys-level opt-out) or in the certificate's own Extensions (CA-level opt-out); in that case skKeyWithoutUP is used to derive a clone of the SK public key (and, for certificates, a clone of the wrapping Certificate whose inner Key is the cloned SK key) whose Verify accepts UP-clear signatures. The originals are never mutated, so a per-session opt-out cannot leak across authentication attempts or connections. Matching OpenSSH, the opt-out is read only from Extensions, never from CriticalOptions. skKeyWithoutUP is iterative and unwraps at most one level of *Certificate: the SSH cert format forbids Certificate.Key from being another Certificate (parseCert rejects it) but callers can still construct such a value directly in Go, so a recursive descent would be driven to unbounded depth by malformed or cyclic input. Any such pathological *Certificate is returned unchanged. CertChecker.CheckCert applies skKeyWithoutUP unconditionally to the certificate's CA key before verifying the CA signature, matching OpenSSH, which calls sshkey_verify with detailsp==NULL in sshkey.c:cert_parse and never extracts or enforces UP/UV flags on CA signatures. The UP bit on a CA signature reflects the CA operator's presence at cert-issuance time, which has no bearing on whether the user being authenticated is present now, so enforcing it here would only break interop with certificates issued by non-interactive SK CAs without a corresponding security benefit. The skKeyWithoutUP call is a no-op for non-SK CA keys (the common case). This change breaks backward compatibility for clients or keys that generate user-authentication signatures without the User Presence flag set. Previously those signatures were accepted by the server. They will now be rejected with "ssh: signature missing required user presence flag" unless the "no-touch-required" extension is explicitly granted to the session by the server callbacks, or carried by the user certificate. This issue was found during a security audit by NCC Group Cryptography Services, sponsored by Teleport. Fixes golang/go#79566 Fixes CVE-2026-39831 Change-Id: I74b6de3bb6a2d7a0f34d7fa36bbbbf06f0b3fc6b Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781662 Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-by: Roland Shoemaker <roland@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1035 lines
28 KiB
Go
1035 lines
28 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"
|
|
"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: []AuthMethod{
|
|
PublicKeys(testSigners["rsa"]),
|
|
Password(clientPassword),
|
|
},
|
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
})
|
|
}
|