Skip to content

Commit 29bf9ec

Browse files
committed
ssh: add initial support for extension negotiation (rfc 8308) and server-sig-algs
1 parent 089bfa5 commit 29bf9ec

9 files changed

+719
-44
lines changed

ssh/client_auth.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,31 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
2525
if err := c.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil {
2626
return err
2727
}
28-
packet, err := c.transport.readPacket()
29-
if err != nil {
30-
return err
31-
}
28+
3229
var serviceAccept serviceAcceptMsg
33-
if err := Unmarshal(packet, &serviceAccept); err != nil {
34-
return err
30+
readAcceptLoop:
31+
for {
32+
packet, err := c.transport.readPacket()
33+
if err != nil {
34+
return err
35+
}
36+
37+
switch packet[0] {
38+
case msgExtInfo:
39+
var extInfo extInfoMsg
40+
if err := Unmarshal(packet, &extInfo); err != nil {
41+
return err
42+
}
43+
c.transport.extensions = extInfo.Extensions
44+
continue
45+
case msgServiceAccept:
46+
if err := Unmarshal(packet, &serviceAccept); err != nil {
47+
return err
48+
}
49+
break readAcceptLoop
50+
default:
51+
return fmt.Errorf("ssh: unexpected message received")
52+
}
3553
}
3654

3755
// during the authentication phase the client first attempts the "none" method
@@ -337,6 +355,14 @@ func handleAuthResponse(c packetConn) (authResult, []string, error) {
337355
}
338356

339357
switch packet[0] {
358+
case msgExtInfo:
359+
var extInfo extInfoMsg
360+
if err := Unmarshal(packet, &extInfo); err != nil {
361+
return authFailure, nil, err
362+
}
363+
if transport, ok := c.(*handshakeTransport); ok {
364+
transport.extensions = extInfo.Extensions
365+
}
340366
case msgUserAuthBanner:
341367
if err := handleBannerResponse(c, packet); err != nil {
342368
return authFailure, nil, err
@@ -420,6 +446,15 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
420446

421447
// like handleAuthResponse, but with less options.
422448
switch packet[0] {
449+
case msgExtInfo:
450+
var extInfo extInfoMsg
451+
if err := Unmarshal(packet, &extInfo); err != nil {
452+
return authFailure, nil, err
453+
}
454+
if transport, ok := c.(*handshakeTransport); ok {
455+
transport.extensions = extInfo.Extensions
456+
}
457+
continue
423458
case msgUserAuthBanner:
424459
if err := handleBannerResponse(c, packet); err != nil {
425460
return authFailure, nil, err

ssh/client_auth_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,75 @@ func TestClientAuthPublicKey(t *testing.T) {
117117
}
118118
}
119119

120+
func TestClientAuthPublicKeyExtensions(t *testing.T) {
121+
c1, c2, err := netPipe()
122+
if err != nil {
123+
t.Fatalf("netPipe: %v", err)
124+
}
125+
defer c1.Close()
126+
defer c2.Close()
127+
128+
certChecker := CertChecker{
129+
IsUserAuthority: func(k PublicKey) bool {
130+
return bytes.Equal(k.Marshal(), testPublicKeys["ecdsa"].Marshal())
131+
},
132+
UserKeyFallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
133+
if conn.User() == "testuser" && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
134+
return nil, nil
135+
}
136+
137+
return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User())
138+
},
139+
IsRevoked: func(c *Certificate) bool {
140+
return c.Serial == 666
141+
},
142+
}
143+
serverConfig := &ServerConfig{
144+
PublicKeyCallback: certChecker.Authenticate,
145+
}
146+
serverConfig.AddHostKey(testSigners["rsa"])
147+
148+
go newServer(c1, serverConfig)
149+
clientConn, _, _, err := NewClientConn(c2, "", &ClientConfig{
150+
User: "testuser",
151+
Auth: []AuthMethod{
152+
PublicKeys(testSigners["rsa"]),
153+
},
154+
HostKeyCallback: InsecureIgnoreHostKey(),
155+
})
156+
if err != nil {
157+
t.Fatalf("NewClientConn: %v", err)
158+
}
159+
160+
conn, ok := clientConn.(*connection)
161+
if !ok {
162+
t.Fatalf("conn is not a *connection")
163+
}
164+
165+
rawServerSigAlgs, ok := conn.transport.extensions[ExtServerSigAlgs]
166+
if !ok {
167+
t.Fatalf("did not receive server-sig-algs extension")
168+
}
169+
170+
serverSigAlgs := strings.Split(string(rawServerSigAlgs), ",")
171+
if len(serverSigAlgs) == 0 {
172+
t.Fatalf("did not receive any server-sig-algs")
173+
}
174+
175+
for _, expectedAlg := range supportedSigAlgs() {
176+
hasAlg := false
177+
for _, receivedAlg := range serverSigAlgs {
178+
if receivedAlg == expectedAlg {
179+
hasAlg = true
180+
break
181+
}
182+
}
183+
if !hasAlg {
184+
t.Errorf("server-sig-algs did not have expected alg: %s", expectedAlg)
185+
}
186+
}
187+
}
188+
120189
func TestAuthMethodPassword(t *testing.T) {
121190
config := &ClientConfig{
122191
User: "testuser",
@@ -131,6 +200,65 @@ func TestAuthMethodPassword(t *testing.T) {
131200
}
132201
}
133202

203+
func TestClientAuthPasswordExtensions(t *testing.T) {
204+
c1, c2, err := netPipe()
205+
if err != nil {
206+
t.Fatalf("netPipe: %v", err)
207+
}
208+
defer c1.Close()
209+
defer c2.Close()
210+
211+
serverConfig := &ServerConfig{
212+
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
213+
if conn.User() == "testuser" && string(pass) == clientPassword {
214+
return nil, nil
215+
}
216+
return nil, errors.New("password auth failed")
217+
},
218+
}
219+
serverConfig.AddHostKey(testSigners["rsa"])
220+
221+
go newServer(c1, serverConfig)
222+
clientConn, _, _, err := NewClientConn(c2, "", &ClientConfig{
223+
User: "testuser",
224+
Auth: []AuthMethod{
225+
Password(clientPassword),
226+
},
227+
HostKeyCallback: InsecureIgnoreHostKey(),
228+
})
229+
if err != nil {
230+
t.Fatalf("NewClientConn: %v", err)
231+
}
232+
233+
conn, ok := clientConn.(*connection)
234+
if !ok {
235+
t.Fatalf("conn is not a *connection")
236+
}
237+
238+
rawServerSigAlgs, ok := conn.transport.extensions[ExtServerSigAlgs]
239+
if !ok {
240+
t.Fatalf("did not receive server-sig-algs extension")
241+
}
242+
243+
serverSigAlgs := strings.Split(string(rawServerSigAlgs), ",")
244+
if len(serverSigAlgs) == 0 {
245+
t.Fatalf("did not receive any server-sig-algs")
246+
}
247+
248+
for _, expectedAlg := range supportedSigAlgs() {
249+
hasAlg := false
250+
for _, receivedAlg := range serverSigAlgs {
251+
if receivedAlg == expectedAlg {
252+
hasAlg = true
253+
break
254+
}
255+
}
256+
if !hasAlg {
257+
t.Errorf("server-sig-algs did not have expected alg: %s", expectedAlg)
258+
}
259+
}
260+
}
261+
134262
func TestAuthMethodFallback(t *testing.T) {
135263
var passwordCalled bool
136264
config := &ClientConfig{

ssh/common.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ const (
2424
serviceSSH = "ssh-connection"
2525
)
2626

27+
// These are string constants related to extensions and extension negotiation
28+
const (
29+
extInfoServer = "ext-info-s"
30+
extInfoClient = "ext-info-c"
31+
32+
ExtServerSigAlgs = "server-sig-algs"
33+
// extDelayCompression = "delay-compression"
34+
// extNoFlowControl = "no-flow-control"
35+
// extElevation = "elevation"
36+
)
37+
38+
// defaultExtensions lists extensions enabled by default.
39+
var defaultExtensions = []string{
40+
ExtServerSigAlgs,
41+
}
42+
2743
// supportedCiphers lists ciphers we support but might not recommend.
2844
var supportedCiphers = []string{
2945
"aes128-ctr", "aes192-ctr", "aes256-ctr",
@@ -102,6 +118,18 @@ var hashFuncs = map[string]crypto.Hash{
102118
CertAlgoECDSA521v01: crypto.SHA512,
103119
}
104120

121+
// supportedSigAlgs returns a slice of algorithms supported for pubkey authentication
122+
// in no particular order.
123+
func supportedSigAlgs() []string {
124+
// TODO(kxd) I'm not sure if hashFuncs is the best place to get this set but it seemed
125+
// like a sensible first step. Should this be a curated list?
126+
var serverSigAlgs []string
127+
for k := range hashFuncs {
128+
serverSigAlgs = append(serverSigAlgs, k)
129+
}
130+
return serverSigAlgs
131+
}
132+
105133
// unexpectedMessageError results when the SSH message that we received didn't
106134
// match what we wanted.
107135
func unexpectedMessageError(expected, got uint8) error {
@@ -124,6 +152,16 @@ func findCommon(what string, client []string, server []string) (common string, e
124152
return "", fmt.Errorf("ssh: no common algorithm for %s; client offered: %v, server offered: %v", what, client, server)
125153
}
126154

155+
// hasString returns true if string "a" is in slice of strings "x", false otherwise.
156+
func hasString(a string, x []string) bool {
157+
for _, s := range x {
158+
if a == s {
159+
return true
160+
}
161+
}
162+
return false
163+
}
164+
127165
// directionAlgorithms records algorithm choices in one direction (either read or write)
128166
type directionAlgorithms struct {
129167
Cipher string
@@ -159,6 +197,11 @@ func findAgreedAlgorithms(isClient bool, clientKexInit, serverKexInit *kexInitMs
159197
result.kex, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos)
160198
if err != nil {
161199
return
200+
} else if result.kex == extInfoClient || result.kex == extInfoServer {
201+
// According to RFC8308 section 2.2 if either the client or server extension signal
202+
// is chosen as the kex algorithm the parties must disconnect.
203+
// chosen
204+
return result, fmt.Errorf("ssh: invalid kex algorithm chosen")
162205
}
163206

164207
result.hostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos)
@@ -232,6 +275,10 @@ type Config struct {
232275
// The allowed MAC algorithms. If unspecified then a sensible default
233276
// is used.
234277
MACs []string
278+
279+
// A list of enabled extensions. If unspecified then a sensible
280+
// default is used
281+
Extensions []string
235282
}
236283

237284
// SetDefaults sets sensible values for unset fields in config. This is
@@ -261,6 +308,10 @@ func (c *Config) SetDefaults() {
261308
c.MACs = supportedMACs
262309
}
263310

311+
if c.Extensions == nil {
312+
c.Extensions = defaultExtensions
313+
}
314+
264315
if c.RekeyThreshold == 0 {
265316
// cipher specific default
266317
} else if c.RekeyThreshold < minRekeyThreshold {

ssh/common_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,28 @@ func TestFindAgreedAlgorithms(t *testing.T) {
110110
wantErr: true,
111111
},
112112

113+
testcase{
114+
name: "server ext info kex chosen",
115+
serverIn: kexInitMsg{
116+
KexAlgos: []string{extInfoServer},
117+
},
118+
clientIn: kexInitMsg{
119+
KexAlgos: []string{extInfoServer},
120+
},
121+
wantErr: true,
122+
},
123+
124+
testcase{
125+
name: "client ext info kex chosen",
126+
serverIn: kexInitMsg{
127+
KexAlgos: []string{extInfoClient},
128+
},
129+
clientIn: kexInitMsg{
130+
KexAlgos: []string{extInfoClient},
131+
},
132+
wantErr: true,
133+
},
134+
113135
testcase{
114136
name: "client decides cipher",
115137
serverIn: kexInitMsg{

0 commit comments

Comments
 (0)