Skip to content

Commit 9b8c011

Browse files
drakkaniQQBot
authored andcommitted
ssh: add support for extension negotiation (rfc 8308)
This is a rebase of the following PR golang#197 with some changes and improvements: - added support for client certificate authentication - removed read loop from server handshake - adapted extInfoMsg to upstream changes Signed-off-by: Nicola Murino <[email protected]>
1 parent 1143f71 commit 9b8c011

File tree

4 files changed

+102
-5
lines changed

4 files changed

+102
-5
lines changed

ssh/client_auth_test.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ func TestClientAuthPublicKey(t *testing.T) {
132132
if err := tryAuth(t, config); err != nil {
133133
t.Fatalf("unable to dial remote side: %s", err)
134134
}
135-
// Once the server implements the server-sig-algs extension, this will turn
136-
// into KeyAlgoRSASHA256.
137-
if len(signer.used) != 1 || signer.used[0] != KeyAlgoRSA {
135+
if len(signer.used) != 1 || signer.used[0] != KeyAlgoRSASHA256 {
138136
t.Errorf("unexpected Sign/SignWithAlgorithm calls: %q", signer.used)
139137
}
140138
}

ssh/common.go

+33
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ 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+
ExtServerSigAlgs = "server-sig-algs"
32+
)
33+
34+
// defaultExtensions lists extensions enabled by default.
35+
var defaultExtensions = []string{
36+
ExtServerSigAlgs,
37+
}
38+
2739
// supportedCiphers lists ciphers we support but might not recommend.
2840
var supportedCiphers = []string{
2941
"aes128-ctr", "aes192-ctr", "aes256-ctr",
@@ -89,6 +101,15 @@ var supportedMACs = []string{
89101

90102
var supportedCompressions = []string{compressionNone}
91103

104+
// supportedServerSigAlgs defines the algorithms supported for pubkey authentication
105+
// in no particular order.
106+
var supportedServerSigAlgs = []string{KeyAlgoRSASHA256,
107+
KeyAlgoRSASHA512, KeyAlgoRSA,
108+
KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
109+
KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519,
110+
KeyAlgoDSA,
111+
}
112+
92113
// hashFuncs keeps the mapping of supported signature algorithms to their
93114
// respective hashes needed for signing and verification.
94115
var hashFuncs = map[string]crypto.Hash{
@@ -180,6 +201,10 @@ func findAgreedAlgorithms(isClient bool, clientKexInit, serverKexInit *kexInitMs
180201
result.kex, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos)
181202
if err != nil {
182203
return
204+
} else if result.kex == extInfoClient || result.kex == extInfoServer {
205+
// According to RFC8308 section 2.2 if either the client or server extension signal
206+
// is chosen as the kex algorithm the parties must disconnect.
207+
return result, fmt.Errorf("ssh: invalid kex algorithm chosen: %s", result.kex)
183208
}
184209

185210
result.hostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos)
@@ -257,6 +282,10 @@ type Config struct {
257282
// The allowed MAC algorithms. If unspecified then a sensible default
258283
// is used.
259284
MACs []string
285+
286+
// A list of enabled extensions. If unspecified then a sensible
287+
// default is used
288+
Extensions []string
260289
}
261290

262291
// SetDefaults sets sensible values for unset fields in config. This is
@@ -286,6 +315,10 @@ func (c *Config) SetDefaults() {
286315
c.MACs = supportedMACs
287316
}
288317

318+
if c.Extensions == nil {
319+
c.Extensions = defaultExtensions
320+
}
321+
289322
if c.RekeyThreshold == 0 {
290323
// cipher specific default
291324
} else if c.RekeyThreshold < minRekeyThreshold {

ssh/handshake.go

+36-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"log"
1313
"net"
14+
"strings"
1415
"sync"
1516
)
1617

@@ -94,6 +95,10 @@ type handshakeTransport struct {
9495

9596
// The session ID or nil if first kex did not complete yet.
9697
sessionID []byte
98+
99+
// True if the first ext info message has been sent immediately following
100+
// SSH_MSG_NEWKEYS, false otherwise.
101+
extInfoSent bool
97102
}
98103

99104
type pendingKex struct {
@@ -474,6 +479,9 @@ func (t *handshakeTransport) sendKexInit() error {
474479
msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat)
475480
}
476481
}
482+
if contains(t.config.Extensions, ExtServerSigAlgs) {
483+
msg.KexAlgos = append(msg.KexAlgos, extInfoServer)
484+
}
477485
} else {
478486
msg.ServerHostKeyAlgos = t.hostKeyAlgorithms
479487

@@ -483,7 +491,7 @@ func (t *handshakeTransport) sendKexInit() error {
483491
if firstKeyExchange := t.sessionID == nil; firstKeyExchange {
484492
msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+1)
485493
msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...)
486-
msg.KexAlgos = append(msg.KexAlgos, "ext-info-c")
494+
msg.KexAlgos = append(msg.KexAlgos, extInfoClient)
487495
}
488496
}
489497

