Browse Source

Merge branch 'thirdparty_dev' into dev

Source commit: 70dad0f1516d5eba6d33587b07ece8d3aac4babc
Martin Prikryl 1 year ago
parent
commit
a65773c31c

+ 14 - 11
source/putty/crypto/aes-select.c

@@ -42,7 +42,7 @@ static ssh_cipher *aes_select(const ssh_cipheralg *alg)
 #define IF_NEON(...)
 #endif
 
-#define AES_SELECTOR_VTABLE_IMPL(mode_c, id, mode_display, bits, required_mac) /*WINSCP*/       \
+#define AES_SELECTOR_VTABLE_IMPL(mode_c, id, mode_display, bits, required_mac, flags) /*WINSCP*/       \
     static const ssh_cipheralg *                                        \
     ssh_aes ## bits ## _ ## mode_c ## _impls[] = {                      \
         IF_NI(&ssh_aes ## bits ## _ ## mode_c ## _ni,)                  \
@@ -58,7 +58,7 @@ static ssh_cipher *aes_select(const ssh_cipheralg *alg)
         /*.blksize =*/ 16,                                                  \
         /*.real_keybits =*/ bits,                                           \
         /*.padded_keybytes =*/ bits/8,                                      \
-        0, \
+        flags, \
         /*.text_name =*/ "AES-" #bits " " mode_display                      \
         " (dummy selector vtable)",                                     \
         required_mac, /*WINSCP*/                                                        \
@@ -66,25 +66,28 @@ static ssh_cipher *aes_select(const ssh_cipheralg *alg)
     }
 
 // WINSCP
-#define AES_SELECTOR_VTABLE(mode_c, id, mode_display, bits, dummy) AES_SELECTOR_VTABLE_IMPL(mode_c, id, mode_display, bits, NULL)
+#define AES_SELECTOR_VTABLE(mode_c, id, mode_display, bits, dummy) AES_SELECTOR_VTABLE_IMPL(mode_c, id, mode_display, bits, NULL, 0)
 
-AES_SELECTOR_VTABLE(cbc, "aes128-cbc", "CBC", 128, );
-AES_SELECTOR_VTABLE(cbc, "aes192-cbc", "CBC", 192, );
-AES_SELECTOR_VTABLE(cbc, "aes256-cbc", "CBC", 256, );
+AES_SELECTOR_VTABLE_IMPL(cbc, "aes128-cbc", "CBC", 128, NULL, /*.flags =*/ SSH_CIPHER_IS_CBC);
+AES_SELECTOR_VTABLE_IMPL(cbc, "aes192-cbc", "CBC", 192, NULL, /*.flags =*/ SSH_CIPHER_IS_CBC);
+AES_SELECTOR_VTABLE_IMPL(cbc, "aes256-cbc", "CBC", 256, NULL, /*.flags =*/ SSH_CIPHER_IS_CBC);
 AES_SELECTOR_VTABLE(sdctr, "aes128-ctr", "SDCTR", 128, );
 AES_SELECTOR_VTABLE(sdctr, "aes192-ctr", "SDCTR", 192, );
 AES_SELECTOR_VTABLE(sdctr, "aes256-ctr", "SDCTR", 256, );
 AES_SELECTOR_VTABLE_IMPL(gcm, "[email protected]", "GCM", 128,
-                    /*.required_mac =*/ &ssh2_aesgcm_mac);
+                    /*.required_mac =*/ &ssh2_aesgcm_mac,
+                    /*.flags =*/ SSH_CIPHER_SEPARATE_LENGTH);
 AES_SELECTOR_VTABLE_IMPL(gcm, "[email protected]", "GCM", 256,
-                    /*.required_mac =*/ &ssh2_aesgcm_mac);
+                    /*.required_mac =*/ &ssh2_aesgcm_mac,
+                    /*.flags =*/ SSH_CIPHER_SEPARATE_LENGTH);
 
 /* 192-bit AES-GCM is included only so that testcrypt can run standard
  * test vectors against it. OpenSSH doesn't define a protocol id for
  * it. Hence setting its ssh2_id to NULL here, and more importantly,
  * leaving it out of aesgcm_list[] below. */
-AES_SELECTOR_VTABLE(gcm, NULL, "GCM", 192,
-                    .required_mac = &ssh2_aesgcm_mac);
+AES_SELECTOR_VTABLE_IMPL(gcm, NULL, "GCM", 192,
+                    /*.required_mac =*/ &ssh2_aesgcm_mac,
+                    /*.flags =*/ SSH_CIPHER_SEPARATE_LENGTH);
 
 static const ssh_cipheralg ssh_rijndael_lysator = {
     /* Same as aes256_cbc, but with a different protocol ID */
@@ -94,7 +97,7 @@ static const ssh_cipheralg ssh_rijndael_lysator = {
     /*.blksize =*/ 16,
     /*.real_keybits =*/ 256,
     /*.padded_keybytes =*/ 256/8,
-    0,
+    /*.flags =*/ SSH_CIPHER_IS_CBC,
     /*.text_name =*/ "AES-256 CBC (dummy selector vtable)",
     NULL,
     /*.extra =*/ ssh_aes256_cbc_impls,

+ 2 - 2
source/putty/proxy/socks4.c

@@ -95,8 +95,8 @@ static void proxy_socks4_process_queue(ProxyNegotiator *pn)
 
         if (data[0] != SOCKS4_REPLY_VERSION) {
             pn->error = dupprintf("SOCKS proxy response contained reply "
-                                  "version number %d (expected 0)",
-                                  (int)data[0]);
+                                  "version number %d (expected %d)",
+                                  (int)data[0], SOCKS4_REPLY_VERSION);
             crStopV;
         }
 

+ 2 - 2
source/putty/proxy/socks5.c

@@ -356,7 +356,7 @@ static void proxy_socks5_process_queue(ProxyNegotiator *pn)
                             "SOCKS 5 CHAP authentication failed");
                         crStopV;
                     }
