Skip to content

Commit 44c9b0f

Browse files
awlygopherbot
authored andcommitted
ssh: allow server auth callbacks to send additional banners
Add a new BannerError error type that auth callbacks can return to send banner to the client. While the BannerCallback can send the initial banner message, auth callbacks might want to communicate more information to the client to help them diagnose failures. Updates golang/go#64962 Change-Id: I97a26480ff4064b95a0a26042b0a5e19737cfb62 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/558695 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Nicola Murino <[email protected]> Auto-Submit: Nicola Murino <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 67b1361 commit 44c9b0f

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

ssh/server.go

+30
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,24 @@ func (p *PartialSuccessError) Error() string {
462462
// It is returned in ServerAuthError.Errors from NewServerConn.
463463
var ErrNoAuth = errors.New("ssh: no auth passed yet")
464464

465+
// BannerError is an error that can be returned by authentication handlers in
466+
// ServerConfig to send a banner message to the client.
467+
type BannerError struct {
468+
Err error
469+
Message string
470+
}
471+
472+
func (b *BannerError) Unwrap() error {
473+
return b.Err
474+
}
475+
476+
func (b *BannerError) Error() string {
477+
if b.Err == nil {
478+
return b.Message
479+
}
480+
return b.Err.Error()
481+
}
482+
465483
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
466484
sessionID := s.transport.getSessionID()
467485
var cache pubKeyCache
@@ -734,6 +752,18 @@ userAuthLoop:
734752
config.AuthLogCallback(s, userAuthReq.Method, authErr)
735753
}
736754

755+
var bannerErr *BannerError
756+
if errors.As(authErr, &bannerErr) {
757+
if bannerErr.Message != "" {
758+
bannerMsg := &userAuthBannerMsg{
759+
Message: bannerErr.Message,
760+
}
761+
if err := s.transport.writePacket(Marshal(bannerMsg)); err != nil {
762+
return nil, err
763+
}
764+
}
765+
}
766+
737767
if authErr == nil {
738768
break userAuthLoop
739769
}

ssh/server_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ package ssh
66

77
import (
88
"errors"
9+
"fmt"
910
"io"
1011
"net"
12+
"slices"
1113
"strings"
1214
"sync/atomic"
1315
"testing"
@@ -225,6 +227,78 @@ func TestNewServerConnValidationErrors(t *testing.T) {
225227
}
226228
}
227229

230+
func TestBannerError(t *testing.T) {
231+
serverConfig := &ServerConfig{
232+
BannerCallback: func(ConnMetadata) string {
233+
return "banner from BannerCallback"
234+
},
235+
NoClientAuth: true,
236+
NoClientAuthCallback: func(ConnMetadata) (*Permissions, error) {
237+
err := &BannerError{
238+
Err: errors.New("error from NoClientAuthCallback"),
239+
Message: "banner from NoClientAuthCallback",
240+
}
241+
return nil, fmt.Errorf("wrapped: %w", err)
242+
},
243+
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
244+
return &Permissions{}, nil
245+
},
246+
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
247+
return nil, &BannerError{
248+
Err: errors.New("error from PublicKeyCallback"),
249+
Message: "banner from PublicKeyCallback",
250+
}
251+
},
252+
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
253+
return nil, &BannerError{
254+
Err: nil, // make sure that a nil inner error is allowed
255+
Message: "banner from KeyboardInteractiveCallback",
256+
}
257+
},
258+
}
259+
serverConfig.AddHostKey(testSigners["rsa"])
260+
261+
var banners []string
262+
clientConfig := &ClientConfig{
263+
User: "test",
264+
Auth: []AuthMethod{
265+
PublicKeys(testSigners["rsa"]),
266+
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
267+
return []string{"letmein"}, nil
268+
}),
269+
Password(clientPassword),
270+
},
271+
HostKeyCallback: InsecureIgnoreHostKey(),
272+
BannerCallback: func(msg string) error {
273+
banners = append(banners, msg)
274+
return nil
275+
},
276+
}
277+
278+
c1, c2, err := netPipe()
279+
if err != nil {
280+
t.Fatalf("netPipe: %v", err)
281+
}
282+
defer c1.Close()
283+
defer c2.Close()
284+
go newServer(c1, serverConfig)
285+
c, _, _, err := NewClientConn(c2, "", clientConfig)
286+
if err != nil {
287+
t.Fatalf("client connection failed: %v", err)
288+
}
289+
defer c.Close()
290+
291+
wantBanners := []string{
292+
"banner from BannerCallback",
293+
"banner from NoClientAuthCallback",
294+
"banner from PublicKeyCallback",
295+
"banner from KeyboardInteractiveCallback",
296+
}
297+
if !slices.Equal(banners, wantBanners) {
298+
t.Errorf("got banners:\n%q\nwant banners:\n%q", banners, wantBanners)
299+
}
300+
}
301+
228302
type markerConn struct {
229303
closed uint32
230304
used uint32

0 commit comments

Comments
 (0)