@@ -632,6 +640,33 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
632640
return unexpectedMessageError(msgNewKeys, packet[0])
633641
}
634642

643+
if !isClient {
644+
// We're on the server side, see if the client sent the extension signal
645+
if !t.extInfoSent && contains(clientInit.KexAlgos, extInfoClient) && contains(t.config.Extensions, ExtServerSigAlgs) {
646+
// The other side supports ext info, an ext info message hasn't been sent this session,
647+
// and we have at least one extension enabled, so send an SSH_MSG_EXT_INFO message.
648+
extensions := map[string][]byte{}
649+
// We're the server, the client supports SSH_MSG_EXT_INFO and server-sig-algs
650+
// is enabled. Prepare the server-sig-algos extension message to send.
651+
extensions[ExtServerSigAlgs] = []byte(strings.Join(supportedServerSigAlgs, ","))
652+
var payload []byte
653+
for k, v := range extensions {
654+
payload = appendInt(payload, len(k))
655+
payload = append(payload, k...)
656+
payload = appendInt(payload, len(v))
657+
payload = append(payload, v...)
658+
}
659+
extInfo := extInfoMsg{
660+
NumExtensions: uint32(len(extensions)),
661+
Payload: payload,
662+
}
663+
if err := t.conn.writePacket(Marshal(&extInfo)); err != nil {
664+
return err
665+
}
666+
t.extInfoSent = true
667+
}
668+
}
669+
635670
return nil
636671
}
637672

ssh/server.go

+32-1
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,41 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
274274
// We just did the key change, so the session ID is established.
275275
s.sessionID = s.transport.getSessionID()
276276

277+
// the client could send a SSH_MSG_EXT_INFO before SSH_MSG_SERVICE_REQUEST
277278
var packet []byte
278279
if packet, err = s.transport.readPacket(); err != nil {
279280
return nil, err
280281
}
281282

283+
// be permissive and don't add contains(s.transport.config.Extensions, ExtServerSigAlgs)
284+
if len(packet) > 0 && packet[0] == msgExtInfo {
285+
// read SSH_MSG_EXT_INFO
286+
var extInfo extInfoMsg
287+
extensions := make(map[string][]byte)
288+
if err := Unmarshal(packet, &extInfo); err != nil {
289+
return nil, err
290+
}
291+
payload := extInfo.Payload
292+
for i := uint32(0); i < extInfo.NumExtensions; i++ {
293+
name, rest, ok := parseString(payload)
294+
if !ok {
295+
return nil, parseError(msgExtInfo)
296+
}
297+
value, rest, ok := parseString(rest)
298+
if !ok {
299+
return nil, parseError(msgExtInfo)
300+
}
301+
extensions[string(name)] = value
302+
payload = rest
303+
}
304+
305+
// read the next packet
306+
packet = nil
307+
if packet, err = s.transport.readPacket(); err != nil {
308+
return nil, err
309+
}
310+
}
311+
282312
var serviceRequest serviceRequestMsg
283313
if err = Unmarshal(packet, &serviceRequest); err != nil {
284314
return nil, err
@@ -304,7 +334,8 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
304334
func isAcceptableAlgo(algo string) bool {
305335
switch algo {
306336
case KeyAlgoRSA, KeyAlgoRSASHA256, KeyAlgoRSASHA512, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519,
307-
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01:
337+
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01,
338+
CertAlgoRSASHA256v01, CertAlgoRSASHA512v01:
308339
return true
309340
}
310341
return false

0 commit comments

Comments
 (0)