Skip to content

Commit 9d2ee97

Browse files
ssh: implement strict KEX protocol changes
Implement the "strict KEX" protocol changes, as described in section 1.9 of the OpenSSH PROTOCOL file (as of OpenSSH version 9.6/9.6p1). Namely this makes the following changes: * Both the server and the client add an additional algorithm to the initial KEXINIT message, indicating support for the strict KEX mode. * When one side of the connection sees the strict KEX extension algorithm, the strict KEX mode is enabled for messages originating from the other side of the connection. If the sequence number for the side which requested the extension is not 1 (indicating that it has already received non-KEXINIT packets), the connection is terminated. * When strict kex mode is enabled, unexpected messages during the handshake are considered fatal. Additionally when a key change occurs (on the receipt of the NEWKEYS message) the message sequence numbers are reset. Thanks to Fabian Bäumer, Marcus Brinkmann, and Jörg Schwenk from Ruhr University Bochum for reporting this issue. Fixes CVE-2023-48795 Fixes golang/go#64784 Change-Id: I96b53afd2bd2fb94d2b6f2a46a5dacf325357604 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/550715 Reviewed-by: Nicola Murino <nicola.murino@gmail.com> Reviewed-by: Tatiana Bradley <tatianabradley@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Roland Shoemaker <roland@golang.org> Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 4e5a261 commit 9d2ee97

File tree

3 files changed

+388
-9
lines changed

3 files changed

+388
-9
lines changed

ssh/handshake.go

+52-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ type keyingTransport interface {
3535
// direction will be effected if a msgNewKeys message is sent
3636
// or received.
3737
prepareKeyChange(*algorithms, *kexResult) error
38+
39+
// setStrictMode sets the strict KEX mode, notably triggering
40+
// sequence number resets on sending or receiving msgNewKeys.
41+
// If the sequence number is already > 1 when setStrictMode
42+
// is called, an error is returned.
43+
setStrictMode() error
44+
45+
// setInitialKEXDone indicates to the transport that the initial key exchange
46+
// was completed
47+
setInitialKEXDone()
3848
}
3949

4050
// handshakeTransport implements rekeying on top of a keyingTransport
@@ -100,6 +110,10 @@ type handshakeTransport struct {
100110

101111
// The session ID or nil if first kex did not complete yet.
102112
sessionID []byte
113+
114+
// strictMode indicates if the other side of the handshake indicated
115+
// that we should be following the strict KEX protocol restrictions.
116+
strictMode bool
103117
}
104118

105119
type pendingKex struct {
@@ -209,7 +223,10 @@ func (t *handshakeTransport) readLoop() {
209223
close(t.incoming)
210224
break
211225
}
212-
if p[0] == msgIgnore || p[0] == msgDebug {
226+
// If this is the first kex, and strict KEX mode is enabled,
227+
// we don't ignore any messages, as they may be used to manipulate
228+
// the packet sequence numbers.
229+
if !(t.sessionID == nil && t.strictMode) && (p[0] == msgIgnore || p[0] == msgDebug) {
213230
continue
214231
}
215232
t.incoming <- p
@@ -441,6 +458,11 @@ func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) {
441458
return successPacket, nil
442459
}
443460

461+
const (
462+
kexStrictClient = "kex-strict-c-v00@openssh.com"
463+
kexStrictServer = "kex-strict-s-v00@openssh.com"
464+
)
465+
444466
// sendKexInit sends a key change message.
445467
func (t *handshakeTransport) sendKexInit() error {
446468
t.mu.Lock()
@@ -454,7 +476,6 @@ func (t *handshakeTransport) sendKexInit() error {
454476
}
455477

456478
msg := &kexInitMsg{
457-
KexAlgos: t.config.KeyExchanges,
458479
CiphersClientServer: t.config.Ciphers,
459480
CiphersServerClient: t.config.Ciphers,
460481
MACsClientServer: t.config.MACs,
@@ -464,6 +485,13 @@ func (t *handshakeTransport) sendKexInit() error {
464485
}
465486
io.ReadFull(rand.Reader, msg.Cookie[:])
466487

488+
// We mutate the KexAlgos slice, in order to add the kex-strict extension algorithm,
489+
// and possibly to add the ext-info extension algorithm. Since the slice may be the
490+
// user owned KeyExchanges, we create our own slice in order to avoid using user
491+
// owned memory by mistake.
492+
msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+2) // room for kex-strict and ext-info
493+
msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...)
494+
467495
isServer := len(t.hostKeys) > 0
468496
if isServer {
469497
for _, k := range t.hostKeys {
@@ -488,17 +516,24 @@ func (t *handshakeTransport) sendKexInit() error {
488516
msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat)
489517
}
490518
}
519+
520+
if t.sessionID == nil {
521+
msg.KexAlgos = append(msg.KexAlgos, kexStrictServer)
522+
}
491523
} else {
492524
msg.ServerHostKeyAlgos = t.hostKeyAlgorithms
493525

494526
// As a client we opt in to receiving SSH_MSG_EXT_INFO so we know what
495527
// algorithms the server supports for public key authentication. See RFC
496528
// 8308, Section 2.1.
529+
//
530+
// We also send the strict KEX mode extension algorithm, in order to opt
531+
// into the strict KEX mode.
497532
if firstKeyExchange := t.sessionID == nil; firstKeyExchange {
498-
msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+1)
499-
msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...)
500533
msg.KexAlgos = append(msg.KexAlgos, "ext-info-c")
534+
msg.KexAlgos = append(msg.KexAlgos, kexStrictClient)
501535
}
536+
502537
}
503538

504539
packet := Marshal(msg)
@@ -604,6 +639,13 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
604639
return err
605640
}
606641

642+
if t.sessionID == nil && ((isClient && contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && contains(clientInit.KexAlgos, kexStrictClient))) {
643+
t.strictMode = true
644+
if err := t.conn.setStrictMode(); err != nil {
645+
return err
646+
}
647+
}
648+
607649
// We don't send FirstKexFollows, but we handle receiving it.
608650
//
609651
// RFC 4253 section 7 defines the kex and the agreement method for
@@ -679,6 +721,12 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
679721
return unexpectedMessageError(msgNewKeys, packet[0])
680722
}
681723

724+
if firstKeyExchange {
725+
// Indicates to the transport that the first key exchange is completed
726+
// after receiving SSH_MSG_NEWKEYS.
727+
t.conn.setInitialKEXDone()
728+
}
729+
682730
return nil
683731
}
684732

0 commit comments

Comments
 (0)