-                } else if (s->chap_attr==SOCKS5_AUTH_CHAP_ATTR_CHALLENGE) {
+                } else if (s->chap_attr == SOCKS5_AUTH_CHAP_ATTR_CHALLENGE) {
                     /* The CHAP challenge string. Send the response */
                     strbuf *response = chap_response(
                         make_ptrlen(s->chap_buf, s->chap_attr_len),
@@ -390,7 +390,7 @@ static void proxy_socks5_process_queue(ProxyNegotiator *pn)
      *   byte[]    address, with variable size (see below)
      *   uint16    port
      */
-    put_byte(pn->output, SOCKS5_REPLY_VERSION);
+    put_byte(pn->output, SOCKS5_REQUEST_VERSION);
     put_byte(pn->output, SOCKS_CMD_CONNECT);
     put_byte(pn->output, 0);   /* reserved byte */
 

+ 12 - 12
source/putty/putty.h

@@ -1294,7 +1294,7 @@ struct SeatVtable {
      * confirm_ssh_host_key above.
      */
     SeatPromptResult (*confirm_weak_crypto_primitive)(
-        Seat *seat, const char *algtype, const char *algname,
+        Seat *seat, SeatDialogText *text,
         void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 
     /*
@@ -1305,11 +1305,10 @@ struct SeatVtable {
      * This form is used in the case where we're using a host key
      * below the warning threshold because that's the best one we have
      * cached, but at least one host key algorithm *above* the
-     * threshold is available that we don't have cached. 'betteralgs'
-     * lists the better algorithm(s).
+     * threshold is available that we don't have cached.
      */
     SeatPromptResult (*confirm_weak_cached_hostkey)(
-        Seat *seat, const char *algname, const char *betteralgs,
+        Seat *seat, SeatDialogText *text,
         void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 
     /*
@@ -1447,15 +1446,15 @@ static inline SeatPromptResult seat_confirm_ssh_host_key(
         iseat.seat, h, p, ktyp, kstr, text, helpctx, cb, ctx,
         fingerprints, is_certificate, ca_count, already_verified); } // WINSCP
 static inline SeatPromptResult seat_confirm_weak_crypto_primitive(
-    InteractionReadySeat iseat, const char *atyp, const char *aname,
+    InteractionReadySeat iseat, SeatDialogText *text,
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
 { return iseat.seat->vt->confirm_weak_crypto_primitive(
-        iseat.seat, atyp, aname, cb, ctx); }
+        iseat.seat, text, cb, ctx); }
 static inline SeatPromptResult seat_confirm_weak_cached_hostkey(
-    InteractionReadySeat iseat, const char *aname, const char *better,
+    InteractionReadySeat iseat, SeatDialogText *text,
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
 { return iseat.seat->vt->confirm_weak_cached_hostkey(
-        iseat.seat, aname, better, cb, ctx); }
+        iseat.seat, text, cb, ctx); }
 static inline const SeatDialogPromptDescriptions *seat_prompt_descriptions(
     Seat *seat)
 { return seat->vt->prompt_descriptions(seat); }
@@ -1508,6 +1507,7 @@ struct SeatDialogPromptDescriptions {
     const char *hk_accept_action;
     const char *hk_connect_once_action;
     const char *hk_cancel_action, *hk_cancel_action_Participle;
+    const char *weak_accept_action, *weak_cancel_action;
 };
 
 /* In the utils subdir: print a message to the Seat which can't be
@@ -1541,10 +1541,10 @@ SeatPromptResult nullseat_confirm_ssh_host_key(
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
     char **fingerprints, bool is_certificate, int ca_count, bool already_verified); // WINSCP
 SeatPromptResult nullseat_confirm_weak_crypto_primitive(
-    Seat *seat, const char *algtype, const char *algname,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 SeatPromptResult nullseat_confirm_weak_cached_hostkey(
-    Seat *seat, const char *algname, const char *betteralgs,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat);
 bool nullseat_is_never_utf8(Seat *seat);
@@ -1578,10 +1578,10 @@ SeatPromptResult console_confirm_ssh_host_key(
     char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 SeatPromptResult console_confirm_weak_crypto_primitive(
-    Seat *seat, const char *algtype, const char *algname,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 SeatPromptResult console_confirm_weak_cached_hostkey(
-    Seat *seat, const char *algname, const char *betteralgs,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 StripCtrlChars *console_stripctrl_new(
     Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);

+ 1 - 1
source/putty/settings.c

@@ -146,7 +146,7 @@ static void gpps(settings_r *sesskey, const char *name, const char *def,
  * format of a Filename or FontSpec is platform-dependent. So the
  * platform-dependent functions MUST return some sort of value.
  */
-static void gppfont(settings_r *sesskey, char *name,
+static void gppfont(settings_r *sesskey, const char *name,
                     Conf *conf, int primary)
 {
     FontSpec *result = read_setting_fontspec(sesskey, name);

+ 15 - 0
source/putty/ssh.h

@@ -1950,11 +1950,26 @@ void add_to_commasep(strbuf *buf, const char *data);
 void add_to_commasep_pl(strbuf *buf, ptrlen data);
 bool get_commasep_word(ptrlen *list, ptrlen *word);
 
+/* Reasons why something warned by confirm_weak_crypto_primitive might
+ * be considered weak */
+typedef enum WeakCryptoReason {
+    WCR_BELOW_THRESHOLD, /* user has told us to consider it weak */
+    WCR_TERRAPIN,        /* known vulnerability CVE-2023-48795 */
+    WCR_TERRAPIN_AVOIDABLE, /* same, but demoting ChaCha20 can avoid it */
+} WeakCryptoReason;
+
 SeatPromptResult verify_ssh_host_key(
     InteractionReadySeat iseat, Conf *conf, const char *host, int port,
     ssh_key *key, const char *keytype, char *keystr, const char *keydisp,
     char **fingerprints, int ca_count,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+SeatPromptResult confirm_weak_crypto_primitive(
+    InteractionReadySeat iseat, const char *algtype, const char *algname,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
+    WeakCryptoReason wcr);
+SeatPromptResult confirm_weak_cached_hostkey(
+    InteractionReadySeat iseat, const char *algname, const char **betteralgs,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 
 typedef struct ssh_transient_hostkey_cache ssh_transient_hostkey_cache;
 ssh_transient_hostkey_cache *ssh_transient_hostkey_cache_new(void);

+ 4 - 2
source/putty/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

+ 10 - 2
source/putty/ssh/bpp2.c

@@ -107,7 +107,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);
@@ -151,6 +152,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;
@@ -175,7 +179,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);
@@ -232,6 +237,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);

+ 104 - 0
source/putty/ssh/common_p.c

@@ -1130,6 +1130,110 @@ SeatPromptResult verify_ssh_host_key(
     } // WINSCP
 }
 
+SeatPromptResult confirm_weak_crypto_primitive(
+    InteractionReadySeat iseat, const char *algtype, const char *algname,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
+    WeakCryptoReason wcr)
+{
+    SeatDialogText *text = seat_dialog_text_new();
+    const SeatDialogPromptDescriptions *pds =
+        seat_prompt_descriptions(iseat.seat);
+
+    seat_dialog_text_append(text, SDT_TITLE, "%s Security Alert", appname);
+
+    switch (wcr) {
+      case WCR_BELOW_THRESHOLD:
+        seat_dialog_text_append(
+            text, SDT_PARA,
+            "The first %s supported by the server is %s, "
+            "which is below the configured warning threshold.",
+            algtype, algname);
+        break;
+      case WCR_TERRAPIN:
+      case WCR_TERRAPIN_AVOIDABLE:
+        seat_dialog_text_append(
+            text, SDT_PARA,
+            "The %s selected for this session is %s, "
+            "which, with this server, is vulnerable to the 'Terrapin' attack "
+            "CVE-2023-48795, potentially allowing an attacker to modify "
+            "the encrypted session.",
+            algtype, algname);
+        seat_dialog_text_append(
+            text, SDT_PARA,
+            "Upgrading, patching, or reconfiguring this SSH server is the "
+            "best way to avoid this vulnerability, if possible.");
+        if (wcr == WCR_TERRAPIN_AVOIDABLE) {
+            seat_dialog_text_append(
+                text, SDT_PARA,
+                "You can also avoid this vulnerability by abandoning "
+                "this connection, moving ChaCha20 to below the "
+                "'warn below here' line in PuTTY's SSH cipher "
+                "configuration (so that an algorithm without the "
+                "vulnerability will be selected), and starting a new "
+                "connection.");
+        }
+        break;
+      default:
+        unreachable("bad WeakCryptoReason");
+    }
+
+    /* In batch mode, we print the above information and then this
+     * abort message, and stop. */
+    seat_dialog_text_append(text, SDT_BATCH_ABORT, "Connection abandoned.");
+
+    seat_dialog_text_append(
+        text, SDT_PARA, "To accept the risk and continue, %s. "
+        "To abandon the connection, %s.",
+        pds->weak_accept_action, pds->weak_cancel_action);
+
+    seat_dialog_text_append(text, SDT_PROMPT, "Continue with connection?");
+
+    SeatPromptResult toret = seat_confirm_weak_crypto_primitive(
+        iseat, text, callback, ctx);
+    seat_dialog_text_free(text);
+    return toret;
+}
+
+SeatPromptResult confirm_weak_cached_hostkey(
+    InteractionReadySeat iseat, const char *algname, const char **betteralgs,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
+{
+    SeatDialogText *text = seat_dialog_text_new();
+    const SeatDialogPromptDescriptions *pds =
+        seat_prompt_descriptions(iseat.seat);
+
+    seat_dialog_text_append(text, SDT_TITLE, "%s Security Alert", appname);
+
+    seat_dialog_text_append(
+        text, SDT_PARA,
+        "The first host key type we have stored for this server "
+        "is %s, which is below the configured warning threshold.", algname);
+
+    seat_dialog_text_append(
+        text, SDT_PARA,
+        "The server also provides the following types of host key "
+        "above the threshold, which we do not have stored:");
+
+    for (const char **p = betteralgs; *p; p++)
+        seat_dialog_text_append(text, SDT_DISPLAY, "%s", *p);
+
+    /* In batch mode, we print the above information and then this
+     * abort message, and stop. */
+    seat_dialog_text_append(text, SDT_BATCH_ABORT, "Connection abandoned.");
+
+    seat_dialog_text_append(
+        text, SDT_PARA, "To accept the risk and continue, %s. "
+        "To abandon the connection, %s.",
+        pds->weak_accept_action, pds->weak_cancel_action);
+
+    seat_dialog_text_append(text, SDT_PROMPT, "Continue with connection?");
+
+    SeatPromptResult toret = seat_confirm_weak_cached_hostkey(
+        iseat, text, callback, ctx);
+    seat_dialog_text_free(text);
+    return toret;
+}
+
 /* ----------------------------------------------------------------------
  * Common functions shared between SSH-1 layers.
  */

+ 393 - 59
source/putty/ssh/transport2.c

@@ -29,6 +29,18 @@ const static ssh2_macalg *const buggymacs[] = {
     &ssh_hmac_sha1_buggy, &ssh_hmac_sha1_96_buggy, &ssh_hmac_md5
 };
 
+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("[email protected]");
+const static ptrlen kex_strict_s =
+    PTRLEN_DECL_LITERAL("[email protected]");
+
+/* Pointer value to store in s->weak_algorithms_consented_to to
+ * indicate that the user has accepted the risk of the Terrapin
+ * attack */
+static const char terrapin_weakness[1];
+
 static ssh_compressor *ssh_comp_none_init(void)
 {
     return NULL;
@@ -84,6 +96,9 @@ static void ssh2_transport_set_max_data_size(struct ssh2_transport_state *s);
 static unsigned long sanitise_rekey_time(int rekey_time, unsigned long def);
 static void ssh2_transport_higher_layer_packet_callback(void *context);
 static void ssh2_transport_final_output(PacketProtocolLayer *ppl);
+static const char *terrapin_vulnerable(
+    bool strict_kex, const transport_direction *d);
+static bool try_to_avoid_terrapin(const struct ssh2_transport_state *s);
 static unsigned int ssh2_transport_winscp_query(PacketProtocolLayer *ppl, int query);
 
 static const PacketProtocolLayerVtable ssh2_transport_vtable = {
@@ -108,7 +123,7 @@ static bool ssh2_transport_timer_update(struct ssh2_transport_state *s,
                                         unsigned long rekey_time);
 static SeatPromptResult ssh2_transport_confirm_weak_crypto_primitive(
     struct ssh2_transport_state *s, const char *type, const char *name,
-    const void *alg);
+    const void *alg, WeakCryptoReason wcr);
 
 static const char *const kexlist_descr[NKEXLIST] = {
     "key exchange algorithm",
@@ -476,6 +491,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;
@@ -960,10 +1000,13 @@ static void ssh2_write_kexinit_lists(
         }
         #endif
         if (i == KEXLIST_KEX && first_time) {
-            if (our_hostkeys)          /* we're the server */
-                add_to_commasep(list, "ext-info-s");
-            else                       /* we're the client */
-                add_to_commasep(list, "ext-info-c");
+            if (our_hostkeys) {        /* we're the server */
+                add_to_commasep_pl(list, ext_info_s);
+                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);
     }
@@ -978,15 +1021,37 @@ struct server_hostkeys {
     size_t n, size;
 };
 
-static bool ssh2_scan_kexinits(
-    ptrlen client_kexinit, ptrlen server_kexinit,
+static bool kexinit_keyword_found(ptrlen list, ptrlen keyword)
+{
+    for (ptrlen word; get_commasep_word(&list, &word) ;)
+        if (ptrlen_eq_ptrlen(word, keyword))
+            return true;
+    return false;
+}
+
+typedef struct ScanKexinitsResult {
+    bool success;
+
+    /* only if success is false */
+    enum {
+        SKR_INCOMPLETE,
+        SKR_UNKNOWN_ID,
+        SKR_NO_AGREEMENT,
+    } error;
+
+    const char *kind; /* what kind of thing did we fail to sort out? */
+    ptrlen desc;      /* and what was it? or what was the available list? */
+} ScanKexinitsResult;
+
+static ScanKexinitsResult ssh2_scan_kexinits(
+    ptrlen client_kexinit, ptrlen server_kexinit, bool we_are_server,
     struct kexinit_algorithm_list kexlists[NKEXLIST],
     const ssh_kex **kex_alg, const ssh_keyalg **hostkey_alg,
     transport_direction *cs, transport_direction *sc,
     bool *warn_kex, bool *warn_hk, bool *warn_cscipher, bool *warn_sccipher,
-    Ssh *ssh, bool *ignore_guess_cs_packet, bool *ignore_guess_sc_packet,
+    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;
@@ -1013,11 +1078,10 @@ static bool ssh2_scan_kexinits(
         clists[i] = get_string(client);
         slists[i] = get_string(server);
         if (get_err(client) || get_err(server)) {
-            /* Report a better error than the spurious "Couldn't
-             * agree" that we'd generate if we pressed on regardless
-             * and treated the empty get_string() result as genuine */
-            ssh_proto_error(ssh, "KEXINIT packet was incomplete");
-            return false;
+            ScanKexinitsResult skr = {
+                .success = false, .error = SKR_INCOMPLETE,
+            };
+            return skr;
         }
 
         for (cfirst = true, clist = clists[i];
@@ -1065,10 +1129,11 @@ static bool ssh2_scan_kexinits(
              * produce a reasonably useful message instead of an
              * assertion failure.
              */
-            ssh_sw_abort(ssh, "Selected %s \"%.*s\" does not correspond to "
-                         "any supported algorithm",
-                         kexlist_descr[i], PTRLEN_PRINTF(found));
-            return false;
+            ScanKexinitsResult skr = {
+                .success = false, .error = SKR_UNKNOWN_ID,
+                .kind = kexlist_descr[i], .desc = found,
+            };
+            return skr;
         }
 
         /*
@@ -1123,9 +1188,11 @@ static bool ssh2_scan_kexinits(
             /*
              * Otherwise, any match failure _is_ a fatal error.
              */
-            ssh_sw_abort(ssh, "Couldn't agree a %s (available: %.*s)",
-                         kexlist_descr[i], PTRLEN_PRINTF(slists[i]));
-            return false;
+            ScanKexinitsResult skr = {
+                .success = false, .error = SKR_UNKNOWN_ID,
+                .kind = kexlist_descr[i], .desc = slists[i],
+            };
+            return skr;
         }
 
         switch (i) {
@@ -1188,17 +1255,18 @@ static bool ssh2_scan_kexinits(
     /*
      * Check whether the other side advertised support for EXT_INFO.
      */
-    {
-        ptrlen extinfo_advert =
-            (server_hostkeys ? PTRLEN_LITERAL("ext-info-c") :
-             PTRLEN_LITERAL("ext-info-s"));
-        ptrlen list = (server_hostkeys ? clists[KEXLIST_KEX] :
-                       slists[KEXLIST_KEX]);
-        ptrlen word; // WINSCP
-        for (; get_commasep_word(&list, &word) ;)
-            if (ptrlen_eq_ptrlen(word, extinfo_advert))
-                *can_send_ext_info = true;
-    }
+    if (kexinit_keyword_found(
+            we_are_server ? clists[KEXLIST_KEX] : slists[KEXLIST_KEX],
+            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) {
         /*
@@ -1223,7 +1291,33 @@ static bool ssh2_scan_kexinits(
         } // WINSCP
     }
 
-    return true;
+    ScanKexinitsResult skr = { .success = true };
+    return skr;
+}
+
+static void ssh2_report_scan_kexinits_error(Ssh *ssh, ScanKexinitsResult skr)
+{
+    assert(!skr.success);
+
+    switch (skr.error) {
+      case SKR_INCOMPLETE:
+        /* Report a better error than the spurious "Couldn't
+         * agree" that we'd generate if we pressed on regardless
+         * and treated the empty get_string() result as genuine */
+        ssh_proto_error(ssh, "KEXINIT packet was incomplete");
+        break;
+      case SKR_UNKNOWN_ID:
+        ssh_sw_abort(ssh, "Selected %s \"%.*s\" does not correspond to "
+                     "any supported algorithm",
+                     skr.kind, PTRLEN_PRINTF(skr.desc));
+        break;
+      case SKR_NO_AGREEMENT:
+        ssh_sw_abort(ssh, "Couldn't agree a %s (available: %.*s)",
+                     skr.kind, PTRLEN_PRINTF(skr.desc));
+        break;
+      default:
+        unreachable("bad ScanKexinitsResult");
+    }
 }
 
 static inline bool delay_outgoing_kexinit(struct ssh2_transport_state *s)
@@ -1270,11 +1364,27 @@ static void filter_outgoing_kexinit(struct ssh2_transport_state *s)
         ptrlen olist = get_string(osrc), ilist = get_string(isrc);
         ptrlen oword; // WINSCP
         for (; 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;
             ptrlen iword; // WINSCP
             for (; 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;
@@ -1495,15 +1605,32 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
     {
         struct server_hostkeys hks = { NULL, 0, 0 };
 
-        if (!ssh2_scan_kexinits(
+        ScanKexinitsResult skr = ssh2_scan_kexinits(
                 ptrlen_from_strbuf(s->client_kexinit),
-                ptrlen_from_strbuf(s->server_kexinit),
+                ptrlen_from_strbuf(s->server_kexinit), s->ssc != NULL,
                 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->warn_sccipher, NULL, &s->ignorepkt, &hks,
+                &s->hkflags, &s->can_send_ext_info, !s->got_session_id,
+                &s->strict_kex);
+
+        if (!skr.success) {
             sfree(hks.indices);
-            return; /* false means a fatal error function was called */
+            ssh2_report_scan_kexinits_error(s->ppl.ssh, skr);
+            return; /* we just called a fatal error function */
+        }
+
+        /*
+         * 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;
+            }
         }
 
         /*
@@ -1536,7 +1663,8 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
 
     if (s->warn_kex) {
         s->spr = ssh2_transport_confirm_weak_crypto_primitive(
-            s, "key-exchange algorithm", s->kex_alg->name, s->kex_alg);
+            s, "key-exchange algorithm", s->kex_alg->name, s->kex_alg,
+            WCR_BELOW_THRESHOLD);
         crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
         if (spr_is_abort(s->spr)) {
             ssh_spr_close(s->ppl.ssh, s->spr, "kex warning");
@@ -1546,7 +1674,8 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
 
     if (s->warn_hk) {
         int j, k;
-        char *betteralgs;
+        const char **betteralgs = NULL;
+        size_t nbetter = 0, bettersize = 0;
 
         /*
          * Change warning box wording depending on why we chose a
@@ -1555,7 +1684,6 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
          * could usefully cross-certify. Otherwise, use the same
          * standard wording as any other weak crypto primitive.
          */
-        betteralgs = NULL;
         for (j = 0; j < s->n_uncert_hostkeys; j++) {
             const struct ssh_signkey_with_user_pref_id *hktype =
                 &ssh2_hostkey_algs[s->uncert_hostkeys[j]];
@@ -1570,19 +1698,16 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
                 }
             }
             if (better) {
-                if (betteralgs) {
-                    char *old_ba = betteralgs;
-                    betteralgs = dupcat(betteralgs, ",", hktype->alg->ssh_id);
-                    sfree(old_ba);
-                } else {
-                    betteralgs = dupstr(hktype->alg->ssh_id);
-                }
+                sgrowarray(betteralgs, bettersize, nbetter);
+                betteralgs[nbetter++] = hktype->alg->ssh_id;
             }
         }
         if (betteralgs) {
             /* Use the special warning prompt that lets us provide
              * a list of better algorithms */
-            s->spr = seat_confirm_weak_cached_hostkey(
+            sgrowarray(betteralgs, bettersize, nbetter);
+            betteralgs[nbetter] = NULL;
+            s->spr = confirm_weak_cached_hostkey(
                 ppl_get_iseat(&s->ppl), s->hostkey_alg->ssh_id, betteralgs,
                 ssh2_transport_dialog_callback, s);
             sfree(betteralgs);
@@ -1591,7 +1716,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
              * warning prompt */
             s->spr = ssh2_transport_confirm_weak_crypto_primitive(
                 s, "host key type", s->hostkey_alg->ssh_id,
-                s->hostkey_alg);
+                s->hostkey_alg, WCR_BELOW_THRESHOLD);
         }
         crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
         if (spr_is_abort(s->spr)) {
@@ -1603,7 +1728,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
     if (s->warn_cscipher) {
         s->spr = ssh2_transport_confirm_weak_crypto_primitive(
             s, "client-to-server cipher", s->out.cipher->ssh2_id,
-            s->out.cipher);
+            s->out.cipher, WCR_BELOW_THRESHOLD);
         crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
         if (spr_is_abort(s->spr)) {
             ssh_spr_close(s->ppl.ssh, s->spr, "cipher warning");
@@ -1614,7 +1739,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
     if (s->warn_sccipher) {
         s->spr = ssh2_transport_confirm_weak_crypto_primitive(
             s, "server-to-client cipher", s->in.cipher->ssh2_id,
-            s->in.cipher);
+            s->in.cipher, WCR_BELOW_THRESHOLD);
         crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
         if (spr_is_abort(s->spr)) {
             ssh_spr_close(s->ppl.ssh, s->spr, "cipher warning");
@@ -1622,6 +1747,46 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
         }
     }
 
+    {
+        s->terrapin.csvuln = terrapin_vulnerable(s->strict_kex, s->cstrans);
+        s->terrapin.scvuln = terrapin_vulnerable(s->strict_kex, s->sctrans);
+        s->terrapin.wcr = WCR_TERRAPIN;
+
+        if (s->terrapin.csvuln || s->terrapin.scvuln) {
+            ppl_logevent("SSH connection is vulnerable to 'Terrapin' attack "
+                         "(CVE-2023-48795)");
+            if (try_to_avoid_terrapin(s))
+                s->terrapin.wcr = WCR_TERRAPIN_AVOIDABLE;
+        }
+
+        if (s->terrapin.csvuln) {
+            s->spr = ssh2_transport_confirm_weak_crypto_primitive(
+                s, "client-to-server cipher", s->terrapin.csvuln,
+                terrapin_weakness, s->terrapin.wcr);
+            crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
+            if (spr_is_abort(s->spr)) {
+                ssh_spr_close(s->ppl.ssh, s->spr, "vulnerability warning");
+                return;
+            }
+        }
+
+        if (s->terrapin.scvuln) {
+            s->spr = ssh2_transport_confirm_weak_crypto_primitive(
+                s, "server-to-client cipher", s->terrapin.scvuln,
+                terrapin_weakness, s->terrapin.wcr);
+            crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
+            if (spr_is_abort(s->spr)) {
+                ssh_spr_close(s->ppl.ssh, s->spr, "vulnerability warning");
+                return;
+            }
+        }
+
+        if (s->terrapin.csvuln || s->terrapin.scvuln) {
+            ppl_logevent("Continuing despite 'Terrapin' vulnerability, "
+                         "at user request");
+        }
+    }
+
     /*
      * If the other side has sent an initial key exchange packet that
      * we must treat as a wrong guess, wait for it, and discard it.
@@ -1704,7 +1869,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);
@@ -1799,7 +1966,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);
@@ -2426,20 +2595,21 @@ static int ca_blob_compare(void *av, void *bv)
 }
 
 /*
- * Wrapper on seat_confirm_weak_crypto_primitive(), which uses the
+ * Wrapper on confirm_weak_crypto_primitive(), which uses the
  * tree234 s->weak_algorithms_consented_to to ensure we ask at most
  * once about any given crypto primitive.
  */
 static SeatPromptResult ssh2_transport_confirm_weak_crypto_primitive(
     struct ssh2_transport_state *s, const char *type, const char *name,
-    const void *alg)
+    const void *alg, WeakCryptoReason wcr)
 {
     if (find234(s->weak_algorithms_consented_to, (void *)alg, NULL))
         return SPR_OK;
     add234(s->weak_algorithms_consented_to, (void *)alg);
 
-    return seat_confirm_weak_crypto_primitive(
-        ppl_get_iseat(&s->ppl), type, name, ssh2_transport_dialog_callback, s);
+    return confirm_weak_crypto_primitive(
+        ppl_get_iseat(&s->ppl), type, name, ssh2_transport_dialog_callback,
+        s, wcr);
 }
 
 static size_t ssh2_transport_queued_data_size(PacketProtocolLayer *ppl)
@@ -2459,6 +2629,170 @@ static void ssh2_transport_final_output(PacketProtocolLayer *ppl)
     ssh_ppl_final_output(s->higher_layer);
 }
 
+/* Check the settings for a transport direction to see if they're
+ * vulnerable to the Terrapin attack, aka CVE-2023-48795. If so,
+ * return a string describing the vulnerable thing. */
+static const char *terrapin_vulnerable(
+    bool strict_kex, const transport_direction *d)
+{
+    /*
+     * Strict kex mode eliminates the vulnerability. (That's what it's
+     * for.)
+     */
+    if (strict_kex)
+        return NULL;
+
+    /*
+     * ChaCha20-Poly1305 is vulnerable and perfectly exploitable.
+     */
+    if (d->cipher == &ssh2_chacha20_poly1305)
+        return "ChaCha20-Poly1305";
+
+    /*
+     * CBC-mode ciphers with OpenSSH's ETM modification are vulnerable
+     * and probabilistically exploitable.
+     */
+    if (d->etm_mode && (d->cipher->flags & SSH_CIPHER_IS_CBC))
+        return "a CBC-mode cipher in OpenSSH ETM mode";
+
+    return NULL;
+}
+
+/*
+ * Called when we've detected that at least one transport direction
+ * has the Terrapin vulnerability.
+ *
+ * Before we report it, try to replay what would have happened if the
+ * user had reconfigured their cipher settings to demote
+ * ChaCha20+Poly1305 to below the warning threshold. If that would
+ * have avoided the vulnerability, we should say so in the dialog box.
+ *
+ * This is basically the only change in PuTTY's configuration that has
+ * a chance of avoiding the problem. Terrapin affects the modified
+ * binary packet protocol used with ChaCha20+Poly1305, and also
+ * CBC-mode ciphers in ETM mode. But PuTTY unconditionally offers the
+ * ETM mode of each MAC _after_ the non-ETM mode. So the latter case
+ * can only come up if the server has been configured to _only_ permit
+ * the ETM modes of those MACs, which means there's nothing we can do
+ * anyway.
+ */
+static bool try_to_avoid_terrapin(const struct ssh2_transport_state *s)
+{
+    bool avoidable = false;
+
+    strbuf *alt_client_kexinit = strbuf_new();
+    Conf *alt_conf = conf_copy(s->conf);
+    struct kexinit_algorithm_list alt_kexlists[NKEXLIST];
+    memset(alt_kexlists, 0, sizeof(alt_kexlists));
+
+    /*
+     * We only bother doing this if we're the client, because Uppity
+     * can't present a dialog box anyway.
+     */
+    if (s->ssc)
+        goto out;
+
+    /*
+     * Demote CIPHER_CHACHA20 to just below CIPHER_WARN, if it was
+     * previously above it. If not, don't do anything - we don't want
+     * to _promote_ it.
+     */
+    int ccp_pos_now = -1, ccp_pos_wanted = -1;
+    for (int i = 0; i < CIPHER_MAX; i++) {
+        switch (conf_get_int_int(alt_conf, CONF_ssh_cipherlist,
+                                 i)) {
+          case CIPHER_CHACHA20:
+            ccp_pos_now = i;
+            break;
+          case CIPHER_WARN:
+            ccp_pos_wanted = i;
+            break;
+        }
+    }
+    if (ccp_pos_now < 0 || ccp_pos_wanted < 0)
+        goto out; /* shouldn't ever happen: didn't find the two entries */
+    if (ccp_pos_now >= ccp_pos_wanted)
+        goto out; /* ChaCha20 is already demoted and it didn't help */
+    while (ccp_pos_now < ccp_pos_wanted) {
+        int cnext = conf_get_int_int(alt_conf, CONF_ssh_cipherlist,
+                                     ccp_pos_now + 1);
+        conf_set_int_int(alt_conf, CONF_ssh_cipherlist,
+                         ccp_pos_now, cnext);
+        ccp_pos_now++;
+    }
+    conf_set_int_int(alt_conf, CONF_ssh_cipherlist,
+                     ccp_pos_now + 1, CIPHER_CHACHA20);
+
+    /*
+     * Make the outgoing KEXINIT we would have made using this
+     * configuration.
+     */
+    put_byte(alt_client_kexinit, SSH2_MSG_KEXINIT);
+    put_padding(alt_client_kexinit, 16, 'x'); /* fake random padding */
+    ssh2_write_kexinit_lists(
+        BinarySink_UPCAST(alt_client_kexinit), alt_kexlists, alt_conf,
+        s->ssc, s->ppl.remote_bugs, s->savedhost, s->savedport, s->hostkey_alg,
+        s->thc, s->host_cas, s->hostkeys, s->nhostkeys, !s->got_session_id,
+        s->can_gssapi_keyex,
+        s->gss_kex_used && !s->need_gss_transient_hostkey);
+    put_bool(alt_client_kexinit, false); /* guess packet follows */
+    put_uint32(alt_client_kexinit, 0);   /* reserved */
+
+    /*
+     * Re-analyse the incoming KEXINIT with respect to this one, to
+     * see what we'd have decided on.
+     */
+    transport_direction cstrans, sctrans;
+    bool warn_kex, warn_hk, warn_cscipher, warn_sccipher;
+    bool can_send_ext_info = false, strict_kex = false;
+    unsigned hkflags;
+    const ssh_kex *kex_alg;
+    const ssh_keyalg *hostkey_alg;
+
+    ScanKexinitsResult skr = ssh2_scan_kexinits(
+        ptrlen_from_strbuf(alt_client_kexinit),
+        ptrlen_from_strbuf(s->server_kexinit),
+        s->ssc != NULL, alt_kexlists, &kex_alg, &hostkey_alg,
+        &cstrans, &sctrans,
+        &warn_kex, &warn_hk, &warn_cscipher, &warn_sccipher, NULL, NULL, NULL,
+        &hkflags, &can_send_ext_info, !s->got_session_id, &strict_kex);
+    if (!skr.success) /* something else would have gone wrong */
+        goto out;
+
+    /*
+     * Reject this as an alternative solution if any of the warn flags
+     * has got worse, or if there's still anything
+     * Terrapin-vulnerable.
+     */
+    if (warn_kex > s->warn_kex)
+        goto out;
+    if (warn_hk > s->warn_hk)
+        goto out;
+    if (warn_cscipher > s->warn_cscipher)
+        goto out;
+    if (warn_sccipher > s->warn_sccipher)
+        goto out;
+    if (terrapin_vulnerable(strict_kex, &cstrans))
+        goto out;
+    if (terrapin_vulnerable(strict_kex, &sctrans))
+        goto out;
+
+    /*
+     * Success! The vulnerability could have been avoided by this Conf
+     * tweak, and we should tell the user so.
+     */
+    avoidable = true;
+
+  out:
+
+    for (size_t i = 0; i < NKEXLIST; i++)
+        sfree(alt_kexlists[i].algs);
+    strbuf_free(alt_client_kexinit);
+    conf_free(alt_conf);
+
+    return avoidable;
+}
+
 #include "puttyexp.h"
 
 static unsigned int ssh2_transport_winscp_query(PacketProtocolLayer *ppl, int query)

+ 6 - 0
source/putty/ssh/transport2.h

@@ -181,6 +181,10 @@ struct ssh2_transport_state {
 
     int nbits, pbits;
     bool warn_kex, warn_hk, warn_cscipher, warn_sccipher;
+    struct {
+        const char *csvuln, *scvuln;
+        WeakCryptoReason wcr;
+    } terrapin;
     mp_int *p, *g, *e, *f;
     strbuf *ebuf, *fbuf;
     strbuf *kex_shared_secret;
@@ -203,6 +207,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;

+ 4 - 2
source/putty/stubs/null-seat.c

@@ -27,11 +27,11 @@ SeatPromptResult nullseat_confirm_ssh_host_key(
     char **fingerprints, bool is_certificate, int ca_count, bool already_verified) // WINSCP
 { return SPR_SW_ABORT("this seat can't handle interactive prompts"); }
 SeatPromptResult nullseat_confirm_weak_crypto_primitive(
-    Seat *seat, const char *algtype, const char *algname,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 { return SPR_SW_ABORT("this seat can't handle interactive prompts"); }
 SeatPromptResult nullseat_confirm_weak_cached_hostkey(
-    Seat *seat, const char *algname, const char *betteralgs,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 { return SPR_SW_ABORT("this seat can't handle interactive prompts"); }
 bool nullseat_is_never_utf8(Seat *seat) { return false; }
@@ -61,6 +61,8 @@ const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat)
         /*.hk_connect_once_action =*/ "",
         /*.hk_cancel_action =*/ "",
         /*.hk_cancel_action_Participle =*/ "",
+        .weak_accept_action = "",
+        .weak_cancel_action = "",
     };
     return &descs;
 }

+ 2 - 2
source/putty/utils/tempseat.c

@@ -256,7 +256,7 @@ static SeatPromptResult tempseat_confirm_ssh_host_key(
 }
 
 static SeatPromptResult tempseat_confirm_weak_crypto_primitive(
-    Seat *seat, const char *algtype, const char *algname,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 {
     unreachable("confirm_weak_crypto_primitive "
@@ -264,7 +264,7 @@ static SeatPromptResult tempseat_confirm_weak_crypto_primitive(
 }
 
 static SeatPromptResult tempseat_confirm_weak_cached_hostkey(
-    Seat *seat, const char *algname, const char *betteralgs,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 {
     unreachable("confirm_weak_cached_hostkey "

+ 4 - 4
source/putty/version.h

@@ -1,5 +1,5 @@
 /* Generated by automated build script */
-#define RELEASE 0.79
-#define TEXTVER "Release 0.79"
-#define SSHVER "-Release-0.79"
-#define BINARY_VERSION 0,79,0,0
+#define RELEASE 0.80
+#define TEXTVER "Release 0.80"
+#define SSHVER "-Release-0.80"
+#define BINARY_VERSION 0,80,0,0

+ 9 - 5
source/putty/windows/platform.h

@@ -254,10 +254,10 @@ SeatPromptResult win_seat_confirm_ssh_host_key(
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
     char **fingerprints, bool is_certificate, int ca_count, bool already_verified); // WINSCP
 SeatPromptResult win_seat_confirm_weak_crypto_primitive(
-    Seat *seat, const char *algtype, const char *algname,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 SeatPromptResult win_seat_confirm_weak_cached_hostkey(
-    Seat *seat, const char *algname, const char *betteralgs,
+    Seat *seat, SeatDialogText *text,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 const SeatDialogPromptDescriptions *win_seat_prompt_descriptions(Seat *seat);
 
@@ -752,10 +752,14 @@ char *get_jumplist_registry_entries(void);
 
 /* In utils */
 int reg_override_winscp(void);
-HKEY open_regkey_fn(bool create, HKEY base, const char *path, ...);
+HKEY open_regkey_fn(bool create, bool write, HKEY base, const char *path, ...);
 HKEY open_regkey_fn_winscp(bool create, HKEY base, const char *path, ...);
-#define open_regkey(create, base, ...) \
-    reg_override_winscp() ? open_regkey_fn_winscp(create, base, __VA_ARGS__, (const char *)NULL) : open_regkey_fn(create, base, __VA_ARGS__, (const char *)NULL)
+#define open_regkey_ro(base, ...) \
+    open_regkey_fn(false, false, base, __VA_ARGS__, (const char *)NULL)
+#define open_regkey_rw(base, ...) \
+    open_regkey_fn(false, true, base, __VA_ARGS__, (const char *)NULL)
+#define create_regkey(base, ...) \
+    open_regkey_fn(true, true, base, __VA_ARGS__, (const char *)NULL)
 void close_regkey(HKEY key);
 void close_regkey_winscp(HKEY key);
 void del_regkey(HKEY key, const char *name);

+ 19 - 20
source/putty/windows/storage.c

@@ -44,7 +44,7 @@ settings_w *open_settings_w(const char *sessionname, char **errmsg)
     escape_registry_key(sessionname, sb);
 
     { // WINSCP
-    HKEY sesskey = open_regkey(true, HKEY_CURRENT_USER, puttystr, sb->s);
+    HKEY sesskey = create_regkey(HKEY_CURRENT_USER, puttystr, sb->s);
     if (!sesskey) {
         *errmsg = dupprintf("Unable to create registry key\n"
                             "HKEY_CURRENT_USER\\%s\\%s", puttystr, sb->s);
@@ -93,7 +93,7 @@ settings_r *open_settings_r(const char *sessionname)
     strbuf *sb = strbuf_new();
     escape_registry_key(sessionname, sb);
     { // WINSCP
-    HKEY sesskey = open_regkey(false, HKEY_CURRENT_USER, puttystr, sb->s);
+    HKEY sesskey = open_regkey_ro(HKEY_CURRENT_USER, puttystr, sb->s);
     strbuf_free(sb);
 
     if (!sesskey)
@@ -209,7 +209,7 @@ void close_settings_r(settings_r *handle)
 #ifndef WINSCP
 void del_settings(const char *sessionname)
 {
-    HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, puttystr);
+    HKEY rkey = open_regkey_rw(HKEY_CURRENT_USER, puttystr);
     if (!rkey)
         return;
 
@@ -232,7 +232,7 @@ struct settings_e {
 
 settings_e *enum_settings_start(void)
 {
-    HKEY key = open_regkey(false, HKEY_CURRENT_USER, puttystr);
+    HKEY key = open_regkey_ro(HKEY_CURRENT_USER, puttystr);
     if (!key)
         return NULL;
 
@@ -288,8 +288,8 @@ int check_stored_host_key(const char *hostname, int port,
     hostkey_regname(regname, hostname, port, keytype);
 
     { // WINSCP
-    HKEY rkey = open_regkey(false, HKEY_CURRENT_USER,
-                            PUTTY_REG_POS "\\SshHostKeys");
+    HKEY rkey = open_regkey_ro(HKEY_CURRENT_USER,
+                               PUTTY_REG_POS "\\SshHostKeys");
     if (!rkey) {
         strbuf_free(regname);
         return 1;                      /* key does not exist in registry */
@@ -407,8 +407,8 @@ void store_host_key(const char *hostname, int port,
     hostkey_regname(regname, hostname, port, keytype);
 
     { // WINSCP
-    HKEY rkey = open_regkey(true, HKEY_CURRENT_USER,
-                            PUTTY_REG_POS "\\SshHostKeys");
+    HKEY rkey = create_regkey(HKEY_CURRENT_USER,
+                              PUTTY_REG_POS "\\SshHostKeys");
     if (rkey) {
         put_reg_sz(rkey, regname->s, key);
         close_regkey(rkey);
@@ -429,7 +429,7 @@ host_ca_enum *enum_host_ca_start(void)
     host_ca_enum *e;
     HKEY key;
 
-    if (!(key = open_regkey(false, HKEY_CURRENT_USER, host_ca_key)))
+    if (!(key = open_regkey_ro(HKEY_CURRENT_USER, host_ca_key)))
         return NULL;
 
     e = snew(host_ca_enum);
@@ -465,7 +465,7 @@ host_ca *host_ca_load(const char *name)
     sb = strbuf_new();
     escape_registry_key(name, sb);
     { // WINSCP
-    HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, host_ca_key, sb->s);
+    HKEY rkey = open_regkey_ro(HKEY_CURRENT_USER, host_ca_key, sb->s);
     strbuf_free(sb);
 
     if (!rkey)
@@ -522,7 +522,7 @@ char *host_ca_save(host_ca *hca)
     strbuf *sb = strbuf_new();
     escape_registry_key(hca->name, sb);
     { // WINSCP
-    HKEY rkey = open_regkey(true, HKEY_CURRENT_USER, host_ca_key, sb->s);
+    HKEY rkey = create_regkey(HKEY_CURRENT_USER, host_ca_key, sb->s);
     if (!rkey) {
         char *err = dupprintf("Unable to create registry key\n"
                               "HKEY_CURRENT_USER\\%s\\%s", host_ca_key, sb->s);
@@ -557,7 +557,7 @@ char *host_ca_save(host_ca *hca)
 
 char *host_ca_delete(const char *name)
 {
-    HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, host_ca_key);
+    HKEY rkey = open_regkey_rw(HKEY_CURRENT_USER, host_ca_key);
     if (!rkey)
         return NULL;
 
@@ -626,7 +626,7 @@ static HANDLE access_random_seed(int action)
      * Registry, if any.
      */
     {
-        HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, PUTTY_REG_POS);
+        HKEY rkey = open_regkey_ro(HKEY_CURRENT_USER, PUTTY_REG_POS);
         if (rkey) {
             char *regpath = get_reg_sz(rkey, "RandSeedFile");
             close_regkey(rkey);
@@ -754,7 +754,7 @@ void write_random_seed(void *data, int len)
 static int transform_jumplist_registry(
     const char *add, const char *rem, char **out)
 {
-    HKEY rkey = open_regkey(true, HKEY_CURRENT_USER, reg_jumplist_key);
+    HKEY rkey = create_regkey(HKEY_CURRENT_USER, reg_jumplist_key);
     if (!rkey)
         return JUMPLISTREG_ERROR_KEYOPENCREATE_FAILURE;
 
@@ -857,7 +857,7 @@ static void registry_recursive_remove(HKEY key)
 
     DWORD i = 0;
     while ((name = enum_regkey(key, i)) != NULL) {
-        HKEY subkey = open_regkey(false, key, name);
+        HKEY subkey = open_regkey_rw(key, name);
         if (subkey) {
             registry_recursive_remove(subkey);
             close_regkey(subkey);
@@ -889,7 +889,7 @@ void cleanup_all(void)
      * Open the main PuTTY registry key and remove everything in it.
      */
     { // WINSCP
-    HKEY key = open_regkey(false, HKEY_CURRENT_USER, PUTTY_REG_POS);
+    HKEY key = open_regkey_rw(HKEY_CURRENT_USER, PUTTY_REG_POS);
     if (key) {
         registry_recursive_remove(key);
         close_regkey(key);
@@ -899,8 +899,7 @@ void cleanup_all(void)
      * we've done that, see if the parent key has any other
      * children.
      */
-    if ((key = open_regkey(false, HKEY_CURRENT_USER,
-                           PUTTY_REG_PARENT)) != NULL) {
+    if ((key = open_regkey_rw(HKEY_CURRENT_USER, PUTTY_REG_PARENT)) != NULL) {
         del_regkey(key, PUTTY_REG_PARENT_CHILD);
         { // WINSCP
         char *name = enum_regkey(key, 0);
@@ -914,8 +913,8 @@ void cleanup_all(void)
         if (name) {
             sfree(name);
         } else {
-            if ((key = open_regkey(false, HKEY_CURRENT_USER,
-                                   PUTTY_REG_GPARENT)) != NULL) {
+            if ((key = open_regkey_rw(HKEY_CURRENT_USER,
+                                      PUTTY_REG_GPARENT)) != NULL) {
                 del_regkey(key, PUTTY_REG_GPARENT_CHILD);
                 close_regkey(key);
             }

+ 5 - 5
source/putty/windows/utils/registry.c

@@ -5,7 +5,7 @@
 
 #include "putty.h"
 
-HKEY open_regkey_fn(bool create, HKEY hk, const char *path, ...)
+HKEY open_regkey_fn(bool create, bool write, HKEY hk, const char *path, ...)
 {
     HKEY toret = NULL;
     bool hk_needs_close = false;
@@ -16,14 +16,14 @@ HKEY open_regkey_fn(bool create, HKEY hk, const char *path, ...)
     for (; path; path = va_arg(ap, const char *)) {
         HKEY hk_sub = NULL;
 
+        DWORD access = KEY_READ | (write ? KEY_WRITE : 0);
         LONG status;
         if (create)
             status = RegCreateKeyEx(
                 hk, path, 0, NULL, REG_OPTION_NON_VOLATILE,
-                KEY_READ | KEY_WRITE, NULL, &hk_sub, NULL);
+                access, NULL, &hk_sub, NULL);
         else
-            status = RegOpenKeyEx(
-                hk, path, 0, KEY_READ | KEY_WRITE, &hk_sub);
+            status = RegOpenKeyEx(hk, path, 0, access, &hk_sub);
 
         if (status != ERROR_SUCCESS)
             goto out;
@@ -206,7 +206,7 @@ bool put_reg_multi_sz(HKEY key, const char *name, strbuf *str)
 
 char *get_reg_sz_simple(HKEY key, const char *name, const char *leaf)
 {
-    HKEY subkey = open_regkey(false, key, name);
+    HKEY subkey = open_regkey_ro(key, name);
     if (!subkey)
         return NULL;
     char *toret = get_reg_sz(subkey, leaf);