1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
|
From 79df48025a3f6b08074ebe0549b73b4edeffb78c Mon Sep 17 00:00:00 2001
From: Simon Tatham <anakin@pobox.com>
Date: Wed, 29 Nov 2023 07:29:13 +0000
Subject: Support OpenSSH's new strict kex feature.
This is enabled via magic signalling keywords in the kex algorithms
list, similarly to ext-info-{c,s}. If both sides announce the
appropriate keyword, then this signals two changes to the standard SSH
protocol:
1. NEWKEYS resets packet sequence numbers: following any NEWKEYS, the
next packet sent in the same direction has sequence number zero.
2. No extraneous packets such as SSH_MSG_IGNORE are permitted during
the initial cleartext phase of the SSH protocol.
These two changes between them defeat the 'Terrapin' vulnerability,
aka CVE-2023-48795: a protocol-level exploit in which, for example, a
MITM injects a server-to-client SSH_MSG_IGNORE during the cleartext
phase, and deletes an initial segment of the server-to-client
encrypted data stream that it guesses is the right size to be the
server's SSH_MSG_EXT_INFO, so that both sides agree on the sequence
number of the _following_ server-to-client packet. In OpenSSH's
modified binary packet protocol modes this attack can go completely
undetected, and force a downgrade to (for example) SHA-1 based RSA.
(The ChaCha20/Poly1305 binary packet protocol is most vulnerable,
because it reinitialises the IV for each packet from scratch based on
the sequence number, so the keystream doesn't get out of sync.
Exploiting this in OpenSSH's ETM modes requires additional faff to
resync the keystream, and even then, the client likely sees a
corrupted SSH message at the start of the stream - but it will just
send SSH_MSG_UNIMPLEMENTED in response to that and proceed anyway. CBC
modes and standard AES SDCTR aren't vulnerable, because their MACs are
based on the plaintext rather than the ciphertext, so faking a correct
MAC on the corrupted packet requires the attacker to know what it
would decrypt to.)
Origin: upstream, https://git.tartarus.org/?p=simon/putty.git;a=commit;h=244be5412728a7334a2d457fbac4e0a2597165e5
Last-Update: 2023-12-18
Patch-Name: strict-kex.patch
---
ssh/bpp.h | 6 ++--
ssh/bpp2.c | 12 +++++--
ssh/transport2.c | 88 ++++++++++++++++++++++++++++++++++++++++++++----
ssh/transport2.h | 2 ++
4 files changed, 97 insertions(+), 11 deletions(-)
diff --git a/ssh/bpp.h b/ssh/bpp.h
index 87e7d7e7..23af5236 100644
--- a/ssh/bpp.h
+++ b/ssh/bpp.h
@@ -138,12 +138,14 @@ void ssh2_bpp_new_outgoing_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
- const ssh_compression_alg *compression, bool delayed_compression);
+ const ssh_compression_alg *compression, bool delayed_compression,
+ bool reset_sequence_number);
void ssh2_bpp_new_incoming_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
- const ssh_compression_alg *compression, bool delayed_compression);
+ const ssh_compression_alg *compression, bool delayed_compression,
+ bool reset_sequence_number);
/*
* A query method specific to the interface between ssh2transport and
diff --git a/ssh/bpp2.c b/ssh/bpp2.c
index e019dd2e..88003e82 100644
--- a/ssh/bpp2.c
+++ b/ssh/bpp2.c
@@ -106,7 +106,8 @@ void ssh2_bpp_new_outgoing_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
- const ssh_compression_alg *compression, bool delayed_compression)
+ const ssh_compression_alg *compression, bool delayed_compression,
+ bool reset_sequence_number)
{
struct ssh2_bpp_state *s;
assert(bpp->vt == &ssh2_bpp_vtable);
@@ -150,6 +151,9 @@ void ssh2_bpp_new_outgoing_crypto(
s->out.mac = NULL;
}
+ if (reset_sequence_number)
+ s->out.sequence = 0;
+
if (delayed_compression && !s->seen_userauth_success) {
s->out.pending_compression = compression;
s->out_comp = NULL;
@@ -174,7 +178,8 @@ void ssh2_bpp_new_incoming_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
- const ssh_compression_alg *compression, bool delayed_compression)
+ const ssh_compression_alg *compression, bool delayed_compression,
+ bool reset_sequence_number)
{
struct ssh2_bpp_state *s;
assert(bpp->vt == &ssh2_bpp_vtable);
@@ -231,6 +236,9 @@ void ssh2_bpp_new_incoming_crypto(
* start consuming the input data again. */
s->pending_newkeys = false;
+ if (reset_sequence_number)
+ s->in.sequence = 0;
+
/* And schedule a run of handle_input, in case there's already
* input data in the queue. */
queue_idempotent_callback(&s->bpp.ic_in_raw);
diff --git a/ssh/transport2.c b/ssh/transport2.c
index ec93dae0..e6229ec9 100644
--- a/ssh/transport2.c
+++ b/ssh/transport2.c
@@ -28,6 +28,10 @@ const static ssh2_macalg *const buggymacs[] = {
const static ptrlen ext_info_c = PTRLEN_DECL_LITERAL("ext-info-c");
const static ptrlen ext_info_s = PTRLEN_DECL_LITERAL("ext-info-s");
+const static ptrlen kex_strict_c =
+ PTRLEN_DECL_LITERAL("kex-strict-c-v00@openssh.com");
+const static ptrlen kex_strict_s =
+ PTRLEN_DECL_LITERAL("kex-strict-s-v00@openssh.com");
static ssh_compressor *ssh_comp_none_init(void)
{
@@ -462,6 +466,31 @@ static bool ssh2_transport_filter_queue(struct ssh2_transport_state *s)
{
PktIn *pktin;
+ if (!s->enabled_incoming_crypto) {
+ /*
+ * Record the fact that we've seen any non-KEXINIT packet at
+ * the head of our queue.
+ *
+ * This enables us to check later that the initial incoming
+ * KEXINIT was the very first packet, if scanning the KEXINITs
+ * turns out to enable strict-kex mode.
+ */
+ PktIn *pktin = pq_peek(s->ppl.in_pq);
+ if (pktin && pktin->type != SSH2_MSG_KEXINIT)
+ s->seen_non_kexinit = true;
+
+ if (s->strict_kex) {
+ /*
+ * Also, if we're already in strict-KEX mode and haven't
+ * turned on crypto yet, don't do any actual filtering.
+ * This ensures that extraneous packets _after_ the
+ * KEXINIT will go to the main coroutine, which will
+ * complain about them.
+ */
+ return false;
+ }
+ }
+
while (1) {
if (ssh2_common_filter_queue(&s->ppl))
return true;
@@ -937,10 +966,13 @@ static void ssh2_write_kexinit_lists(
add_to_commasep_pl(list, kexlists[i].algs[j].name);
}
if (i == KEXLIST_KEX && first_time) {
- if (our_hostkeys) /* we're the server */
+ if (our_hostkeys) { /* we're the server */
add_to_commasep_pl(list, ext_info_s);
- else /* we're the client */
+ add_to_commasep_pl(list, kex_strict_s);
+ } else { /* we're the client */
add_to_commasep_pl(list, ext_info_c);
+ add_to_commasep_pl(list, kex_strict_c);
+ }
}
put_stringsb(pktout, list);
}
@@ -971,7 +1003,7 @@ static bool ssh2_scan_kexinits(
bool *warn_kex, bool *warn_hk, bool *warn_cscipher, bool *warn_sccipher,
Ssh *ssh, bool *ignore_guess_cs_packet, bool *ignore_guess_sc_packet,
struct server_hostkeys *server_hostkeys, unsigned *hkflags,
- bool *can_send_ext_info)
+ bool *can_send_ext_info, bool first_time, bool *strict_kex)
{
BinarySource client[1], server[1];
int i;
@@ -1178,6 +1210,14 @@ static bool ssh2_scan_kexinits(
we_are_server ? ext_info_c : ext_info_s))
*can_send_ext_info = true;
+ /*
+ * Check whether the other side advertised support for kex-strict.
+ */
+ if (first_time && kexinit_keyword_found(
+ we_are_server ? clists[KEXLIST_KEX] : slists[KEXLIST_KEX],
+ we_are_server ? kex_strict_c : kex_strict_s))
+ *strict_kex = true;
+
if (server_hostkeys) {
/*
* Finally, make an auxiliary pass over the server's host key
@@ -1241,10 +1281,26 @@ static void filter_outgoing_kexinit(struct ssh2_transport_state *s)
strbuf_clear(out);
ptrlen olist = get_string(osrc), ilist = get_string(isrc);
for (ptrlen oword; get_commasep_word(&olist, &oword) ;) {
+ ptrlen searchword = oword;
ptrlen ilist_copy = ilist;
+
+ /*
+ * Special case: the kex_strict keywords are
+ * asymmetrically named, so if we're contemplating
+ * including one of them in our filtered KEXINIT, we
+ * should search the other side's KEXINIT for the _other_
+ * one, not the same one.
+ */
+ if (i == KEXLIST_KEX) {
+ if (ptrlen_eq_ptrlen(oword, kex_strict_c))
+ searchword = kex_strict_s;
+ else if (ptrlen_eq_ptrlen(oword, kex_strict_s))
+ searchword = kex_strict_c;
+ }
+
bool add = false;
for (ptrlen iword; get_commasep_word(&ilist_copy, &iword) ;) {
- if (ptrlen_eq_ptrlen(oword, iword)) {
+ if (ptrlen_eq_ptrlen(searchword, iword)) {
/* Found this word in the incoming list. */
add = true;
break;
@@ -1469,11 +1525,25 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
s->kexlists, &s->kex_alg, &s->hostkey_alg, s->cstrans,
s->sctrans, &s->warn_kex, &s->warn_hk, &s->warn_cscipher,
&s->warn_sccipher, s->ppl.ssh, NULL, &s->ignorepkt, &hks,
- &s->hkflags, &s->can_send_ext_info)) {
+ &s->hkflags, &s->can_send_ext_info, !s->got_session_id,
+ &s->strict_kex)) {
sfree(hks.indices);
return; /* false means a fatal error function was called */
}
+ /*
+ * If we've just turned on strict kex mode, say so, and
+ * retrospectively fault any pre-KEXINIT extraneous packets.
+ */
+ if (!s->got_session_id && s->strict_kex) {
+ ppl_logevent("Enabling strict key exchange semantics");
+ if (s->seen_non_kexinit) {
+ ssh_proto_error(s->ppl.ssh, "Received a packet before KEXINIT "
+ "in strict-kex mode");
+ return;
+ }
+ }
+
/*
* In addition to deciding which host key we're actually going
* to use, we should make a list of the host keys offered by
@@ -1666,7 +1736,9 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
s->ppl.bpp,
s->out.cipher, cipher_key->u, cipher_iv->u,
s->out.mac, s->out.etm_mode, mac_key->u,
- s->out.comp, s->out.comp_delayed);
+ s->out.comp, s->out.comp_delayed,
+ s->strict_kex);
+ s->enabled_outgoing_crypto = true;
strbuf_free(cipher_key);
strbuf_free(cipher_iv);
@@ -1758,7 +1830,9 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
s->ppl.bpp,
s->in.cipher, cipher_key->u, cipher_iv->u,
s->in.mac, s->in.etm_mode, mac_key->u,
- s->in.comp, s->in.comp_delayed);
+ s->in.comp, s->in.comp_delayed,
+ s->strict_kex);
+ s->enabled_incoming_crypto = true;
strbuf_free(cipher_key);
strbuf_free(cipher_iv);
diff --git a/ssh/transport2.h b/ssh/transport2.h
index 204573fb..1322cf5b 100644
--- a/ssh/transport2.h
+++ b/ssh/transport2.h
@@ -202,6 +202,8 @@ struct ssh2_transport_state {
bool warned_about_no_gss_transient_hostkey;
bool got_session_id;
bool can_send_ext_info, post_newkeys_ext_info;
+ bool strict_kex, enabled_outgoing_crypto, enabled_incoming_crypto;
+ bool seen_non_kexinit;
SeatPromptResult spr;
bool guessok;
bool ignorepkt;
|