From 140abfad5538b9660e9339d31213702f6c0b832b Mon Sep 17 00:00:00 2001
From: Simon Tatham <anakin@pobox.com>
Date: Wed, 29 Nov 2023 08:50:45 +0000
Subject: Warn about Terrapin vulnerability for unpatched servers.

If the KEXINIT exchange results in a vulnerable cipher mode, we now
give a warning, similar to the 'we selected a crypto primitive below
the warning threshold' one. But there's nothing we can do about it at
that point other than let the user abort the connection.

Origin: backport, https://git.tartarus.org/?p=simon/putty.git;a=commit;h=0b00e4ce26d89cd010e31e66fd02ac77cb982367
Last-Update: 2023-12-18

Patch-Name: terrapin-warning.patch
---
 ssh.h            | 10 +++++-
 ssh/common.c     | 32 +++++++++++++----
 ssh/login1.c     |  2 +-
 ssh/transport2.c | 90 ++++++++++++++++++++++++++++++++++++++++++++----
 ssh/transport2.h |  4 +++
 5 files changed, 123 insertions(+), 15 deletions(-)

diff --git a/ssh.h b/ssh.h
index 3b9df7d2..b7290bc5 100644
--- a/ssh.h
+++ b/ssh.h
@@ -1901,6 +1901,13 @@ 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 */
+} 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,
@@ -1908,7 +1915,8 @@ SeatPromptResult verify_ssh_host_key(
     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);
+    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);
diff --git a/ssh/common.c b/ssh/common.c
index af534e3b..9ccf078b 100644
--- a/ssh/common.c
+++ b/ssh/common.c
@@ -1077,7 +1077,8 @@ SeatPromptResult verify_ssh_host_key(
 
 SeatPromptResult confirm_weak_crypto_primitive(
     InteractionReadySeat iseat, const char *algtype, const char *algname,
-    void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
+    WeakCryptoReason wcr)
 {
     SeatDialogText *text = seat_dialog_text_new();
     const SeatDialogPromptDescriptions *pds =
@@ -1085,11 +1086,30 @@ SeatPromptResult confirm_weak_crypto_primitive(
 
     seat_dialog_text_append(text, SDT_TITLE, "%s Security Alert", appname);
 
-    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);
+    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:
+        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.");
+        break;
+      default:
+        unreachable("bad WeakCryptoReason");
+    }
 
     /* In batch mode, we print the above information and then this
      * abort message, and stop. */
diff --git a/ssh/login1.c b/ssh/login1.c
index ec316575..2bf1ab3b 100644
--- a/ssh/login1.c
+++ b/ssh/login1.c
@@ -324,7 +324,7 @@ static void ssh1_login_process_queue(PacketProtocolLayer *ppl)
         if (warn) {
             s->spr = confirm_weak_crypto_primitive(
                 ppl_get_iseat(&s->ppl), "cipher", cipher_string,
-                ssh1_login_dialog_callback, s);
+                ssh1_login_dialog_callback, s, 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");
diff --git a/ssh/transport2.c b/ssh/transport2.c
index e6229ec9..b6dc4a47 100644
--- a/ssh/transport2.c
+++ b/ssh/transport2.c
@@ -33,6 +33,11 @@ const static ptrlen kex_strict_c =
 const static ptrlen kex_strict_s =
     PTRLEN_DECL_LITERAL("kex-strict-s-v00@openssh.com");
 
+/* 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;
@@ -86,6 +91,8 @@ static size_t ssh2_transport_queued_data_size(PacketProtocolLayer *ppl);
 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 const char *terrapin_vulnerable(
+    bool strict_kex, const transport_direction *d);
 
 static const PacketProtocolLayerVtable ssh2_transport_vtable = {
     .free = ssh2_transport_free,
@@ -106,7 +113,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",
@@ -1571,7 +1578,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");
@@ -1623,7 +1631,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)) {
@@ -1635,7 +1643,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");
@@ -1646,7 +1654,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");
@@ -1654,6 +1662,44 @@ 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 (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.
@@ -2463,14 +2509,15 @@ static int ca_blob_compare(void *av, void *bv)
  */
 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 confirm_weak_crypto_primitive(
-        ppl_get_iseat(&s->ppl), type, name, ssh2_transport_dialog_callback, s);
+        ppl_get_iseat(&s->ppl), type, name, ssh2_transport_dialog_callback,
+        s, wcr);
 }
 
 static size_t ssh2_transport_queued_data_size(PacketProtocolLayer *ppl)
@@ -2481,3 +2528,32 @@ static size_t ssh2_transport_queued_data_size(PacketProtocolLayer *ppl)
     return (ssh_ppl_default_queued_data_size(ppl) +
             ssh_ppl_queued_data_size(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;
+}
diff --git a/ssh/transport2.h b/ssh/transport2.h
index 1322cf5b..ea739df9 100644
--- a/ssh/transport2.h
+++ b/ssh/transport2.h
@@ -180,6 +180,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;
