untrusted comment: verify with openbsd-74-base.pub
RWRoyQmAD08ajThCI5YRDlPA+bc8InFbgRjdadmjzgOBFw6zk5VngSHkhiaYAp60ygXN6bCFoTqDL3THclcBYsIoKN2aWzrpjg8=

OpenBSD 7.4 errata 011, December 18, 2023:

An SSH protocol weakness (the Terrapin Attack) exists that allows an
on-path adversary to disable keystroke timing obfuscation.

Apply by doing:
   signify -Vep /etc/signify/openbsd-74-base.pub -x 011_ssh.patch.sig \
       -m - | (cd /usr/src && patch -p0)

And then rebuild and install OpenSSH:
   cd /usr/src/usr.bin/ssh
   make obj
   make
   make install

Index: usr.bin/ssh/PROTOCOL
===================================================================
RCS file: /cvs/src/usr.bin/ssh/PROTOCOL,v
diff -u -p -r1.49 PROTOCOL
--- usr.bin/ssh/PROTOCOL        28 Aug 2023 03:28:43 -0000      1.49
+++ usr.bin/ssh/PROTOCOL        13 Dec 2023 23:15:14 -0000
@@ -137,6 +137,32 @@ than as a named global or channel reques
short packet lengths, which would not be possible with other
approaches.

+1.9 transport: strict key exchange extension
+
+OpenSSH supports a number of transport-layer hardening measures under
+a "strict KEX" feature. This feature is signalled similarly to the
+RFC8308 ext-info feature: by including a additional algorithm in the
+initiial SSH2_MSG_KEXINIT kex_algorithms field. The client may append
+"[email protected]" to its kex_algorithms and the server
+may append "[email protected]". These pseudo-algorithms
+are only valid in the initial SSH2_MSG_KEXINIT and MUST be ignored
+if they are present in subsequent SSH2_MSG_KEXINIT packets.
+
+When an endpoint that supports this extension observes this algorithm
+name in a peer's KEXINIT packet, it MUST make the following changes to
+the the protocol:
+
+a) During initial KEX, terminate the connection if any unexpected or
+   out-of-sequence packet is received. This includes terminating the
+   connection if the first packet received is not SSH2_MSG_KEXINIT.
+   Unexpected packets for the purpose of strict KEX include messages
+   that are otherwise valid at any time during the connection such as
+   SSH2_MSG_DEBUG and SSH2_MSG_IGNORE.
+b) After sending or receiving a SSH2_MSG_NEWKEYS message, reset the
+   packet sequence number to zero. This behaviour persists for the
+   duration of the connection (i.e. not just the first
+   SSH2_MSG_NEWKEYS).
+
2. Connection protocol changes

2.1. connection: Channel write close extension "[email protected]"
Index: usr.bin/ssh/kex.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/kex.c,v
diff -u -p -r1.181 kex.c
--- usr.bin/ssh/kex.c   28 Aug 2023 03:28:43 -0000      1.181
+++ usr.bin/ssh/kex.c   13 Dec 2023 23:15:15 -0000
@@ -60,7 +60,7 @@
#include "xmalloc.h"

/* prototype */
-static int kex_choose_conf(struct ssh *);
+static int kex_choose_conf(struct ssh *, uint32_t seq);
static int kex_input_newkeys(int, u_int32_t, struct ssh *);

static const char * const proposal_names[PROPOSAL_MAX] = {
@@ -162,6 +162,18 @@ kex_names_valid(const char *names)
       return 1;
}

+/* returns non-zero if proposal contains any algorithm from algs */
+static int
+has_any_alg(const char *proposal, const char *algs)
+{
+       char *cp;
+
+       if ((cp = match_list(proposal, algs, NULL)) == NULL)
+               return 0;
+       free(cp);
+       return 1;
+}
+
/*
 * Concatenate algorithm names, avoiding duplicates in the process.
 * Caller must free returned string.
@@ -169,7 +181,7 @@ kex_names_valid(const char *names)
char *
kex_names_cat(const char *a, const char *b)
{
-       char *ret = NULL, *tmp = NULL, *cp, *p, *m;
+       char *ret = NULL, *tmp = NULL, *cp, *p;
       size_t len;

       if (a == NULL || *a == '\0')
@@ -186,10 +198,8 @@ kex_names_cat(const char *a, const char
       }
       strlcpy(ret, a, len);
       for ((p = strsep(&cp, ",")); p && *p != '\0'; (p = strsep(&cp, ","))) {
-               if ((m = match_list(ret, p, NULL)) != NULL) {
-                       free(m);
+               if (has_any_alg(ret, p))
                       continue; /* Algorithm already present */
