Skip to content

Commit

Permalink
ssh: implement strict KEX protocol changes
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
rolandshoemaker committed Dec 18, 2023
1 parent 4e5a261 commit 9d2ee97
Show file tree
Hide file tree
Showing 3 changed files with 388 additions and 9 deletions.
56 changes: 52 additions & 4 deletions ssh/handshake.go
Expand Up @@ -35,6 +35,16 @@ type keyingTransport interface {
// direction will be effected if a msgNewKeys message is sent
// or received.
prepareKeyChange(*algorithms, *kexResult) error

// setStrictMode sets the strict KEX mode, notably triggering
// sequence number resets on sending or receiving msgNewKeys.
// If the sequence number is already > 1 when setStrictMode
// is called, an error is returned.
setStrictMode() error

// setInitialKEXDone indicates to the transport that the initial key exchange
// was completed
setInitialKEXDone()
}

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

// The session ID or nil if first kex did not complete yet.
sessionID []byte

// strictMode indicates if the other side of the handshake indicated
// that we should be following the strict KEX protocol restrictions.
strictMode bool
}

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

const (
kexStrictClient = "kex-strict-c-v00@openssh.com"
kexStrictServer = "kex-strict-s-v00@openssh.com"
)

// sendKexInit sends a key change message.
func (t *handshakeTransport) sendKexInit() error {
t.mu.Lock()
Expand All @@ -454,7 +476,6 @@ func (t *handshakeTransport) sendKexInit() error {
}

msg := &kexInitMsg{
KexAlgos: t.config.KeyExchanges,
CiphersClientServer: t.config.Ciphers,
CiphersServerClient: t.config.Ciphers,
MACsClientServer: t.config.MACs,
Expand All @@ -464,6 +485,13 @@ func (t *handshakeTransport) sendKexInit() error {
}
io.ReadFull(rand.Reader, msg.Cookie[:])

// We mutate the KexAlgos slice, in order to add the kex-strict extension algorithm,
// and possibly to add the ext-info extension algorithm. Since the slice may be the
// user owned KeyExchanges, we create our own slice in order to avoid using user
// owned memory by mistake.
msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+2) // room for kex-strict and ext-info
msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...)

isServer := len(t.hostKeys) > 0
if isServer {
for _, k := range t.hostKeys {
Expand All @@ -488,17 +516,24 @@ func (t *handshakeTransport) sendKexInit() error {
msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat)
}
}

if t.sessionID == nil {
msg.KexAlgos = append(msg.KexAlgos, kexStrictServer)
}
} else {
msg.ServerHostKeyAlgos = t.hostKeyAlgorithms

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

}

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

if t.sessionID == nil && ((isClient && contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && contains(clientInit.KexAlgos, kexStrictClient))) {
t.strictMode = true
if err := t.conn.setStrictMode(); err != nil {
return err
}
}

// We don't send FirstKexFollows, but we handle receiving it.
//
// RFC 4253 section 7 defines the kex and the agreement method for
Expand Down Expand Up @@ -679,6 +721,12 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
return unexpectedMessageError(msgNewKeys, packet[0])
}

if firstKeyExchange {
// Indicates to the transport that the first key exchange is completed
// after receiving SSH_MSG_NEWKEYS.
t.conn.setInitialKEXDone()
}

return nil
}

Expand Down

0 comments on commit 9d2ee97

Please sign in to comment.