File: strict-kex.patch

package info (click to toggle)
putty 0.78-2%2Bdeb12u2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 13,964 kB
  • sloc: ansic: 137,777; python: 7,775; perl: 1,798; makefile: 133; sh: 111
file content (291 lines) | stat: -rw-r--r-- 12,377 bytes parent folder | download
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;