-               }
               if (strlcat(ret, ",", len) >= len ||
                   strlcat(ret, p, len) >= len) {
                       free(tmp);
@@ -319,15 +329,23 @@ kex_proposal_populate_entries(struct ssh
       const char *defpropclient[PROPOSAL_MAX] = { KEX_CLIENT };
       const char **defprop = ssh->kex->server ? defpropserver : defpropclient;
       u_int i;
+       char *cp;

       if (prop == NULL)
               fatal_f("proposal missing");

+       /* Append EXT_INFO signalling to KexAlgorithms */
+       if (kexalgos == NULL)
+               kexalgos = defprop[PROPOSAL_KEX_ALGS];
+       if ((cp = kex_names_cat(kexalgos, ssh->kex->server ?
+           "[email protected]" :
+           "ext-info-c,[email protected]")) == NULL)
+               fatal_f("kex_names_cat");
+
       for (i = 0; i < PROPOSAL_MAX; i++) {
               switch(i) {
               case PROPOSAL_KEX_ALGS:
-                       prop[i] = compat_kex_proposal(ssh,
-                           kexalgos ? kexalgos : defprop[i]);
+                       prop[i] = compat_kex_proposal(ssh, cp);
                       break;
               case PROPOSAL_ENC_ALGS_CTOS:
               case PROPOSAL_ENC_ALGS_STOC:
@@ -348,6 +366,7 @@ kex_proposal_populate_entries(struct ssh
                       prop[i] = xstrdup(defprop[i]);
               }
       }
+       free(cp);
}

void
@@ -451,7 +470,12 @@ kex_protocol_error(int type, u_int32_t s
{
       int r;

-       error("kex protocol error: type %d seq %u", type, seq);
+       /* If in strict mode, any unexpected message is an error */
+       if ((ssh->kex->flags & KEX_INITIAL) && ssh->kex->kex_strict) {
+               ssh_packet_disconnect(ssh, "strict KEX violation: "
+                   "unexpected packet type %u (seqnr %u)", type, seq);
+       }
+       error_f("type %u seq %u", type, seq);
       if ((r = sshpkt_start(ssh, SSH2_MSG_UNIMPLEMENTED)) != 0 ||
           (r = sshpkt_put_u32(ssh, seq)) != 0 ||
           (r = sshpkt_send(ssh)) != 0)
@@ -548,7 +572,7 @@ kex_input_ext_info(int type, u_int32_t s
       if (ninfo >= 1024) {
               error("SSH2_MSG_EXT_INFO with too many entries, expected "
                   "<=1024, received %u", ninfo);
-               return SSH_ERR_INVALID_FORMAT;
+               return dispatch_protocol_error(type, seq, ssh);
       }
       for (i = 0; i < ninfo; i++) {
               if ((r = sshpkt_get_cstring(ssh, &name, NULL)) != 0)
@@ -666,7 +690,7 @@ kex_input_kexinit(int type, u_int32_t se
               error_f("no kex");
               return SSH_ERR_INTERNAL_ERROR;
       }
-       ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, NULL);
+       ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, &kex_protocol_error);
       ptr = sshpkt_ptr(ssh, &dlen);
       if ((r = sshbuf_put(kex->peer, ptr, dlen)) != 0)
               return r;
@@ -702,7 +726,7 @@ kex_input_kexinit(int type, u_int32_t se
       if (!(kex->flags & KEX_INIT_SENT))
               if ((r = kex_send_kexinit(ssh)) != 0)
                       return r;
-       if ((r = kex_choose_conf(ssh)) != 0)
+       if ((r = kex_choose_conf(ssh, seq)) != 0)
               return r;

       if (kex->kex_type < KEX_MAX && kex->kex[kex->kex_type] != NULL)
@@ -964,20 +988,14 @@ proposals_match(char *my[PROPOSAL_MAX],
       return (1);
}

-/* returns non-zero if proposal contains any algorithm from algs */
static int
-has_any_alg(const char *proposal, const char *algs)
+kexalgs_contains(char **peer, const char *ext)
{
-       char *cp;
-
-       if ((cp = match_list(proposal, algs, NULL)) == NULL)
-               return 0;
-       free(cp);
-       return 1;
+       return has_any_alg(peer[PROPOSAL_KEX_ALGS], ext);
}

static int
-kex_choose_conf(struct ssh *ssh)
+kex_choose_conf(struct ssh *ssh, uint32_t seq)
{
       struct kex *kex = ssh->kex;
       struct newkeys *newkeys;
@@ -1002,13 +1020,23 @@ kex_choose_conf(struct ssh *ssh)
               sprop=peer;
       }

-       /* Check whether client supports ext_info_c */
-       if (kex->server && (kex->flags & KEX_INITIAL)) {
-               char *ext;
-
-               ext = match_list("ext-info-c", peer[PROPOSAL_KEX_ALGS], NULL);
-               kex->ext_info_c = (ext != NULL);
-               free(ext);
+       /* Check whether peer supports ext_info/kex_strict */
+       if ((kex->flags & KEX_INITIAL) != 0) {
+               if (kex->server) {
+                       kex->ext_info_c = kexalgs_contains(peer, "ext-info-c");
+                       kex->kex_strict = kexalgs_contains(peer,
+                           "[email protected]");
+               } else {
+                       kex->kex_strict = kexalgs_contains(peer,
+                           "[email protected]");
+               }
+               if (kex->kex_strict) {
+                       debug3_f("will use strict KEX ordering");
+                       if (seq != 0)
+                               ssh_packet_disconnect(ssh,
+                                   "strict KEX violation: "
+                                   "KEXINIT was not the first packet");
+               }
       }

       /* Check whether client supports rsa-sha2 algorithms */
Index: usr.bin/ssh/kex.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/kex.h,v
diff -u -p -r1.119 kex.h
--- usr.bin/ssh/kex.h   28 Aug 2023 03:28:43 -0000      1.119
+++ usr.bin/ssh/kex.h   13 Dec 2023 23:15:15 -0000
@@ -142,6 +142,7 @@ struct kex {
       u_int   kex_type;
       char    *server_sig_algs;
       int     ext_info_c;
+       int     kex_strict;
       struct sshbuf *my;
       struct sshbuf *peer;
       struct sshbuf *client_version;
Index: usr.bin/ssh/packet.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/packet.c,v
diff -u -p -r1.312 packet.c
--- usr.bin/ssh/packet.c        28 Aug 2023 03:31:16 -0000      1.312
+++ usr.bin/ssh/packet.c        13 Dec 2023 23:15:15 -0000
@@ -1187,8 +1187,13 @@ ssh_packet_send2_wrapped(struct ssh *ssh
       sshbuf_dump(state->output, stderr);
#endif
       /* increment sequence number for outgoing packets */
-       if (++state->p_send.seqnr == 0)
+       if (++state->p_send.seqnr == 0) {
+               if ((ssh->kex->flags & KEX_INITIAL) != 0) {
+                       ssh_packet_disconnect(ssh, "outgoing sequence number "
+                           "wrapped during initial key exchange");
+               }
               logit("outgoing seqnr wraps around");
+       }
       if (++state->p_send.packets == 0)
               if (!(ssh->compat & SSH_BUG_NOREKEY))
                       return SSH_ERR_NEED_REKEY;
@@ -1196,6 +1201,11 @@ ssh_packet_send2_wrapped(struct ssh *ssh
       state->p_send.bytes += len;
       sshbuf_reset(state->outgoing_packet);

+       if (type == SSH2_MSG_NEWKEYS && ssh->kex->kex_strict) {
+               debug_f("resetting send seqnr %u", state->p_send.seqnr);
+               state->p_send.seqnr = 0;
+       }
+
       if (type == SSH2_MSG_NEWKEYS)
               r = ssh_set_newkeys(ssh, MODE_OUT);
       else if (type == SSH2_MSG_USERAUTH_SUCCESS && state->server_side)
@@ -1324,8 +1334,7 @@ ssh_packet_read_seqnr(struct ssh *ssh, u
       /* Stay in the loop until we have received a complete packet. */
       for (;;) {
               /* Try to read a packet from the buffer. */
-               r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p);
-               if (r != 0)
+               if ((r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p)) != 0)
                       break;
               /* If we got a packet, return it. */
               if (*typep != SSH_MSG_NONE)
@@ -1395,29 +1404,6 @@ ssh_packet_read(struct ssh *ssh)
       return type;
}

-/*
- * Waits until a packet has been received, verifies that its type matches
- * that given, and gives a fatal error and exits if there is a mismatch.
- */
-
-int
-ssh_packet_read_expect(struct ssh *ssh, u_int expected_type)
-{
-       int r;
-       u_char type;
-
-       if ((r = ssh_packet_read_seqnr(ssh, &type, NULL)) != 0)
-               return r;
-       if (type != expected_type) {
-               if ((r = sshpkt_disconnect(ssh,
-                   "Protocol error: expected packet type %d, got %d",
-                   expected_type, type)) != 0)
-                       return r;
-               return SSH_ERR_PROTOCOL_ERROR;
-       }
-       return 0;
-}
-
static int
ssh_packet_read_poll2_mux(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p)
{
@@ -1608,10 +1594,16 @@ ssh_packet_read_poll2(struct ssh *ssh, u
               if ((r = sshbuf_consume(state->input, mac->mac_len)) != 0)
                       goto out;
       }
+
       if (seqnr_p != NULL)
               *seqnr_p = state->p_read.seqnr;
-       if (++state->p_read.seqnr == 0)
+       if (++state->p_read.seqnr == 0) {
+               if ((ssh->kex->flags & KEX_INITIAL) != 0) {
+                       ssh_packet_disconnect(ssh, "incoming sequence number "
+                           "wrapped during initial key exchange");
+               }
               logit("incoming seqnr wraps around");
+       }
       if (++state->p_read.packets == 0)
               if (!(ssh->compat & SSH_BUG_NOREKEY))
                       return SSH_ERR_NEED_REKEY;
@@ -1677,6 +1669,10 @@ ssh_packet_read_poll2(struct ssh *ssh, u
#endif
       /* reset for next packet */
       state->packlen = 0;
+       if (*typep == SSH2_MSG_NEWKEYS && ssh->kex->kex_strict) {
+               debug_f("resetting read seqnr %u", state->p_read.seqnr);
+               state->p_read.seqnr = 0;
+       }

       if ((r = ssh_packet_check_rekey(ssh)) != 0)
               return r;
@@ -1699,10 +1695,39 @@ ssh_packet_read_poll_seqnr(struct ssh *s
               r = ssh_packet_read_poll2(ssh, typep, seqnr_p);
               if (r != 0)
                       return r;
-               if (*typep) {
-                       state->keep_alive_timeouts = 0;
-                       DBG(debug("received packet type %d", *typep));
+               if (*typep == 0) {
+                       /* no message ready */
+                       return 0;
               }
+               state->keep_alive_timeouts = 0;
+               DBG(debug("received packet type %d", *typep));
+
+               /* Always process disconnect messages */
+               if (*typep == SSH2_MSG_DISCONNECT) {
+                       if ((r = sshpkt_get_u32(ssh, &reason)) != 0 ||
+                           (r = sshpkt_get_string(ssh, &msg, NULL)) != 0)
+                               return r;
+                       /* Ignore normal client exit notifications */
+                       do_log2(ssh->state->server_side &&
+                           reason == SSH2_DISCONNECT_BY_APPLICATION ?
+                           SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR,
+                           "Received disconnect from %s port %d:"
+                           "%u: %.400s", ssh_remote_ipaddr(ssh),
+                           ssh_remote_port(ssh), reason, msg);
+                       free(msg);
+                       return SSH_ERR_DISCONNECTED;
+               }
+
+               /*
+                * Do not implicitly handle any messages here during initial
+                * KEX when in strict mode. They will be need to be allowed
+                * explicitly by the KEX dispatch table or they will generate
+                * protocol errors.
+                */
+               if (ssh->kex != NULL &&
+                   (ssh->kex->flags & KEX_INITIAL) && ssh->kex->kex_strict)
+                       return 0;
+               /* Implicitly handle transport-level messages */
               switch (*typep) {
               case SSH2_MSG_IGNORE:
                       debug3("Received SSH2_MSG_IGNORE");
@@ -1717,19 +1742,6 @@ ssh_packet_read_poll_seqnr(struct ssh *s
                       debug("Remote: %.900s", msg);
                       free(msg);
                       break;
-               case SSH2_MSG_DISCONNECT:
-                       if ((r = sshpkt_get_u32(ssh, &reason)) != 0 ||
-                           (r = sshpkt_get_string(ssh, &msg, NULL)) != 0)
-                               return r;
-                       /* Ignore normal client exit notifications */
-                       do_log2(ssh->state->server_side &&
-                           reason == SSH2_DISCONNECT_BY_APPLICATION ?
-                           SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR,
-                           "Received disconnect from %s port %d:"
-                           "%u: %.400s", ssh_remote_ipaddr(ssh),
-                           ssh_remote_port(ssh), reason, msg);
-                       free(msg);
-                       return SSH_ERR_DISCONNECTED;
               case SSH2_MSG_UNIMPLEMENTED:
                       if ((r = sshpkt_get_u32(ssh, &seqnr)) != 0)
                               return r;
@@ -2219,6 +2231,7 @@ kex_to_blob(struct sshbuf *m, struct kex
           (r = sshbuf_put_u32(m, kex->hostkey_type)) != 0 ||
           (r = sshbuf_put_u32(m, kex->hostkey_nid)) != 0 ||
           (r = sshbuf_put_u32(m, kex->kex_type)) != 0 ||
+           (r = sshbuf_put_u32(m, kex->kex_strict)) != 0 ||
           (r = sshbuf_put_stringb(m, kex->my)) != 0 ||
           (r = sshbuf_put_stringb(m, kex->peer)) != 0 ||
           (r = sshbuf_put_stringb(m, kex->client_version)) != 0 ||
@@ -2381,6 +2394,7 @@ kex_from_blob(struct sshbuf *m, struct k
           (r = sshbuf_get_u32(m, (u_int *)&kex->hostkey_type)) != 0 ||
           (r = sshbuf_get_u32(m, (u_int *)&kex->hostkey_nid)) != 0 ||
           (r = sshbuf_get_u32(m, &kex->kex_type)) != 0 ||
+           (r = sshbuf_get_u32(m, &kex->kex_strict)) != 0 ||
           (r = sshbuf_get_stringb(m, kex->my)) != 0 ||
           (r = sshbuf_get_stringb(m, kex->peer)) != 0 ||
           (r = sshbuf_get_stringb(m, kex->client_version)) != 0 ||
@@ -2705,6 +2719,7 @@ sshpkt_disconnect(struct ssh *ssh, const
       vsnprintf(buf, sizeof(buf), fmt, args);
       va_end(args);

+       debug2_f("sending SSH2_MSG_DISCONNECT: %s", buf);
       if ((r = sshpkt_start(ssh, SSH2_MSG_DISCONNECT)) != 0 ||
           (r = sshpkt_put_u32(ssh, SSH2_DISCONNECT_PROTOCOL_ERROR)) != 0 ||
           (r = sshpkt_put_cstring(ssh, buf)) != 0 ||
Index: usr.bin/ssh/packet.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/packet.h,v
diff -u -p -r1.95 packet.h
--- usr.bin/ssh/packet.h        28 Aug 2023 03:31:16 -0000      1.95
+++ usr.bin/ssh/packet.h        13 Dec 2023 23:15:15 -0000
@@ -118,7 +118,6 @@ int  ssh_packet_send2_wrapped(struct ssh
int     ssh_packet_send2(struct ssh *);

int      ssh_packet_read(struct ssh *);
-int     ssh_packet_read_expect(struct ssh *, u_int type);
int      ssh_packet_read_poll(struct ssh *);
int ssh_packet_read_poll2(struct ssh *, u_char *, u_int32_t *seqnr_p);
int     ssh_packet_process_incoming(struct ssh *, const char *buf, u_int len);
Index: usr.bin/ssh/sshconnect2.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/sshconnect2.c,v
diff -u -p -r1.367 sshconnect2.c
--- usr.bin/ssh/sshconnect2.c   1 Aug 2023 08:15:04 -0000       1.367
+++ usr.bin/ssh/sshconnect2.c   13 Dec 2023 23:15:15 -0000
@@ -351,7 +351,6 @@ struct cauthmethod {
};

static int input_userauth_service_accept(int, u_int32_t, struct ssh *);
-static int input_userauth_ext_info(int, u_int32_t, struct ssh *);
static int input_userauth_success(int, u_int32_t, struct ssh *);
static int input_userauth_failure(int, u_int32_t, struct ssh *);
static int input_userauth_banner(int, u_int32_t, struct ssh *);
@@ -465,7 +464,7 @@ ssh_userauth2(struct ssh *ssh, const cha

       ssh->authctxt = &authctxt;
       ssh_dispatch_init(ssh, &input_userauth_error);
-       ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, &input_userauth_ext_info);
+       ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, kex_input_ext_info);
       ssh_dispatch_set(ssh, SSH2_MSG_SERVICE_ACCEPT, &input_userauth_service_accept);
       ssh_dispatch_run_fatal(ssh, DISPATCH_BLOCK, &authctxt.success); /* loop until success */
       pubkey_cleanup(ssh);
@@ -516,12 +515,6 @@ input_userauth_service_accept(int type,
       return r;
}

-static int
-input_userauth_ext_info(int type, u_int32_t seqnr, struct ssh *ssh)
-{
-       return kex_input_ext_info(type, seqnr, ssh);
-}
-
void
userauth(struct ssh *ssh, char *authlist)
{
@@ -600,6 +593,7 @@ input_userauth_success(int type, u_int32
       free(authctxt->methoddata);
       authctxt->methoddata = NULL;
       authctxt->success = 1;                  /* break out */
+       ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, dispatch_protocol_error);
       return 0;
}