Browse Source

PuTTY 0.78

Source commit: 4eba336bd557f717ffaa78be798684bf09042571
Martin Prikryl 2 years ago
parent
commit
a4b3791c54
100 changed files with 8065 additions and 1176 deletions
  1. 6 0
      source/putty/crypto/aes-common.c
  2. 60 0
      source/putty/crypto/aes-ni.c
  3. 29 8
      source/putty/crypto/aes-select.c
  4. 94 1
      source/putty/crypto/aes-sw.c
  5. 56 5
      source/putty/crypto/aes.h
  6. 8 0
      source/putty/crypto/aesgcm-common.c
  7. 368 0
      source/putty/crypto/aesgcm-footer.h
  8. 364 0
      source/putty/crypto/aesgcm-ref-poly.c
  9. 38 0
      source/putty/crypto/aesgcm-select.c
  10. 145 0
      source/putty/crypto/aesgcm-sw.c
  11. 44 0
      source/putty/crypto/aesgcm.h
  12. 2 0
      source/putty/crypto/arcfour.c
  13. 9 9
      source/putty/crypto/bcrypt.c
  14. 12 9
      source/putty/crypto/blowfish.c
  15. 22 5
      source/putty/crypto/chacha20-poly1305.c
  16. 6 1
      source/putty/crypto/des.c
  17. 32 0
      source/putty/crypto/diffie-hellman.c
  18. 16 2
      source/putty/crypto/dsa.c
  19. 235 124
      source/putty/crypto/ecc-ssh.c
  20. 6 0
      source/putty/crypto/hmac.c
  21. 8 0
      source/putty/crypto/mpint.c
  22. 1160 0
      source/putty/crypto/openssh-certs.c
  23. 57 8
      source/putty/crypto/rsa.c
  24. 22 0
      source/putty/defs.h
  25. 519 0
      source/putty/doc/authplugin.but
  26. 6 0
      source/putty/doc/chmextra.but
  27. 306 33
      source/putty/doc/config.but
  28. 38 0
      source/putty/doc/errors.but
  29. 31 16
      source/putty/doc/faq.but
  30. 4 4
      source/putty/doc/gs.but
  31. 24 0
      source/putty/doc/index.but
  32. 6 0
      source/putty/doc/man-pageant.but
  33. 51 11
      source/putty/doc/man-puttygen.but
  34. 65 13
      source/putty/doc/pageant.but
  35. 1 1
      source/putty/doc/plink.but
  36. 1 1
      source/putty/doc/pscp.but
  37. 59 13
      source/putty/doc/pubkey.but
  38. 3 3
      source/putty/doc/pubkeyfmt.but
  39. 396 100
      source/putty/doc/puttydoc.txt
  40. 1 1
      source/putty/doc/sshnames.but
  41. 22 0
      source/putty/doc/using.but
  42. 1 1
      source/putty/doc/version.but
  43. 49 51
      source/putty/import.c
  44. 1 1
      source/putty/marshal.h
  45. 56 5
      source/putty/misc.h
  46. 7 0
      source/putty/mpint.h
  47. 4 0
      source/putty/network.h
  48. 2 1
      source/putty/proxy/cproxy.c
  49. 11 9
      source/putty/proxy/proxy.c
  50. 24 24
      source/putty/proxy/telnet.c
  51. 137 23
      source/putty/putty.h
  52. 21 2
      source/putty/settings.c
  53. 254 31
      source/putty/ssh.h
  54. 15 9
      source/putty/ssh/bpp2.c
  55. 2 2
      source/putty/ssh/channel.h
  56. 159 9
      source/putty/ssh/common.c
  57. 3 3
      source/putty/ssh/connection2.c
  58. 1 1
      source/putty/ssh/gssc.c
  59. 214 75
      source/putty/ssh/kex2-client.c
  60. 1 1
      source/putty/ssh/mainchan.c
  61. 69 39
      source/putty/ssh/pgssapi.c
  62. 3 3
      source/putty/ssh/pgssapi.h
  63. 1 1
      source/putty/ssh/portfwd.c
  64. 6 5
      source/putty/ssh/ppl.h
  65. 40 43
      source/putty/ssh/sharing.c
  66. 6 4
      source/putty/ssh/ssh.c
  67. 338 92
      source/putty/ssh/transport2.c
  68. 33 17
      source/putty/ssh/transport2.h
  69. 696 208
      source/putty/ssh/userauth2-client.c
  70. 9 3
      source/putty/ssh/verstring.c
  71. 15 16
      source/putty/ssh/zlib.c
  72. 77 84
      source/putty/sshpubk.c
  73. 27 0
      source/putty/storage.h
  74. 11 0
      source/putty/stubs/null-cipher.c
  75. 22 0
      source/putty/stubs/null-key.c
  76. 0 0
      source/putty/stubs/null-lp.c
  77. 11 0
      source/putty/stubs/null-mac.c
  78. 20 0
      source/putty/stubs/null-opener.c
  79. 0 0
      source/putty/stubs/null-plug.c
  80. 12 1
      source/putty/stubs/null-seat.c
  81. 11 11
      source/putty/tree234.h
  82. 37 0
      source/putty/utils/base64_decode.c
  83. 40 0
      source/putty/utils/base64_encode.c
  84. 968 0
      source/putty/utils/cert-expr.c
  85. 2 2
      source/putty/utils/conf.c
  86. 22 0
      source/putty/utils/host_ca_new_free.c
  87. 93 0
      source/putty/utils/key_components.c
  88. 9 2
      source/putty/utils/log_proxy_stderr.c
  89. 41 0
      source/putty/utils/percent_decode.c
  90. 34 0
      source/putty/utils/percent_encode.c
  91. 16 0
      source/putty/utils/ptrlen.c
  92. 41 0
      source/putty/utils/seat_dialog_text.c
  93. 1 1
      source/putty/utils/smemeq.c
  94. 14 0
      source/putty/utils/strbuf.c
  95. 13 1
      source/putty/utils/tempseat.c
  96. 16 16
      source/putty/utils/tree234.c
  97. 4 4
      source/putty/version.h
  98. 2 2
      source/putty/windows/agent-client.c
  99. 4 5
      source/putty/windows/gss.c
  100. 7 0
      source/putty/windows/handle-socket.c

+ 6 - 0
source/putty/crypto/aes-common.c

@@ -12,3 +12,9 @@ const uint8_t aes_key_setup_round_constants[10] = {
      * regardless of the key. */
     0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36,
 };
+
+void aesgcm_cipher_crypt_length(
+    ssh_cipher *cipher, void *blk, int len, unsigned long seq)
+{
+    /* Do nothing: lengths are sent in clear for this cipher. */
+}

+ 60 - 0
source/putty/crypto/aes-ni.c

@@ -137,6 +137,16 @@ static inline __m128i aes_ni_sdctr_increment(__m128i v)
     return v;
 }
 
+/*
+ * Much simpler auxiliary routine to increment the counter for GCM
+ * mode. This only has to increment the low word.
+ */
+static inline __m128i aes_ni_gcm_increment(__m128i v)
+{
+    const __m128i ONE  = _mm_setr_epi32(1,0,0,0);
+    return _mm_add_epi32(v, ONE);
+}
+
 /*
  * Auxiliary routine to reverse the byte order of a vector, so that
  * the SDCTR IV can be made big-endian for feeding to the cipher.
@@ -214,6 +224,25 @@ static void aes_ni_setiv_sdctr(ssh_cipher *ciph, const void *iv)
     ctx->iv = aes_ni_sdctr_reverse(counter);
 }
 
+static void aes_ni_setiv_gcm(ssh_cipher *ciph, const void *iv)
+{
+    aes_ni_context *ctx = container_of(ciph, aes_ni_context, ciph);
+    __m128i counter = _mm_loadu_si128(iv);
+    ctx->iv = aes_ni_sdctr_reverse(counter);
+    ctx->iv = _mm_insert_epi32(ctx->iv, 1, 0);
+}
+
+static void aes_ni_next_message_gcm(ssh_cipher *ciph)
+{
+    aes_ni_context *ctx = container_of(ciph, aes_ni_context, ciph);
+    uint32_t fixed = _mm_extract_epi32(ctx->iv, 3);
+    uint64_t msg_counter = _mm_extract_epi32(ctx->iv, 2);
+    msg_counter <<= 32;
+    msg_counter |= (uint32_t)_mm_extract_epi32(ctx->iv, 1);
+    msg_counter++;
+    ctx->iv = _mm_set_epi32(fixed, msg_counter >> 32, msg_counter, 1);
+}
+
 typedef __m128i (*aes_ni_fn)(__m128i v, const __m128i *keysched);
 
 static inline void aes_cbc_ni_encrypt(
@@ -262,6 +291,31 @@ static inline void aes_sdctr_ni(
     }
 }
 
+static inline void aes_encrypt_ecb_block_ni(
+    ssh_cipher *ciph, void *blk, aes_ni_fn encrypt)
+{
+    aes_ni_context *ctx = container_of(ciph, aes_ni_context, ciph);
+    __m128i plaintext = _mm_loadu_si128(blk);
+    __m128i ciphertext = encrypt(plaintext, ctx->keysched_e);
+    _mm_storeu_si128(blk, ciphertext);
+}
+
+static inline void aes_gcm_ni(
+    ssh_cipher *ciph, void *vblk, int blklen, aes_ni_fn encrypt)
+{
+    aes_ni_context *ctx = container_of(ciph, aes_ni_context, ciph);
+
+    for (uint8_t *blk = (uint8_t *)vblk, *finish = blk + blklen;
+         blk < finish; blk += 16) {
+        __m128i counter = aes_ni_sdctr_reverse(ctx->iv);
+        __m128i keystream = encrypt(counter, ctx->keysched_e);
+        __m128i input = _mm_loadu_si128((const __m128i *)blk);
+        __m128i output = _mm_xor_si128(input, keystream);
+        _mm_storeu_si128((__m128i *)blk, output);
+        ctx->iv = aes_ni_gcm_increment(ctx->iv);
+    }
+}
+
 #define NI_ENC_DEC(len)                                                 \
     static void aes##len##_ni_cbc_encrypt(                              \
         ssh_cipher *ciph, void *vblk, int blklen)                       \
@@ -272,6 +326,12 @@ static inline void aes_sdctr_ni(
     static void aes##len##_ni_sdctr(                                    \
         ssh_cipher *ciph, void *vblk, int blklen)                       \
     { aes_sdctr_ni(ciph, vblk, blklen, aes_ni_##len##_e); }             \
+    static void aes##len##_ni_gcm(                                      \
+        ssh_cipher *ciph, void *vblk, int blklen)                       \
+    { aes_gcm_ni(ciph, vblk, blklen, aes_ni_##len##_e); }               \
+    static void aes##len##_ni_encrypt_ecb_block(                        \
+        ssh_cipher *ciph, void *vblk)                                   \
+    { aes_encrypt_ecb_block_ni(ciph, vblk, aes_ni_##len##_e); }
 
 NI_ENC_DEC(128)
 NI_ENC_DEC(192)

+ 29 - 8
source/putty/crypto/aes-select.c

@@ -39,7 +39,7 @@ static ssh_cipher *aes_select(const ssh_cipheralg *alg)
 #define IF_NEON(...)
 #endif
 
-#define AES_SELECTOR_VTABLE(mode_c, mode_protocol, mode_display, bits)  \
+#define AES_SELECTOR_VTABLE(mode_c, id, mode_display, bits, ...)        \
     static const ssh_cipheralg *                                        \
     ssh_aes ## bits ## _ ## mode_c ## _impls[] = {                      \
         IF_NI(&ssh_aes ## bits ## _ ## mode_c ## _ni,)                  \
@@ -49,21 +49,33 @@ static ssh_cipher *aes_select(const ssh_cipheralg *alg)
     };                                                                  \
     const ssh_cipheralg ssh_aes ## bits ## _ ## mode_c = {              \
         .new = aes_select,                                              \
-        .ssh2_id = "aes" #bits "-" mode_protocol,                       \
+        .ssh2_id = id,                                                  \
         .blksize = 16,                                                  \
         .real_keybits = bits,                                           \
         .padded_keybytes = bits/8,                                      \
         .text_name = "AES-" #bits " " mode_display                      \
         " (dummy selector vtable)",                                     \
         .extra = ssh_aes ## bits ## _ ## mode_c ## _impls,              \
+        __VA_ARGS__                                                     \
     }
 
-AES_SELECTOR_VTABLE(cbc, "cbc", "CBC", 128);
-AES_SELECTOR_VTABLE(cbc, "cbc", "CBC", 192);
-AES_SELECTOR_VTABLE(cbc, "cbc", "CBC", 256);
-AES_SELECTOR_VTABLE(sdctr, "ctr", "SDCTR", 128);
-AES_SELECTOR_VTABLE(sdctr, "ctr", "SDCTR", 192);
-AES_SELECTOR_VTABLE(sdctr, "ctr", "SDCTR", 256);
+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(sdctr, "aes128-ctr", "SDCTR", 128, );
+AES_SELECTOR_VTABLE(sdctr, "aes192-ctr", "SDCTR", 192, );
+AES_SELECTOR_VTABLE(sdctr, "aes256-ctr", "SDCTR", 256, );
+AES_SELECTOR_VTABLE(gcm, "[email protected]", "GCM", 128,
+                    .required_mac = &ssh2_aesgcm_mac);
+AES_SELECTOR_VTABLE(gcm, "[email protected]", "GCM", 256,
+                    .required_mac = &ssh2_aesgcm_mac);
+
+/* 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);
 
 static const ssh_cipheralg ssh_rijndael_lysator = {
     /* Same as aes256_cbc, but with a different protocol ID */
@@ -87,3 +99,12 @@ static const ssh_cipheralg *const aes_list[] = {
 };
 
 const ssh2_ciphers ssh2_aes = { lenof(aes_list), aes_list };
+
+static const ssh_cipheralg *const aesgcm_list[] = {
+    /* OpenSSH only defines protocol ids for 128- and 256-bit AES-GCM,
+     * not 192-bit. */
+    &ssh_aes128_gcm,
+    &ssh_aes256_gcm,
+};
+
+const ssh2_ciphers ssh2_aesgcm = { lenof(aesgcm_list), aesgcm_list };

+ 94 - 1
source/putty/crypto/aes-sw.c

@@ -827,6 +827,18 @@ struct aes_sw_context {
             uint8_t keystream[SLICE_PARALLELISM * 16];
             uint8_t *keystream_pos;
         } sdctr;
+        struct {
+            /* In GCM mode, the cipher preimage consists of three
+             * sections: one fixed, one that increments per message
+             * sent and MACed, and one that increments per cipher
+             * block. */
+            uint64_t msg_counter;
+            uint32_t fixed_iv, block_counter;
+            /* But we keep the precomputed keystream chunks just like
+             * SDCTR mode. */
+            uint8_t keystream[SLICE_PARALLELISM * 16];
+            uint8_t *keystream_pos;
+        } gcm;
     } iv;
     ssh_cipher ciph;
 };
@@ -874,6 +886,31 @@ static void aes_sw_setiv_sdctr(ssh_cipher *ciph, const void *viv)
         ctx->iv.sdctr.keystream + sizeof(ctx->iv.sdctr.keystream);
 }
 
+static void aes_sw_setiv_gcm(ssh_cipher *ciph, const void *viv)
+{
+    aes_sw_context *ctx = container_of(ciph, aes_sw_context, ciph);
+    const uint8_t *iv = (const uint8_t *)viv;
+
+    ctx->iv.gcm.fixed_iv = GET_32BIT_MSB_FIRST(iv);
+    ctx->iv.gcm.msg_counter = GET_64BIT_MSB_FIRST(iv + 4);
+    ctx->iv.gcm.block_counter = 1;
+
+    /* Set keystream_pos to indicate that the keystream cache is
+     * currently empty */
+    ctx->iv.gcm.keystream_pos =
+        ctx->iv.gcm.keystream + sizeof(ctx->iv.gcm.keystream);
+}
+
+static void aes_sw_next_message_gcm(ssh_cipher *ciph)
+{
+    aes_sw_context *ctx = container_of(ciph, aes_sw_context, ciph);
+
+    ctx->iv.gcm.msg_counter++;
+    ctx->iv.gcm.block_counter = 1;
+    ctx->iv.gcm.keystream_pos =
+        ctx->iv.gcm.keystream + sizeof(ctx->iv.gcm.keystream);
+}
+
 typedef void (*aes_sw_fn)(uint32_t v[4], const uint32_t *keysched);
 
 static inline void memxor16(void *vout, const void *vlhs, const void *vrhs)
@@ -1021,6 +1058,56 @@ static inline void aes_sdctr_sw(
     }
 }
 
+static inline void aes_encrypt_ecb_block_sw(ssh_cipher *ciph, void *blk)
+{
+    aes_sw_context *ctx = container_of(ciph, aes_sw_context, ciph);
+    aes_sliced_e_serial(blk, blk, &ctx->sk);
+}
+
+static inline void aes_gcm_sw(
+    ssh_cipher *ciph, void *vblk, int blklen)
+{
+    aes_sw_context *ctx = container_of(ciph, aes_sw_context, ciph);
+
+    /*
+     * GCM encrypt/decrypt looks just like SDCTR, except that the
+     * method of generating more keystream varies slightly.
+     */
+
+    uint8_t *keystream_end =
+        ctx->iv.gcm.keystream + sizeof(ctx->iv.gcm.keystream);
+
+    for (uint8_t *blk = (uint8_t *)vblk, *finish = blk + blklen;
+         blk < finish; blk += 16) {
+
+        if (ctx->iv.gcm.keystream_pos == keystream_end) {
+            /*
+             * Generate some keystream.
+             */
+            for (uint8_t *block = ctx->iv.gcm.keystream;
+                 block < keystream_end; block += 16) {
+                /* Format the counter value into the buffer. */
+                PUT_32BIT_MSB_FIRST(block, ctx->iv.gcm.fixed_iv);
+                PUT_64BIT_MSB_FIRST(block + 4, ctx->iv.gcm.msg_counter);
+                PUT_32BIT_MSB_FIRST(block + 12, ctx->iv.gcm.block_counter);
+
+                /* Increment the counter. */
+                ctx->iv.gcm.block_counter++;
+            }
+
+            /* Encrypt all those counter blocks. */
+            aes_sliced_e_parallel(ctx->iv.gcm.keystream,
+                                  ctx->iv.gcm.keystream, &ctx->sk);
+
+            /* Reset keystream_pos to the start of the buffer. */
+            ctx->iv.gcm.keystream_pos = ctx->iv.gcm.keystream;
+        }
+
+        memxor16(blk, blk, ctx->iv.gcm.keystream_pos);
+        ctx->iv.gcm.keystream_pos += 16;
+    }
+}
+
 #define SW_ENC_DEC(len)                                 \
     static void aes##len##_sw_cbc_encrypt(              \
         ssh_cipher *ciph, void *vblk, int blklen)       \
@@ -1030,7 +1117,13 @@ static inline void aes_sdctr_sw(
     { aes_cbc_sw_decrypt(ciph, vblk, blklen); }         \
     static void aes##len##_sw_sdctr(                    \
         ssh_cipher *ciph, void *vblk, int blklen)       \
-    { aes_sdctr_sw(ciph, vblk, blklen); }
+    { aes_sdctr_sw(ciph, vblk, blklen); }               \
+    static void aes##len##_sw_gcm(                      \
+        ssh_cipher *ciph, void *vblk, int blklen)       \
+    { aes_gcm_sw(ciph, vblk, blklen); }                 \
+    static void aes##len##_sw_encrypt_ecb_block(        \
+        ssh_cipher *ciph, void *vblk)                   \
+    { aes_encrypt_ecb_block_sw(ciph, vblk); }
 
 SW_ENC_DEC(128)
 SW_ENC_DEC(192)

+ 56 - 5
source/putty/crypto/aes.h

@@ -15,6 +15,11 @@ struct aes_extra {
 
     /* Point to a writable substructure. */
     struct aes_extra_mutable *mut;
+
+    /* Extra API function specific to AES, to encrypt a single block
+     * in ECB mode without touching the IV. Used by AES-GCM MAC
+     * setup. */
+    void (*encrypt_ecb_block)(ssh_cipher *, void *);
 };
 struct aes_extra_mutable {
     bool checked_availability;
@@ -30,6 +35,17 @@ static inline bool check_availability(const struct aes_extra *extra)
     return extra->mut->is_available;
 }
 
+/* Shared stub function for all the AES-GCM vtables. */
+void aesgcm_cipher_crypt_length(
+    ssh_cipher *cipher, void *blk, int len, unsigned long seq);
+
+/* External entry point for the encrypt_ecb_block function. */
+static inline void aes_encrypt_ecb_block(ssh_cipher *ciph, void *blk)
+{
+    const struct aes_extra *extra = ciph->vt->extra;
+    extra->encrypt_ecb_block(ciph, blk);
+}
+
 /*
  * Macros to define vtables for AES variants. There are a lot of
  * these, because of the cross product between cipher modes, key
@@ -37,13 +53,19 @@ static inline bool check_availability(const struct aes_extra *extra)
  * some effort here to reduce the boilerplate in the sub-files.
  */
 
-#define AES_EXTRA(impl_c)                                               \
+#define AES_EXTRA_BITS(impl_c, bits)                                    \
     static struct aes_extra_mutable aes ## impl_c ## _extra_mut;        \
-    static const struct aes_extra aes ## impl_c ## _extra = {           \
+    static const struct aes_extra aes ## bits ## impl_c ## _extra = {   \
         .check_available = aes ## impl_c ## _available,                 \
         .mut = &aes ## impl_c ## _extra_mut,                            \
+        .encrypt_ecb_block = &aes ## bits ## impl_c ## _encrypt_ecb_block, \
     }
 
+#define AES_EXTRA(impl_c)                       \
+    AES_EXTRA_BITS(impl_c, 128);                \
+    AES_EXTRA_BITS(impl_c, 192);                \
+    AES_EXTRA_BITS(impl_c, 256)
+
 #define AES_CBC_VTABLE(impl_c, impl_display, bits)                      \
     const ssh_cipheralg ssh_aes ## bits ## _cbc ## impl_c = {           \
         .new = aes ## impl_c ## _new,                                   \
@@ -52,13 +74,14 @@ static inline bool check_availability(const struct aes_extra *extra)
         .setkey = aes ## impl_c ## _setkey,                             \
         .encrypt = aes ## bits ## impl_c ## _cbc_encrypt,               \
         .decrypt = aes ## bits ## impl_c ## _cbc_decrypt,               \
+        .next_message = nullcipher_next_message,                        \
         .ssh2_id = "aes" #bits "-cbc",                                  \
         .blksize = 16,                                                  \
         .real_keybits = bits,                                           \
         .padded_keybytes = bits/8,                                      \
         .flags = SSH_CIPHER_IS_CBC,                                     \
         .text_name = "AES-" #bits " CBC (" impl_display ")",            \
-        .extra = &aes ## impl_c ## _extra,                              \
+        .extra = &aes ## bits ## impl_c ## _extra,                      \
     }
 
 #define AES_SDCTR_VTABLE(impl_c, impl_display, bits)                    \
@@ -69,13 +92,38 @@ static inline bool check_availability(const struct aes_extra *extra)
         .setkey = aes ## impl_c ## _setkey,                             \
         .encrypt = aes ## bits ## impl_c ## _sdctr,                     \
         .decrypt = aes ## bits ## impl_c ## _sdctr,                     \
+        .next_message = nullcipher_next_message,                        \
         .ssh2_id = "aes" #bits "-ctr",                                  \
         .blksize = 16,                                                  \
         .real_keybits = bits,                                           \
         .padded_keybytes = bits/8,                                      \
         .flags = 0,                                                     \
         .text_name = "AES-" #bits " SDCTR (" impl_display ")",          \
-        .extra = &aes ## impl_c ## _extra,                              \
+        .extra = &aes ## bits ## impl_c ## _extra,                      \
+    }
+
+#define AES_GCM_VTABLE(impl_c, impl_display, bits)                      \
+    const ssh_cipheralg ssh_aes ## bits ## _gcm ## impl_c = {           \
+        .new = aes ## impl_c ## _new,                                   \
+        .free = aes ## impl_c ## _free,                                 \
+        .setiv = aes ## impl_c ## _setiv_gcm,                           \
+        .setkey = aes ## impl_c ## _setkey,                             \
+        .encrypt = aes ## bits ## impl_c ## _gcm,                       \
+        .decrypt = aes ## bits ## impl_c ## _gcm,                       \
+        .encrypt_length = aesgcm_cipher_crypt_length,                   \
+        .decrypt_length = aesgcm_cipher_crypt_length,                   \
+        .next_message = aes ## impl_c ## _next_message_gcm,             \
+        /* 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. So we set its ssh2_id to NULL. */        \
+        .ssh2_id = bits==192 ? NULL : "aes" #bits "[email protected]",   \
+        .blksize = 16,                                                  \
+        .real_keybits = bits,                                           \
+        .padded_keybytes = bits/8,                                      \
+        .flags = SSH_CIPHER_SEPARATE_LENGTH,                            \
+        .text_name = "AES-" #bits " GCM (" impl_display ")",            \
+        .required_mac = &ssh2_aesgcm_mac,                               \
+        .extra = &aes ## bits ## impl_c ## _extra,                      \
     }
 
 #define AES_ALL_VTABLES(impl_c, impl_display)           \
@@ -84,7 +132,10 @@ static inline bool check_availability(const struct aes_extra *extra)
     AES_CBC_VTABLE(impl_c, impl_display, 256);          \
     AES_SDCTR_VTABLE(impl_c, impl_display, 128);        \
     AES_SDCTR_VTABLE(impl_c, impl_display, 192);        \
-    AES_SDCTR_VTABLE(impl_c, impl_display, 256)
+    AES_SDCTR_VTABLE(impl_c, impl_display, 256);        \
+    AES_GCM_VTABLE(impl_c, impl_display, 128);          \
+    AES_GCM_VTABLE(impl_c, impl_display, 192);          \
+    AES_GCM_VTABLE(impl_c, impl_display, 256)
 
 /*
  * Macros to repeat a piece of code particular numbers of times that

+ 8 - 0
source/putty/crypto/aesgcm-common.c

@@ -0,0 +1,8 @@
+#include "ssh.h"
+#include "aesgcm.h"
+
+void aesgcm_set_prefix_lengths(ssh2_mac *mac, size_t skip, size_t aad)
+{
+    const struct aesgcm_extra *extra = mac->vt->extra;
+    extra->set_prefix_lengths(mac, skip, aad);
+}

+ 368 - 0
source/putty/crypto/aesgcm-footer.h

@@ -0,0 +1,368 @@
+/*
+ * Common footer included by every implementation of the AES-GCM MAC.
+ *
+ * The difficult part of AES-GCM, which is done differently depending
+ * on what hardware acceleration is available, is the actual
+ * evaluation of a polynomial over GF(2^128) whose coefficients are
+ * 128-bit chunks of data. But preparing those chunks in the first
+ * place (out of the ciphertext, associated data, and an
+ * administrative block containing the lengths of both) is done in the
+ * same way no matter what technique is used for the evaluation, so
+ * that's centralised into this file, along with as much of the other
+ * functionality as posible.
+ *
+ * This footer file is #included by each implementation, but each one
+ * will define its own struct type for the state, so that each alloc
+ * function will test sizeof() a different structure, and similarly
+ * for free when it zeroes out the state on cleanup.
+ *
+ * The functions in the source file may be defined as 'inline' so that
+ * the functions in here can inline them. The 'coeff' function in
+ * particular probably should be, because that's called once per
+ * 16-byte block, so eliminating function call overheads is especially
+ * useful there.
+ *
+ * This footer has the following expectations from the source file
+ * that #includes it:
+ *
+ *  - define AESGCM_FLAVOUR to be a fragment of a C identifier that
+ *    will be included in all the function names (both the ones
+ *    defined in the implementation source file and those in here).
+ *    For example purposes below I'll suppose that this is 'foo'.
+ *
+ *  - define AESGCM_NAME to be a string literal that will be included
+ *    in the display name of the implementation.
+ *
+ *  - define a typedef 'aesgcm_foo' to be the state structure for the
+ *    implementation, and inside that structure, expand the macro
+ *    AESGCM_COMMON_FIELDS defined in aesgcm.h
+ *
+ *  - define the following functions:
+ *
+ *    // Determine whether this implementation is available at run time
+ *    static bool aesgcm_foo_available(void);
+ *
+ *    // Set up the 'key' of the polynomial part of the MAC, that is,
+ *    // the value at which the polynomial will be evaluated. 'var' is
+ *    // a 16-byte data block in the byte order it comes out of AES.
+ *    static void aesgcm_foo_setkey_impl(aesgcm_foo *ctx,
+ *                                       const unsigned char *var);
+ *
+ *    // Set up at the start of evaluating an individual polynomial.
+ *    // 'mask' is the 16-byte data block that will be XORed into the
+ *    // output value of the polynomial, also in AES byte order. This
+ *    // function should store 'mask' in whatever form is most
+ *    // convenient, and initialise an accumulator to zero.
+ *    static void aesgcm_foo_setup(aesgcm_foo *ctx,
+ *                                 const unsigned char *mask);
+ *
+ *    // Fold in a coefficient of the polynomial, by means of XORing
+ *    // it into the accumulator and then multiplying the accumulator
+ *    // by the variable passed to setkey_impl() above.
+ *    //
+ *    // 'coeff' points to the 16-byte block of data that the
+ *    // polynomial coefficient will be made out of.
+ *    //
+ *    // You probably want to mark this function 'inline'.
+ *    static void aesgcm_foo_coeff(aesgcm_foo *ctx,
+ *                                 const unsigned char *coeff);
+ *
+ *    // Generate the output MAC, by XORing the accumulator's final
+ *    // value with the mask passed to setup() above.
+ *    //
+ *    // 'output' points to a 16-byte region of memory to write the
+ *    // result to.
+ *    static void aesgcm_foo_output(aesgcm_foo *ctx,
+ *                                  unsigned char *output);
+ *
+ *  - if allocation of the state structure must be done in a
+ *    non-standard way (e.g. x86 needs this to force greater alignment
+ *    than standard malloc provides), then #define SPECIAL_ALLOC and
+ *    define this additional function:
+ *
+ *    // Allocate a state structure, zero out its contents, and return it.
+ *    static aesgcm_foo *aesgcm_foo_alloc(void);
+ *
+ *  - if freeing must also be done in an unusual way, #define
+ *    SPECIAL_FREE and define this function:
+ *
+ *    // Zero out the state structure to avoid information leaks if the
+ *    // memory is reused, and then free it.
+ *    static void aesgcm_foo_free(aesgcm_foo *ctx);
+ */
+
+#ifndef AESGCM_FLAVOUR
+#error AESGCM_FLAVOUR must be defined by any module including this footer
+#endif
+#ifndef AESGCM_NAME
+#error AESGCM_NAME must be defined by any module including this footer
+#endif
+
+#define CONTEXT CAT(aesgcm_, AESGCM_FLAVOUR)
+#define PREFIX(name) CAT(CAT(aesgcm_, AESGCM_FLAVOUR), CAT(_, name))
+
+#include "aes.h" // for aes_encrypt_ecb_block
+
+static const char *PREFIX(mac_text_name)(ssh2_mac *mac)
+{
+    return "AES-GCM (" AESGCM_NAME ")";
+}
+
+static void PREFIX(mac_next_message)(ssh2_mac *mac)
+{
+    CONTEXT *ctx = container_of(mac, CONTEXT, mac);
+
+    /*
+     * Make the mask value for a single MAC instance, by encrypting
+     * the all-zeroes word using the associated AES instance in its
+     * ordinary GCM fashion. This consumes the first block of
+     * keystream (with per-block counter equal to 1), leaving the
+     * second block of keystream ready to be used on the first block
+     * of plaintext.
+     */
+    unsigned char buf[16];
+    memset(buf, 0, 16);
+    ssh_cipher_encrypt(ctx->cipher, buf, 16);
+    PREFIX(setup)(ctx, buf); /* give it to the implementation to store */
+    smemclr(buf, sizeof(buf));
+}
+
+static void PREFIX(mac_setkey)(ssh2_mac *mac, ptrlen key)
+{
+    CONTEXT *ctx = container_of(mac, CONTEXT, mac);
+
+    /*
+     * Make the value of the polynomial variable, by encrypting the
+     * all-zeroes word using the associated AES instance in the
+     * special ECB mode. This is done via the special AES-specific API
+     * function encrypt_ecb_block, which doesn't touch the counter
+     * state at all.
+     */
+    unsigned char var[16];
+    memset(var, 0, 16);
+    aes_encrypt_ecb_block(ctx->cipher, var);
+    PREFIX(setkey_impl)(ctx, var);
+    smemclr(var, sizeof(var));
+
+    PREFIX(mac_next_message)(mac);     /* set up mask */
+}
+
+static void PREFIX(mac_start)(ssh2_mac *mac)
+{
+    CONTEXT *ctx = container_of(mac, CONTEXT, mac);
+
+    ctx->skipgot = ctx->aadgot = ctx->ciphertextlen = ctx->partlen = 0;
+}
+
+/*
+ * Handle receiving data via the BinarySink API and turning it into a
+ * collection of 16-byte blocks to use as polynomial coefficients.
+ *
+ * This code is written in a fully general way, which is able to
+ * handle an arbitrary number of bytes at the start of the data to
+ * ignore completely (necessary for PuTTY integration), and an
+ * arbitrary number to treat as associated data, and the rest will be
+ * regarded as ciphertext. The stream can be interrupted at any byte
+ * position and resumed later; a partial block will be stored as
+ * necessary.
+ *
+ * At the time of writing this comment, in live use most of that
+ * generality isn't required: the full data is passed to this function
+ * in just one call. But there's no guarantee of that staying true in
+ * future, so we do the full deal here just in case, and the test
+ * vectors in cryptsuite.py will test it. (And they'll use
+ * set_prefix_lengths to set up different configurations from the SSH
+ * usage.)
+ */
+static void PREFIX(mac_BinarySink_write)(
+    BinarySink *bs, const void *blkv, size_t len)
+{
+    CONTEXT *ctx = BinarySink_DOWNCAST(bs, CONTEXT);
+    const unsigned char *blk = (const unsigned char *)blkv;
+
+    /*
+     * Skip the prefix sequence number used as implicit extra data in
+     * SSH MACs. This is not included in the associated data field for
+     * GCM, because the IV incrementation policy provides its own
+     * sequence numbering.
+     */
+    if (ctx->skipgot < ctx->skiplen) {
+        size_t n = ctx->skiplen - ctx->skipgot;
+        if (n > len)
+            n = len;
+        blk += n;
+        len -= n;
+        ctx->skipgot += n;
+
+        if (len == 0)
+            return;
+    }
+
+    /*
+     * Read additional authenticated data and fold it in to the MAC.
+     */
+    while (ctx->aadgot < ctx->aadlen) {
+        size_t n = ctx->aadlen - ctx->aadgot;
+        if (n > len)
+            n = len;
+
+        if (ctx->partlen || n < 16) {
+            /*
+             * Fold data into the partial block.
+             */
+            if (n > 16 - ctx->partlen)
+                n = 16 - ctx->partlen;
+            memcpy(ctx->partblk + ctx->partlen, blk, n);
+            ctx->partlen += n;
+        } else if (n >= 16) {
+            /*
+             * Consume a whole block of AAD.
+             */
+            PREFIX(coeff)(ctx, blk);
+            n = 16;
+        }
+        blk += n;
+        len -= n;
+        ctx->aadgot += n;
+
+        if (ctx->partlen == 16) {
+            PREFIX(coeff)(ctx, ctx->partblk);
+            ctx->partlen = 0;
+        }
+
+        if (ctx->aadgot == ctx->aadlen && ctx->partlen) {
+            memset(ctx->partblk + ctx->partlen, 0, 16 - ctx->partlen);
+            PREFIX(coeff)(ctx, ctx->partblk);
+            ctx->partlen = 0;
+        }
+
+        if (len == 0)
+            return;
+    }
+
+    /*
+     * Read the main ciphertext and fold it in to the MAC.
+     */
+    while (len > 0) {
+        size_t n = len;
+
+        if (ctx->partlen || n < 16) {
+            /*
+             * Fold data into the partial block.
+             */
+            if (n > 16 - ctx->partlen)
+                n = 16 - ctx->partlen;
+            memcpy(ctx->partblk + ctx->partlen, blk, n);
+            ctx->partlen += n;
+        } else if (n >= 16) {
+            /*
+             * Consume a whole block of ciphertext.
+             */
+            PREFIX(coeff)(ctx, blk);
+            n = 16;
+        }
+        blk += n;
+        len -= n;
+        ctx->ciphertextlen += n;
+
+        if (ctx->partlen == 16) {
+            PREFIX(coeff)(ctx, ctx->partblk);
+            ctx->partlen = 0;
+        }
+    }
+}
+
+static void PREFIX(mac_genresult)(ssh2_mac *mac, unsigned char *output)
+{
+    CONTEXT *ctx = container_of(mac, CONTEXT, mac);
+
+    /*
+     * Consume any partial block of ciphertext remaining.
+     */
+    if (ctx->partlen) {
+        memset(ctx->partblk + ctx->partlen, 0, 16 - ctx->partlen);
+        PREFIX(coeff)(ctx, ctx->partblk);
+    }
+
+    /*
+     * Consume the final block giving the lengths of the AAD and ciphertext.
+     */
+    unsigned char blk[16];
+    memset(blk, 0, 16);
+    PUT_64BIT_MSB_FIRST(blk, ctx->aadlen * 8);
+    PUT_64BIT_MSB_FIRST(blk + 8, ctx->ciphertextlen * 8);
+    PREFIX(coeff)(ctx, blk);
+
+    /*
+     * And call the implementation's output function.
+     */
+    PREFIX(output)(ctx, output);
+
+    smemclr(blk, sizeof(blk));
+    smemclr(ctx->partblk, 16);
+}
+
+static ssh2_mac *PREFIX(mac_new)(const ssh2_macalg *alg, ssh_cipher *cipher)
+{
+    const struct aesgcm_extra *extra = alg->extra;
+    if (!check_aesgcm_availability(extra))
+        return NULL;
+
+#ifdef SPECIAL_ALLOC
+    CONTEXT *ctx = PREFIX(alloc)();
+#else
+    CONTEXT *ctx = snew(CONTEXT);
+    memset(ctx, 0, sizeof(CONTEXT));
+#endif
+
+    ctx->mac.vt = alg;
+    ctx->cipher = cipher;
+    /* Default values for SSH-2, overridable by set_prefix_lengths for
+     * testcrypt purposes */
+    ctx->skiplen = 4;
+    ctx->aadlen = 4;
+    BinarySink_INIT(ctx, PREFIX(mac_BinarySink_write));
+    BinarySink_DELEGATE_INIT(&ctx->mac, ctx);
+    return &ctx->mac;
+}
+
+static void PREFIX(set_prefix_lengths)(ssh2_mac *mac, size_t skip, size_t aad)
+{
+    CONTEXT *ctx = container_of(mac, CONTEXT, mac);
+    ctx->skiplen = skip;
+    ctx->aadlen = aad;
+}
+
+static void PREFIX(mac_free)(ssh2_mac *mac)
+{
+    CONTEXT *ctx = container_of(mac, CONTEXT, mac);
+#ifdef SPECIAL_FREE
+    PREFIX(free)(ctx);
+#else
+    smemclr(ctx, sizeof(*ctx));
+    sfree(ctx);
+#endif
+}
+
+static struct aesgcm_extra_mutable PREFIX(extra_mut);
+
+static const struct aesgcm_extra PREFIX(extra) = {
+    .check_available = PREFIX(available),
+    .mut = &PREFIX(extra_mut),
+    .set_prefix_lengths = PREFIX(set_prefix_lengths),
+};
+
+const ssh2_macalg CAT(ssh2_aesgcm_mac_, AESGCM_FLAVOUR) = {
+    .new = PREFIX(mac_new),
+    .free = PREFIX(mac_free),
+    .setkey = PREFIX(mac_setkey),
+    .start = PREFIX(mac_start),
+    .genresult = PREFIX(mac_genresult),
+    .next_message = PREFIX(mac_next_message),
+    .text_name = PREFIX(mac_text_name),
+    .name = "",
+    .etm_name = "", /* Not selectable independently */
+    .len = 16,
+    .keylen = 0,
+    .extra = &PREFIX(extra),
+};

+ 364 - 0
source/putty/crypto/aesgcm-ref-poly.c

@@ -0,0 +1,364 @@
+/*
+ * Implementation of the GCM polynomial hash in pure software, but
+ * based on a primitive that performs 64x64->128 bit polynomial
+ * multiplication over GF(2).
+ *
+ * This implementation is technically correct (should even be
+ * side-channel safe as far as I can see), but it's hopelessly slow,
+ * so no live SSH connection should ever use it. Therefore, it's
+ * deliberately not included in the lists in aesgcm-select.c. For pure
+ * software GCM in live use, you want aesgcm-sw.c, and that's what the
+ * selection system will choose.
+ *
+ * However, this implementation _is_ made available to testcrypt, so
+ * all the GCM tests in cryptsuite.py are run over this as well as the
+ * other implementations.
+ *
+ * The reason why this code exists at all is to act as a reference for
+ * GCM implementations that use a CPU-specific polynomial multiply
+ * intrinsic or asm statement. This version will run on whatever
+ * platform you're trying to port to, and will generate all the same
+ * intermediate results you expect the CPU-specific one to go through.
+ * So you can insert parallel diagnostics in this version and in your
+ * new version, to see where the two diverge.
+ *
+ * Also, this version is a good place to put long comments explaining
+ * the entire strategy of this implementation and its rationale. That
+ * avoids those comments having to be duplicated in the multiple
+ * platform-specific implementations, which can focus on commenting
+ * the way the local platform's idioms match up to this version, and
+ * refer to this file for the explanation of the underlying technique.
+ */
+
+#include "ssh.h"
+#include "aesgcm.h"
+
+/*
+ * Store a 128-bit value in the most convenient form standard C will
+ * let us, namely two uint64_t giving its most and least significant
+ * halves.
+ */
+typedef struct {
+    uint64_t hi, lo;
+} value128_t;
+
+typedef struct aesgcm_ref_poly {
+    AESGCM_COMMON_FIELDS;
+
+    /*
+     * The state of our GCM implementation is represented entirely by
+     * three 128-bit values:
+     */
+
+    /*
+     * The value at which we're evaluating the polynomial. The GCM
+     * spec calls this 'H'. It's defined once at GCM key setup time,
+     * by encrypting the all-zeroes value with our block cipher.
+     */
+    value128_t var;
+
+    /*
+     * Accumulator containing the result of evaluating the polynomial
+     * so far.
+     */
+    value128_t acc;
+
+    /*
+     * The mask value that is XORed into the final value of 'acc' to
+     * produce the output MAC. This is different for every MAC
+     * generated, because its purpose is to ensure that no information
+     * gathered from a legal MAC can be used to help the forgery of
+     * another one, and that comparing two legal MACs gives you no
+     * useful information about the text they cover, because in each
+     * case, the masks are different and pseudorandom.
+     */
+    value128_t mask;
+} aesgcm_ref_poly;
+
+static bool aesgcm_ref_poly_available(void)
+{
+    return true;  /* pure software implementation, always available */
+}
+
+/*
+ * Primitive function that takes two uint64_t values representing
+ * polynomials, multiplies them, and returns a value128_t struct
+ * containing the full product.
+ *
+ * Because the input polynomials have maximum degree 63, the output
+ * has max degree 63+63 = 127, not 128. As a result, the topmost bit
+ * of the output is always zero.
+ *
+ * The inside of this function is implemented in the simplest way,
+ * with no attention paid to performance. The important feature of
+ * this implementation is not what's _inside_ this function, but
+ * what's _outside_ it: aesgcm_ref_poly_coeff() tries to minimise the
+ * number of these operations.
+ */
+static value128_t pmul(uint64_t x, uint64_t y)
+{
+    value128_t r;
+    r.hi = r.lo = 0;
+
+    uint64_t bit = 1 & y;
+    r.lo ^= x & -bit;
+
+    for (unsigned i = 1; i < 64; i++) {
+        bit = 1 & (y >> i);
+        uint64_t z = x & -bit;
+        r.lo ^= z << i;
+        r.hi ^= z >> (64-i);
+    }
+
+    return r;
+}
+
+/*
+ * OK, I promised a long comment explaining what's going on in this
+ * implementation, and now it's time.
+ *
+ * The way AES-GCM _itself_ is defined by its own spec, its finite
+ * field consists of polynomials over GF(2), constrained to be 128
+ * bits long by reducing them modulo P = x^128 + x^7 + x^2 + x + 1.
+ * Using the usual binary representation in which bit i is the
+ * coefficient of x^i, that's 0x100000000000000000000000000000087.
+ *
+ * That is, whenever you multiply two polynomials and find a term
+ * x^128, you can replace it with x^7+x^2+x+1. More generally,
+ * x^(128+n) can be replaced with x^(7+n)+x^(2+n)+x^(1+n)+x^n. In
+ * binary terms, a 1 bit at the 128th position or above is replaced by
+ * 0x87 exactly 128 bits further down.
+ *
+ * So you'd think that multiplying two 128-bit polynomials mod P would
+ * be a matter of generating their full 256-bit product in the form of
+ * four words HI:HU:LU:LO, and then reducing it mod P by a two-stage
+ * process of computing HI * 0x87 and XORing it into HU:LU, then
+ * computing HU * 0x87 and XORing it into LU:LO.
+ *
+ * But it's not!
+ *
+ * The reason why not is because when AES-GCM is applied to SSH,
+ * somehow the _bit_ order got reversed. A 16-byte block of data in
+ * memory is converted into a polynomial by regarding bit 7 of the
+ * first byte as the constant term, bit 0 of the first byte as the x^7
+ * coefficient, ..., bit 0 of the last byte as the x^127 coefficient.
+ * So if we load that 16-byte block as a big-endian 128-bit integer,
+ * we end up with it representing a polynomial back to front, with the
+ * constant term at the top and the x^127 bit at the bottom.
+ *
+ * Well, that _shouldn't_ be a problem, right? The nice thing about
+ * polynomial multiplication is that it's essentially reversible. If
+ * you reverse the order of the coefficients of two polynomials, then
+ * the product of the reversed polys is exactly the reversal of the
+ * product of the original ones. So we bit-reverse our modulo
+ * polynomial to get 0x1c2000000000000000000000000000001, and we just
+ * pretend we're working mod that instead.
+ *
+ * And that is basically what we're about to do. But there's one
+ * complication, that arises from the semantics of the polynomial
+ * multiplication function we're using as our primitive operation.
+ *
+ * That function multiplies two polynomials of degree at most 63, to
+ * give one with degree at most 127. So it returns a 128-bit integer
+ * whose low bit is the constant term, and its very highest bit is 0,
+ * and its _next_ highest bit is the product of the high bits of the
+ * two inputs.
+ *
+ * That operation is _not_ symmetric in bit-reversal. If you give it
+ * the 64-bit-wise reversals of two polynomials P,Q, then its output
+ * is not the 128-bit-wise reversal of their product PQ, because that
+ * would require the constant term of PQ to appear in bit 127 of the
+ * output, and in fact it appears in bit 126. So in fact, what we get
+ * is offset by one bit from where we'd like it: it's the bit-reversal
+ * of PQx, not of PQ.
+ *
+ * There's more than one way we could fix this. One approach would be
+ * to work around this off-by-one error by taking the 128-bit output
+ * of pmul() and shifting it left by a bit. Then it _is_ the bitwise
+ * reversal of the 128-bit value we'd have wanted to get, and we could
+ * implement the exact algorithm described above, in fully
+ * bit-reversed form.
+ *
+ * But a 128-bit left shift is not a trivial operation in the vector
+ * architectures that this code is acting as a reference for. So we'd
+ * prefer to find a fix that doesn't need compensation during the
+ * actual per-block multiplication step.
+ *
+ * If we did the obvious thing anyway - compute the unshifted 128-bit
+ * product representing the bit-reversal of PQx, and reduce it mod
+ * 0x1c2000000000000000000000000000001 - then we'd get a result which
+ * is exactly what we want, except that it's got a factor of x in it
+ * that we need to get rid of. The obvious answer is to divide by x
+ * (which is legal and safe, since mod P, x is invertible).
+ *
+ * Dividing a 128-bit polynomial by x is easy in principle. Shift left
+ * (because we're still bit-reversed), and if that shifted a 1 bit off
+ * the top, XOR 0xc2000000000000000000000000000001 into the remaining
+ * 128 bits.
+ *
+ * But we're back to having that expensive left shift. What can we do
+ * about that?
+ *
+ * Happily, one of the two input values to our per-block multiply
+ * operation is fixed! It's given to us at key setup stage, and never
+ * changed until the next rekey. So if at key setup time we do this
+ * left shift business _once_, replacing the logical value Q with Q/x,
+ * then that exactly cancels out the unwanted factor of x that shows
+ * up in our multiply operation. And now it doesn't matter that it's
+ * expensive (in the sense of 'a few more instructions than you'd
+ * like'), because it only happens once per SSH key exchange, not once
+ * per 16 bytes of data transferred.
+ */
+
+static void aesgcm_ref_poly_setkey_impl(aesgcm_ref_poly *ctx,
+                                        const unsigned char *var)
+{
+    /*
+     * Key setup function. We copy the provided 16-byte 'var'
+     * value into our polynomial. But, as discussed above, we also
+     * need to divide it by x.
+     */
+
+    ctx->var.hi = GET_64BIT_MSB_FIRST(var);
+    ctx->var.lo = GET_64BIT_MSB_FIRST(var + 8);
+
+    uint64_t bit = 1 & (ctx->var.hi >> 63);
+    ctx->var.hi = (ctx->var.hi << 1) ^ (ctx->var.lo >> 63);
+    ctx->var.lo = (ctx->var.lo << 1) ^ bit;
+    ctx->var.hi ^= 0xC200000000000000 & -bit;
+}
+
+static inline void aesgcm_ref_poly_setup(aesgcm_ref_poly *ctx,
+                                         const unsigned char *mask)
+{
+    /*
+     * Set up to start evaluating a particular MAC. Copy in the mask
+     * value for this packet, and initialise acc to zero.
+     */
+
+    ctx->mask.hi = GET_64BIT_MSB_FIRST(mask);
+    ctx->mask.lo = GET_64BIT_MSB_FIRST(mask + 8);
+    ctx->acc.hi = ctx->acc.lo = 0;
+}
+
+static inline void aesgcm_ref_poly_coeff(aesgcm_ref_poly *ctx,
+                                         const unsigned char *coeff)
+{
+    /*
+     * One step of Horner's-rule polynomial evaluation (with each
+     * coefficient of the polynomial being an element of GF(2^128),
+     * itself composed of polynomials over GF(2) mod P).
+     *
+     * We take our accumulator value, add the incoming coefficient
+     * (which means XOR, by GF(2) rules), and multiply by x (that is,
+     * 'var').
+     */
+
+    /*
+     * The addition first, which is easy.
+     */
+    ctx->acc.hi ^= GET_64BIT_MSB_FIRST(coeff);
+    ctx->acc.lo ^= GET_64BIT_MSB_FIRST(coeff + 8);
+
+    /*
+     * First, create the 256-bit product of the two 128-bit
+     * polynomials over GF(2) stored in ctx->acc and ctx->var.
+     *
+     * The obvious way to do this is by four smaller multiplications
+     * of 64x64 -> 128 bits. But we can do better using a single
+     * iteration of the Karatsuba technique, which is actually more
+     * convenient in polynomials over GF(2) than it is in integers,
+     * because there aren't all those awkward carries complicating
+     * things.
+     *
+     * Letting B denote x^64, and imagining our two inputs are split
+     * up into 64-bit chunks ah,al,bh,bl, the product we want is
+     *
+     *         (ah B + al) (bh B + bl)
+     *       = (ah bh) B^2 + (al bh + ah bl) B + (al bl)
+     *
+     * which looks like four smaller multiplications of each of ah,al
+     * with each of bh,bl. But Karatsuba's trick is to first compute
+     *
+     *         (ah + al) (bh + bl)
+     *       = ah bh + al bh + ah bl + al bl
+     *
+     * and then subtract the terms (ah bh) and (al bl), which we had
+     * to compute anyway, to get the middle two terms (al bh + ah bl)
+     * which are our coefficient of B.
+     *
+     * This involves more bookkeeping instructions like XORs, but with
+     * any luck those are faster than the main multiplication.
+     */
+    uint64_t ah = ctx->acc.hi, al = ctx->acc.lo;
+    uint64_t bh = ctx->var.hi, bl = ctx->var.lo;
+    /* Compute the outer two terms */
+    value128_t lo = pmul(al, bl);
+    value128_t hi = pmul(ah, bh);
+    /* Compute the trick product (ah+al)(bh+bl) */
+    value128_t md = pmul(ah ^ al, bh ^ bl);
+    /* Subtract off the outer two terms to get md = al bh + ah bl */
+    md.hi ^= lo.hi ^ hi.hi;
+    md.lo ^= lo.lo ^ hi.lo;
+    /* And add that into the 256-bit value given by hi * x^128 + lo */
+    lo.hi ^= md.lo;
+    hi.lo ^= md.hi;
+
+    /*
+     * OK. Now hi and lo together make up the 256-bit full product.
+     * Now reduce it mod the reversal of the GCM modulus polynomial.
+     * As discussed above, that's 0x1c2000000000000000000000000000001.
+     *
+     * We want the _topmost_ 128 bits of this, because we're working
+     * in a bit-reversed world. So what we fundamentally want to do is
+     * to take our 256-bit product, and add to it the product of its
+     * low 128 bits with 0x1c2000000000000000000000000000001. Then the
+     * top 128 bits will be the output we want.
+     *
+     * Since there's no carrying in this arithmetic, it's enough to
+     * discard the 1 bit at the bottom of that, because it won't
+     * affect anything in the half we're keeping. So it's enough to
+     * add 0x1c2000000000000000000000000000000 * lo to (hi:lo).
+     *
+     * We can only work with 64 bits at a time, so the first thing we
+     * do is to break that up:
+     *
+     *  - add 0x1c200000000000000 * lo.lo to (hi.lo : lo.hi)
+     *  - add 0x1c200000000000000 * lo.hi to (hi.hi : hi.lo)
+     *
+     * But there's still a problem: 0x1c200000000000000 is just too
+     * _big_ to fit in 64 bits. So we have to break it up into the low
+     * 64 bits 0xc200000000000000, and its leading 1. So each of those
+     * steps of the form 'add 0x1c200000000000000 * x to y:z' becomes
+     * 'add 0xc200000000000000 * x to y:z' followed by 'add x to y',
+     * the latter step dealing with the leading 1.
+     */
+
+    /* First step, adding to the middle two words of our number. After
+     * this the lowest word (in lo.lo) is ignored. */
+    value128_t r1 = pmul(0xC200000000000000, lo.lo);
+    hi.lo ^= r1.hi ^ lo.lo;
+    lo.hi ^= r1.lo;
+
+    /* Second of those steps, adding to the top two words, and
+     * discarding lo.hi. */
+    value128_t r2 = pmul(0xC200000000000000, lo.hi);
+    hi.hi ^= r2.hi ^ lo.hi;
+    hi.lo ^= r2.lo;
+
+    /* Now 'hi' is precisely what we have left. */
+    ctx->acc = hi;
+}
+
+static inline void aesgcm_ref_poly_output(aesgcm_ref_poly *ctx,
+                                          unsigned char *output)
+{
+    PUT_64BIT_MSB_FIRST(output, ctx->acc.hi ^ ctx->mask.hi);
+    PUT_64BIT_MSB_FIRST(output + 8, ctx->acc.lo ^ ctx->mask.lo);
+    smemclr(&ctx->acc, 16);
+    smemclr(&ctx->mask, 16);
+}
+
+#define AESGCM_FLAVOUR ref_poly
+#define AESGCM_NAME "reference polynomial-based implementation"
+#include "aesgcm-footer.h"

+ 38 - 0
source/putty/crypto/aesgcm-select.c

@@ -0,0 +1,38 @@
+#include "ssh.h"
+#include "aesgcm.h"
+
+static ssh2_mac *aesgcm_mac_selector_new(const ssh2_macalg *alg,
+                                         ssh_cipher *cipher)
+{
+    static const ssh2_macalg *const real_algs[] = {
+#if HAVE_CLMUL
+        &ssh2_aesgcm_mac_clmul,
+#endif
+#if HAVE_NEON_PMULL
+        &ssh2_aesgcm_mac_neon,
+#endif
+        &ssh2_aesgcm_mac_sw,
+        NULL,
+    };
+
+    for (size_t i = 0; real_algs[i]; i++) {
+        const ssh2_macalg *alg = real_algs[i];
+        const struct aesgcm_extra *alg_extra =
+            (const struct aesgcm_extra *)alg->extra;
+        if (check_aesgcm_availability(alg_extra))
+            return ssh2_mac_new(alg, cipher);
+    }
+
+    /* We should never reach the NULL at the end of the list, because
+     * the last non-NULL entry should be software-only GCM, which is
+     * always available. */
+    unreachable("aesgcm_select ran off the end of its list");
+}
+
+const ssh2_macalg ssh2_aesgcm_mac = {
+    .new = aesgcm_mac_selector_new,
+    .name = "",
+    .etm_name = "", /* Not selectable independently */
+    .len = 16,
+    .keylen = 0,
+};

+ 145 - 0
source/putty/crypto/aesgcm-sw.c

@@ -0,0 +1,145 @@
+/*
+ * Implementation of the GCM polynomial hash in pure software.
+ *
+ * I don't know of a faster way to do this in a side-channel safe
+ * manner than by precomputing a giant table and iterating over the
+ * whole thing.
+ *
+ * The original GCM reference suggests that you precompute the effects
+ * of multiplying a 128-bit value by the fixed key, in the form of a
+ * table indexed by some number of bits of the input value, so that
+ * you end up computing something of the form
+ *
+ *   table1[x & 0xFF] ^ table2[(x>>8) & 0xFF] ^ ... ^ table15[(x>>120) & 0xFF]
+ *
+ * But that was obviously written before cache and timing leaks were
+ * known about. What's a time-safe approach?
+ *
+ * Well, the above technique isn't fixed to 8 bits of input per table.
+ * You could trade off the number of tables against the size of each
+ * table. At one extreme of this tradeoff, you have 128 tables each
+ * indexed by a single input bit - which is to say, you have 128
+ * values, each 128 bits wide, and you XOR together the subset of
+ * those values corresponding to the input bits, which you can do by
+ * making a bitmask out of each input bit using standard constant-
+ * time-coding bit twiddling techniques.
+ *
+ * That's pretty unpleasant when GCM is supposed to be a fast
+ * algorithm, but I don't know of a better approach that meets current
+ * security standards! Suggestions welcome, if they can get through
+ * testsc.
+ */
+
+#include "ssh.h"
+#include "aesgcm.h"
+
+/*
+ * Store a 128-bit value in the most convenient form standard C will
+ * let us, namely two uint64_t giving its most and least significant
+ * halves.
+ */
+typedef struct {
+    uint64_t hi, lo;
+} value128_t;
+
+typedef struct aesgcm_sw {
+    AESGCM_COMMON_FIELDS;
+
+    /* Accumulator for the current evaluation, and mask that will be
+     * XORed in at the end. High  */
+    value128_t acc, mask;
+
+    /*
+     * Table of values to XOR in for each bit, representing the effect
+     * of multiplying by the fixed key. The key itself doesn't need to
+     * be stored separately, because it's never used. (However, it is
+     * also the first entry in the table, so if you _do_ need it,
+     * there it is.)
+     *
+     * Table is indexed from the low bit of the input upwards.
+     */
+    value128_t table[128];
+} aesgcm_sw;
+
+static bool aesgcm_sw_available(void)
+{
+    return true;  /* pure software implementation, always available */
+}
+
+static void aesgcm_sw_setkey_impl(aesgcm_sw *gcm, const unsigned char *var)
+{
+    value128_t v;
+    v.hi = GET_64BIT_MSB_FIRST(var);
+    v.lo = GET_64BIT_MSB_FIRST(var + 8);
+
+    /*
+     * Prepare the table. This has to be done in reverse order, so
+     * that the original value of the variable corresponds to
+     * table[127], because AES-GCM works in the bit-reversal of its
+     * logical specification so that's where the logical constant term
+     * lives. (See more detailed comment in aesgcm-ref-poly.c.)
+     */
+    for (size_t i = 0; i < 128; i++) {
+        gcm->table[127 - i] = v;
+
+        /* Multiply v by x, which means shifting right (bit reversal
+         * again) and then adding 0xE1 at the top if we shifted a 1 out. */
+        uint64_t lobit = v.lo & 1;
+        v.lo = (v.lo >> 1) ^ (v.hi << 63);
+        v.hi = (v.hi >> 1) ^ (0xE100000000000000ULL & -lobit);
+    }
+}
+
+static inline void aesgcm_sw_setup(aesgcm_sw *gcm, const unsigned char *mask)
+{
+    gcm->mask.hi = GET_64BIT_MSB_FIRST(mask);
+    gcm->mask.lo = GET_64BIT_MSB_FIRST(mask + 8);
+    gcm->acc.hi = gcm->acc.lo = 0;
+}
+
+static inline void aesgcm_sw_coeff(aesgcm_sw *gcm, const unsigned char *coeff)
+{
+    /* XOR in the new coefficient */
+    gcm->acc.hi ^= GET_64BIT_MSB_FIRST(coeff);
+    gcm->acc.lo ^= GET_64BIT_MSB_FIRST(coeff + 8);
+
+    /* And now just loop over the bits of acc, making up a new value
+     * by XORing together the entries of 'table' corresponding to set
+     * bits. */
+
+    value128_t out;
+    out.lo = out.hi = 0;
+
+    const value128_t *tableptr = gcm->table;
+
+    for (size_t i = 0; i < 64; i++) {
+        uint64_t bit = 1 & gcm->acc.lo;
+        gcm->acc.lo >>= 1;
+        uint64_t mask = -bit;
+        out.hi ^= mask & tableptr->hi;
+        out.lo ^= mask & tableptr->lo;
+        tableptr++;
+    }
+    for (size_t i = 0; i < 64; i++) {
+        uint64_t bit = 1 & gcm->acc.hi;
+        gcm->acc.hi >>= 1;
+        uint64_t mask = -bit;
+        out.hi ^= mask & tableptr->hi;
+        out.lo ^= mask & tableptr->lo;
+        tableptr++;
+    }
+
+    gcm->acc = out;
+}
+
+static inline void aesgcm_sw_output(aesgcm_sw *gcm, unsigned char *output)
+{
+    PUT_64BIT_MSB_FIRST(output, gcm->acc.hi ^ gcm->mask.hi);
+    PUT_64BIT_MSB_FIRST(output + 8, gcm->acc.lo ^ gcm->mask.lo);
+    smemclr(&gcm->acc, 16);
+    smemclr(&gcm->mask, 16);
+}
+
+#define AESGCM_FLAVOUR sw
+#define AESGCM_NAME "unaccelerated"
+#include "aesgcm-footer.h"

+ 44 - 0
source/putty/crypto/aesgcm.h

@@ -0,0 +1,44 @@
+/*
+ * Common parts of the state structure for AESGCM MAC implementations.
+ */
+#define AESGCM_COMMON_FIELDS                    \
+    ssh_cipher *cipher;                         \
+    unsigned char partblk[16];                  \
+    size_t skiplen, aadlen, ciphertextlen;      \
+    size_t skipgot, aadgot, partlen;            \
+    BinarySink_IMPLEMENTATION;                  \
+    ssh2_mac mac
+
+/*
+ * The 'extra' structure is used to include information about how to
+ * check if a given implementation is available at run time, and
+ * whether we've already checked.
+ */
+struct aesgcm_extra_mutable;
+struct aesgcm_extra {
+    /* Function to check availability. Might be expensive, so we don't
+     * want to call it more than once. */
+    bool (*check_available)(void);
+
+    /* Point to a writable substructure. */
+    struct aesgcm_extra_mutable *mut;
+
+    /*
+     * Extra API function specific to this MAC type that allows
+     * testcrypt to set more general lengths for skiplen and aadlen.
+     */
+    void (*set_prefix_lengths)(ssh2_mac *mac, size_t skip, size_t aad);
+};
+struct aesgcm_extra_mutable {
+    bool checked_availability;
+    bool is_available;
+};
+static inline bool check_aesgcm_availability(const struct aesgcm_extra *extra)
+{
+    if (!extra->mut->checked_availability) {
+        extra->mut->is_available = extra->check_available();
+        extra->mut->checked_availability = true;
+    }
+
+    return extra->mut->is_available;
+}

+ 2 - 0
source/putty/crypto/arcfour.c

@@ -110,6 +110,7 @@ const ssh_cipheralg ssh_arcfour128_ssh2 = {
     .setkey = arcfour_ssh2_setkey,
     .encrypt = arcfour_ssh2_block,
     .decrypt = arcfour_ssh2_block,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "arcfour128",
     .blksize = 1,
     .real_keybits = 128,
@@ -125,6 +126,7 @@ const ssh_cipheralg ssh_arcfour256_ssh2 = {
     .setkey = arcfour_ssh2_setkey,
     .encrypt = arcfour_ssh2_block,
     .decrypt = arcfour_ssh2_block,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "arcfour256",
     .blksize = 1,
     .real_keybits = 256,

+ 9 - 9
source/putty/crypto/bcrypt.c

@@ -11,8 +11,8 @@
 #include "ssh.h"
 #include "blowfish.h"
 
-BlowfishContext *bcrypt_setup(const unsigned char *key, int keybytes,
-                              const unsigned char *salt, int saltbytes)
+static BlowfishContext *bcrypt_setup(const unsigned char *key, int keybytes,
+                                     const unsigned char *salt, int saltbytes)
 {
     int i;
     BlowfishContext *ctx;
@@ -32,9 +32,9 @@ BlowfishContext *bcrypt_setup(const unsigned char *key, int keybytes,
     return ctx;
 }
 
-void bcrypt_hash(const unsigned char *key, int keybytes,
-                 const unsigned char *salt, int saltbytes,
-                 unsigned char output[32])
+static void bcrypt_hash(const unsigned char *key, int keybytes,
+                        const unsigned char *salt, int saltbytes,
+                        unsigned char output[32])
 {
     BlowfishContext *ctx;
     int i;
@@ -49,10 +49,10 @@ void bcrypt_hash(const unsigned char *key, int keybytes,
     blowfish_free_context(ctx);
 }
 
-void bcrypt_genblock(int counter,
-                     const unsigned char hashed_passphrase[64],
-                     const unsigned char *salt, int saltbytes,
-                     unsigned char output[32])
+static void bcrypt_genblock(int counter,
+                            const unsigned char hashed_passphrase[64],
+                            const unsigned char *salt, int saltbytes,
+                            unsigned char output[32])
 {
     unsigned char hashed_salt[64];
 

+ 12 - 9
source/putty/crypto/blowfish.c

@@ -234,7 +234,7 @@ static const uint32_t sbox3[] = {
 #define ROUND(n) ( xL ^= P[n], t = xL, xL = F(xL) ^ xR, xR = t )
 
 static void blowfish_encrypt(uint32_t xL, uint32_t xR, uint32_t *output,
-                             BlowfishContext * ctx)
+                             BlowfishContext *ctx)
 {
     uint32_t *S0 = ctx->S0;
     uint32_t *S1 = ctx->S1;
@@ -267,7 +267,7 @@ static void blowfish_encrypt(uint32_t xL, uint32_t xR, uint32_t *output,
 }
 
 static void blowfish_decrypt(uint32_t xL, uint32_t xR, uint32_t *output,
-                             BlowfishContext * ctx)
+                             BlowfishContext *ctx)
 {
     uint32_t *S0 = ctx->S0;
     uint32_t *S1 = ctx->S1;
@@ -300,7 +300,7 @@ static void blowfish_decrypt(uint32_t xL, uint32_t xR, uint32_t *output,
 }
 
 static void blowfish_lsb_encrypt_cbc(unsigned char *blk, int len,
-                                     BlowfishContext * ctx)
+                                     BlowfishContext *ctx)
 {
     uint32_t xL, xR, out[2], iv0, iv1;
 
@@ -327,7 +327,7 @@ static void blowfish_lsb_encrypt_cbc(unsigned char *blk, int len,
     ctx->iv1 = iv1;
 }
 
-void blowfish_lsb_encrypt_ecb(void *vblk, int len, BlowfishContext * ctx)
+void blowfish_lsb_encrypt_ecb(void *vblk, int len, BlowfishContext *ctx)
 {
     unsigned char *blk = (unsigned char *)vblk;
     uint32_t xL, xR, out[2];
@@ -346,7 +346,7 @@ void blowfish_lsb_encrypt_ecb(void *vblk, int len, BlowfishContext * ctx)
 }
 
 static void blowfish_lsb_decrypt_cbc(unsigned char *blk, int len,
-                                     BlowfishContext * ctx)
+                                     BlowfishContext *ctx)
 {
     uint32_t xL, xR, out[2], iv0, iv1;
 
@@ -374,7 +374,7 @@ static void blowfish_lsb_decrypt_cbc(unsigned char *blk, int len,
 }
 
 static void blowfish_msb_encrypt_cbc(unsigned char *blk, int len,
-                                     BlowfishContext * ctx)
+                                     BlowfishContext *ctx)
 {
     uint32_t xL, xR, out[2], iv0, iv1;
 
@@ -402,7 +402,7 @@ static void blowfish_msb_encrypt_cbc(unsigned char *blk, int len,
 }
 
 static void blowfish_msb_decrypt_cbc(unsigned char *blk, int len,
-                                     BlowfishContext * ctx)
+                                     BlowfishContext *ctx)
 {
     uint32_t xL, xR, out[2], iv0, iv1;
 
@@ -430,7 +430,7 @@ static void blowfish_msb_decrypt_cbc(unsigned char *blk, int len,
 }
 
 static void blowfish_msb_sdctr(unsigned char *blk, int len,
-                                     BlowfishContext * ctx)
+                               BlowfishContext *ctx)
 {
     uint32_t b[2], iv0, iv1, tmp;
 
@@ -471,7 +471,7 @@ void blowfish_initkey(BlowfishContext *ctx)
     }
 }
 
-void blowfish_expandkey(BlowfishContext * ctx,
+void blowfish_expandkey(BlowfishContext *ctx,
                         const void *vkey, short keybytes,
                         const void *vsalt, short saltbytes)
 {
@@ -654,6 +654,7 @@ const ssh_cipheralg ssh_blowfish_ssh1 = {
     .setkey = blowfish_ssh_setkey,
     .encrypt = blowfish_ssh1_encrypt_blk,
     .decrypt = blowfish_ssh1_decrypt_blk,
+    .next_message = nullcipher_next_message,
     .blksize = 8,
     .real_keybits = 128,
     .padded_keybytes = SSH1_SESSION_KEY_LENGTH,
@@ -668,6 +669,7 @@ const ssh_cipheralg ssh_blowfish_ssh2 = {
     .setkey = blowfish_ssh_setkey,
     .encrypt = blowfish_ssh2_encrypt_blk,
     .decrypt = blowfish_ssh2_decrypt_blk,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "blowfish-cbc",
     .blksize = 8,
     .real_keybits = 128,
@@ -683,6 +685,7 @@ const ssh_cipheralg ssh_blowfish_ssh2_ctr = {
     .setkey = blowfish_ssh_setkey,
     .encrypt = blowfish_ssh2_sdctr,
     .decrypt = blowfish_ssh2_sdctr,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "blowfish-ctr",
     .blksize = 8,
     .real_keybits = 256,

+ 22 - 5
source/putty/crypto/chacha20-poly1305.c

@@ -869,6 +869,7 @@ struct ccp_context {
     BinarySink_IMPLEMENTATION;
     ssh_cipher ciph;
     ssh2_mac mac_if;
+    bool ciph_allocated, mac_allocated;
 };
 
 static ssh2_mac *poly_ssh2_new(
@@ -876,13 +877,27 @@ static ssh2_mac *poly_ssh2_new(
 {
     struct ccp_context *ctx = container_of(cipher, struct ccp_context, ciph);
     ctx->mac_if.vt = alg;
+    ctx->mac_allocated = true;
     BinarySink_DELEGATE_INIT(&ctx->mac_if, ctx);
     return &ctx->mac_if;
 }
 
+static void ccp_common_free(struct ccp_context *ctx)
+{
+    if (ctx->ciph_allocated || ctx->mac_allocated)
+        return;
+
+    smemclr(&ctx->a_cipher, sizeof(ctx->a_cipher));
+    smemclr(&ctx->b_cipher, sizeof(ctx->b_cipher));
+    smemclr(&ctx->mac, sizeof(ctx->mac));
+    sfree(ctx);
+}
+
 static void poly_ssh2_free(ssh2_mac *mac)
 {
-    /* Not allocated, just forwarded, no need to free */
+    struct ccp_context *ctx = container_of(mac, struct ccp_context, mac_if);
+    ctx->mac_allocated = false;
+    ccp_common_free(ctx);
 }
 
 static void poly_setkey(ssh2_mac *mac, ptrlen key)
@@ -949,6 +964,7 @@ const ssh2_macalg ssh2_poly1305 = {
     .setkey = poly_setkey,
     .start = poly_start,
     .genresult = poly_genresult,
+    .next_message = nullmac_next_message,
     .text_name = poly_text_name,
     .name = "",
     .etm_name = "", /* Not selectable individually, just part of
@@ -963,16 +979,16 @@ static ssh_cipher *ccp_new(const ssh_cipheralg *alg)
     BinarySink_INIT(ctx, poly_BinarySink_write);
     poly1305_init(&ctx->mac);
     ctx->ciph.vt = alg;
+    ctx->ciph_allocated = true;
+    ctx->mac_allocated = false;
     return &ctx->ciph;
 }
 
 static void ccp_free(ssh_cipher *cipher)
 {
     struct ccp_context *ctx = container_of(cipher, struct ccp_context, ciph);
-    smemclr(&ctx->a_cipher, sizeof(ctx->a_cipher));
-    smemclr(&ctx->b_cipher, sizeof(ctx->b_cipher));
-    smemclr(&ctx->mac, sizeof(ctx->mac));
-    sfree(ctx);
+    ctx->ciph_allocated = false;
+    ccp_common_free(ctx);
 }
 
 static void ccp_iv(ssh_cipher *cipher, const void *iv)
@@ -1046,6 +1062,7 @@ const ssh_cipheralg ssh2_chacha20_poly1305 = {
     .decrypt = ccp_decrypt,
     .encrypt_length = ccp_encrypt_length,
     .decrypt_length = ccp_decrypt_length,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "[email protected]",
     .blksize = 1,
     .real_keybits = 512,

+ 6 - 1
source/putty/crypto/des.c

@@ -296,7 +296,7 @@ static inline uint32_t des_S(uint32_t si6420, uint32_t si7531)
         s73 ^= c73 & t->t73; c73 += 0x00080008;
     }
     debug("S out: s40=%08"PRIx32" s62=%08"PRIx32
-           " s51=%08"PRIx32" s73=%08"PRIx32"\n", s40, s62, s51, s73);
+          " s51=%08"PRIx32" s73=%08"PRIx32"\n", s40, s62, s51, s73);
 
     /* Final selection within each pair */
     s40 ^= (s40 << 4) & ((0xf000/0x004) * (c40 & 0x00040004));
@@ -689,6 +689,7 @@ const ssh_cipheralg ssh_des = {
     .setkey = des_cbc_setkey,
     .encrypt = des_cbc_encrypt,
     .decrypt = des_cbc_decrypt,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "des-cbc",
     .blksize = 8,
     .real_keybits = 56,
@@ -705,6 +706,7 @@ const ssh_cipheralg ssh_des_sshcom_ssh2 = {
     .setkey = des_cbc_setkey,
     .encrypt = des_cbc_encrypt,
     .decrypt = des_cbc_decrypt,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "[email protected]",
     .blksize = 8,
     .real_keybits = 56,
@@ -808,6 +810,7 @@ const ssh_cipheralg ssh_3des_ssh2 = {
     .setkey = des3_cbc1_setkey,
     .encrypt = des3_cbc1_cbc_encrypt,
     .decrypt = des3_cbc1_cbc_decrypt,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "3des-cbc",
     .blksize = 8,
     .real_keybits = 168,
@@ -905,6 +908,7 @@ const ssh_cipheralg ssh_3des_ssh2_ctr = {
     .setkey = des3_sdctr_setkey,
     .encrypt = des3_sdctr_encrypt_decrypt,
     .decrypt = des3_sdctr_encrypt_decrypt,
+    .next_message = nullcipher_next_message,
     .ssh2_id = "3des-ctr",
     .blksize = 8,
     .real_keybits = 168,
@@ -1040,6 +1044,7 @@ const ssh_cipheralg ssh_3des_ssh1 = {
     .setkey = des3_cbc3_setkey,
     .encrypt = des3_cbc3_cbc_encrypt,
     .decrypt = des3_cbc3_cbc_decrypt,
+    .next_message = nullcipher_next_message,
     .blksize = 8,
     .real_keybits = 168,
     .padded_keybytes = 24,

File diff suppressed because it is too large
+ 32 - 0
source/putty/crypto/diffie-hellman.c


+ 16 - 2
source/putty/crypto/dsa.c

@@ -317,6 +317,12 @@ static void dsa_openssh_blob(ssh_key *key, BinarySink *bs)
     put_mp_ssh2(bs, dsa->x);
 }
 
+static bool dsa_has_private(ssh_key *key)
+{
+    struct dsa_key *dsa = container_of(key, struct dsa_key, sshk);
+    return dsa->x != NULL;
+}
+
 static int dsa_pubkey_bits(const ssh_keyalg *self, ptrlen pub)
 {
     ssh_key *sshk;
@@ -335,8 +341,8 @@ static int dsa_pubkey_bits(const ssh_keyalg *self, ptrlen pub)
 }
 
 mp_int *dsa_gen_k(const char *id_string, mp_int *modulus,
-                     mp_int *private_key,
-                     unsigned char *digest, int digest_len)
+                  mp_int *private_key,
+                  unsigned char *digest, int digest_len)
 {
     /*
      * The basic DSA signing algorithm is:
@@ -484,6 +490,8 @@ static void dsa_sign(ssh_key *key, ptrlen data, unsigned flags, BinarySink *bs)
     mp_free(s);
 }
 
+static char *dsa_alg_desc(const ssh_keyalg *self) { return dupstr("DSA"); }
+
 const ssh_keyalg ssh_dsa = {
     .new_pub = dsa_new_pub,
     .new_priv = dsa_new_priv,
@@ -495,9 +503,15 @@ const ssh_keyalg ssh_dsa = {
     .public_blob = dsa_public_blob,
     .private_blob = dsa_private_blob,
     .openssh_blob = dsa_openssh_blob,
+    .has_private = dsa_has_private,
     .cache_str = dsa_cache_str,
     .components = dsa_components,
+    .base_key = nullkey_base_key,
     .pubkey_bits = dsa_pubkey_bits,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
+    .alg_desc = dsa_alg_desc,
+    .variable_size = nullkey_variable_size_yes,
     .ssh_id = "ssh-dss",
     .cache_id = "dss",
 };

+ 235 - 124
source/putty/crypto/ecc-ssh.c

@@ -6,7 +6,7 @@
  * References:
  *
  * Elliptic curves in SSH are specified in RFC 5656:
- *   http://tools.ietf.org/html/rfc5656
+ *   https://www.rfc-editor.org/rfc/rfc5656
  *
  * That specification delegates details of public key formatting and a
  * lot of underlying mechanism to SEC 1:
@@ -325,6 +325,9 @@ struct ecsign_extra {
     const unsigned char *oid;
     int oidlen;
 
+    /* Human-readable algorithm description */
+    const char *alg_desc;
+
     /* Some EdDSA instances prefix a string to all hash preimages, to
      * disambiguate which signature variant they're being used with */
     ptrlen hash_prefix;
@@ -787,6 +790,12 @@ static void ecdsa_private_blob(ssh_key *key, BinarySink *bs)
     put_mp_ssh2(bs, ek->privateKey);
 }
 
+static bool ecdsa_has_private(ssh_key *key)
+{
+    struct ecdsa_key *ek = container_of(key, struct ecdsa_key, sshk);
+    return ek->privateKey != NULL;
+}
+
 static void eddsa_private_blob(ssh_key *key, BinarySink *bs)
 {
     struct eddsa_key *ek = container_of(key, struct eddsa_key, sshk);
@@ -796,6 +805,12 @@ static void eddsa_private_blob(ssh_key *key, BinarySink *bs)
     put_mp_le_fixedlen(bs, ek->privateKey, ek->curve->fieldBytes);
 }
 
+static bool eddsa_has_private(ssh_key *key)
+{
+    struct eddsa_key *ek = container_of(key, struct eddsa_key, sshk);
+    return ek->privateKey != NULL;
+}
+
 static ssh_key *ecdsa_new_priv(const ssh_keyalg *alg, ptrlen pub, ptrlen priv)
 {
     ssh_key *sshk = ecdsa_new_pub(alg, pub);
@@ -1239,9 +1254,16 @@ static void eddsa_sign(ssh_key *key, ptrlen data,
     mp_free(s);
 }
 
+static char *ec_alg_desc(const ssh_keyalg *self)
+{
+    const struct ecsign_extra *extra =
+        (const struct ecsign_extra *)self->extra;
+    return dupstr(extra->alg_desc);
+}
+
 static const struct ecsign_extra sign_extra_ed25519 = {
     ec_ed25519, &ssh_sha512,
-    NULL, 0, PTRLEN_DECL_LITERAL(""),
+    NULL, 0, "Ed25519", PTRLEN_DECL_LITERAL(""),
 };
 const ssh_keyalg ssh_ecdsa_ed25519 = {
     .new_pub = eddsa_new_pub,
@@ -1254,9 +1276,15 @@ const ssh_keyalg ssh_ecdsa_ed25519 = {
     .public_blob = eddsa_public_blob,
     .private_blob = eddsa_private_blob,
     .openssh_blob = eddsa_openssh_blob,
+    .has_private = eddsa_has_private,
     .cache_str = eddsa_cache_str,
     .components = eddsa_components,
+    .base_key = nullkey_base_key,
     .pubkey_bits = ec_shared_pubkey_bits,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
+    .alg_desc = ec_alg_desc,
+    .variable_size = nullkey_variable_size_no,
     .ssh_id = "ssh-ed25519",
     .cache_id = "ssh-ed25519",
     .extra = &sign_extra_ed25519,
@@ -1264,7 +1292,7 @@ const ssh_keyalg ssh_ecdsa_ed25519 = {
 
 static const struct ecsign_extra sign_extra_ed448 = {
     ec_ed448, &ssh_shake256_114bytes,
-    NULL, 0, PTRLEN_DECL_LITERAL("SigEd448\0\0"),
+    NULL, 0, "Ed448", PTRLEN_DECL_LITERAL("SigEd448\0\0"),
 };
 const ssh_keyalg ssh_ecdsa_ed448 = {
     .new_pub = eddsa_new_pub,
@@ -1277,9 +1305,15 @@ const ssh_keyalg ssh_ecdsa_ed448 = {
     .public_blob = eddsa_public_blob,
     .private_blob = eddsa_private_blob,
     .openssh_blob = eddsa_openssh_blob,
+    .has_private = eddsa_has_private,
     .cache_str = eddsa_cache_str,
     .components = eddsa_components,
+    .base_key = nullkey_base_key,
     .pubkey_bits = ec_shared_pubkey_bits,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
+    .alg_desc = ec_alg_desc,
+    .variable_size = nullkey_variable_size_no,
     .ssh_id = "ssh-ed448",
     .cache_id = "ssh-ed448",
     .extra = &sign_extra_ed448,
@@ -1291,7 +1325,7 @@ static const unsigned char nistp256_oid[] = {
 };
 static const struct ecsign_extra sign_extra_nistp256 = {
     ec_p256, &ssh_sha256,
-    nistp256_oid, lenof(nistp256_oid),
+    nistp256_oid, lenof(nistp256_oid), "NIST p256",
 };
 const ssh_keyalg ssh_ecdsa_nistp256 = {
     .new_pub = ecdsa_new_pub,
@@ -1304,9 +1338,15 @@ const ssh_keyalg ssh_ecdsa_nistp256 = {
     .public_blob = ecdsa_public_blob,
     .private_blob = ecdsa_private_blob,
     .openssh_blob = ecdsa_openssh_blob,
+    .has_private = ecdsa_has_private,
     .cache_str = ecdsa_cache_str,
     .components = ecdsa_components,
+    .base_key = nullkey_base_key,
     .pubkey_bits = ec_shared_pubkey_bits,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
+    .alg_desc = ec_alg_desc,
+    .variable_size = nullkey_variable_size_no,
     .ssh_id = "ecdsa-sha2-nistp256",
     .cache_id = "ecdsa-sha2-nistp256",
     .extra = &sign_extra_nistp256,
@@ -1318,7 +1358,7 @@ static const unsigned char nistp384_oid[] = {
 };
 static const struct ecsign_extra sign_extra_nistp384 = {
     ec_p384, &ssh_sha384,
-    nistp384_oid, lenof(nistp384_oid),
+    nistp384_oid, lenof(nistp384_oid), "NIST p384",
 };
 const ssh_keyalg ssh_ecdsa_nistp384 = {
     .new_pub = ecdsa_new_pub,
@@ -1331,9 +1371,15 @@ const ssh_keyalg ssh_ecdsa_nistp384 = {
     .public_blob = ecdsa_public_blob,
     .private_blob = ecdsa_private_blob,
     .openssh_blob = ecdsa_openssh_blob,
+    .has_private = ecdsa_has_private,
     .cache_str = ecdsa_cache_str,
     .components = ecdsa_components,
+    .base_key = nullkey_base_key,
     .pubkey_bits = ec_shared_pubkey_bits,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
+    .alg_desc = ec_alg_desc,
+    .variable_size = nullkey_variable_size_no,
     .ssh_id = "ecdsa-sha2-nistp384",
     .cache_id = "ecdsa-sha2-nistp384",
     .extra = &sign_extra_nistp384,
@@ -1345,7 +1391,7 @@ static const unsigned char nistp521_oid[] = {
 };
 static const struct ecsign_extra sign_extra_nistp521 = {
     ec_p521, &ssh_sha512,
-    nistp521_oid, lenof(nistp521_oid),
+    nistp521_oid, lenof(nistp521_oid), "NIST p521",
 };
 const ssh_keyalg ssh_ecdsa_nistp521 = {
     .new_pub = ecdsa_new_pub,
@@ -1358,130 +1404,146 @@ const ssh_keyalg ssh_ecdsa_nistp521 = {
     .public_blob = ecdsa_public_blob,
     .private_blob = ecdsa_private_blob,
     .openssh_blob = ecdsa_openssh_blob,
+    .has_private = ecdsa_has_private,
     .cache_str = ecdsa_cache_str,
     .components = ecdsa_components,
+    .base_key = nullkey_base_key,
     .pubkey_bits = ec_shared_pubkey_bits,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
+    .alg_desc = ec_alg_desc,
+    .variable_size = nullkey_variable_size_no,
     .ssh_id = "ecdsa-sha2-nistp521",
     .cache_id = "ecdsa-sha2-nistp521",
     .extra = &sign_extra_nistp521,
 };
 
 /* ----------------------------------------------------------------------
- * Exposed ECDH interface
+ * Exposed ECDH interfaces
  */
 
 struct eckex_extra {
     struct ec_curve *(*curve)(void);
-    void (*setup)(ecdh_key *dh);
-    void (*cleanup)(ecdh_key *dh);
-    void (*getpublic)(ecdh_key *dh, BinarySink *bs);
-    mp_int *(*getkey)(ecdh_key *dh, ptrlen remoteKey);
 };
 
-struct ecdh_key {
+typedef struct ecdh_key_w {
     const struct eckex_extra *extra;
     const struct ec_curve *curve;
     mp_int *private;
-    union {
-        WeierstrassPoint *w_public;
-        MontgomeryPoint *m_public;
-    };
-};
+    WeierstrassPoint *w_public;
+
+    ecdh_key ek;
+} ecdh_key_w;
 
-const char *ssh_ecdhkex_curve_textname(const ssh_kex *kex)
+typedef struct ecdh_key_m {
+    const struct eckex_extra *extra;
+    const struct ec_curve *curve;
+    mp_int *private;
+    MontgomeryPoint *m_public;
+
+    ecdh_key ek;
+} ecdh_key_m;
+
+static ecdh_key *ssh_ecdhkex_w_new(const ssh_kex *kex, bool is_server)
 {
     const struct eckex_extra *extra = (const struct eckex_extra *)kex->extra;
-    struct ec_curve *curve = extra->curve();
-    return curve->textname;
-}
+    const struct ec_curve *curve = extra->curve();
+
+    ecdh_key_w *dhw = snew(ecdh_key_w);
+    dhw->ek.vt = kex->ecdh_vt;
+    dhw->extra = extra;
+    dhw->curve = curve;
 
-static void ssh_ecdhkex_w_setup(ecdh_key *dh)
-{
     mp_int *one = mp_from_integer(1);
-    dh->private = mp_random_in_range(one, dh->curve->w.G_order);
+    dhw->private = mp_random_in_range(one, dhw->curve->w.G_order);
     mp_free(one);
 
-    dh->w_public = ecc_weierstrass_multiply(dh->curve->w.G, dh->private);
+    dhw->w_public = ecc_weierstrass_multiply(dhw->curve->w.G, dhw->private);
+
+    return &dhw->ek;
 }
 
-static void ssh_ecdhkex_m_setup(ecdh_key *dh)
+static ecdh_key *ssh_ecdhkex_m_new(const ssh_kex *kex, bool is_server)
 {
+    const struct eckex_extra *extra = (const struct eckex_extra *)kex->extra;
+    const struct ec_curve *curve = extra->curve();
+
+    ecdh_key_m *dhm = snew(ecdh_key_m);
+    dhm->ek.vt = kex->ecdh_vt;
+    dhm->extra = extra;
+    dhm->curve = curve;
+
     strbuf *bytes = strbuf_new_nm();
-    random_read(strbuf_append(bytes, dh->curve->fieldBytes),
-                dh->curve->fieldBytes);
+    random_read(strbuf_append(bytes, dhm->curve->fieldBytes),
+                dhm->curve->fieldBytes);
 
-    dh->private = mp_from_bytes_le(ptrlen_from_strbuf(bytes));
+    dhm->private = mp_from_bytes_le(ptrlen_from_strbuf(bytes));
 
     /* Ensure the private key has the highest valid bit set, and no
      * bits _above_ the highest valid one */
-    mp_reduce_mod_2to(dh->private, dh->curve->fieldBits);
-    mp_set_bit(dh->private, dh->curve->fieldBits - 1, 1);
+    mp_reduce_mod_2to(dhm->private, dhm->curve->fieldBits);
+    mp_set_bit(dhm->private, dhm->curve->fieldBits - 1, 1);
 
     /* Clear a curve-specific number of low bits */
-    for (unsigned bit = 0; bit < dh->curve->m.log2_cofactor; bit++)
-        mp_set_bit(dh->private, bit, 0);
+    for (unsigned bit = 0; bit < dhm->curve->m.log2_cofactor; bit++)
+        mp_set_bit(dhm->private, bit, 0);
 
     strbuf_free(bytes);
 
-    dh->m_public = ecc_montgomery_multiply(dh->curve->m.G, dh->private);
-}
-
-ecdh_key *ssh_ecdhkex_newkey(const ssh_kex *kex)
-{
-    const struct eckex_extra *extra = (const struct eckex_extra *)kex->extra;
-    const struct ec_curve *curve = extra->curve();
+    dhm->m_public = ecc_montgomery_multiply(dhm->curve->m.G, dhm->private);
 
-    ecdh_key *dh = snew(ecdh_key);
-    dh->extra = extra;
-    dh->curve = curve;
-    dh->extra->setup(dh);
-    return dh;
+    return &dhm->ek;
 }
 
 static void ssh_ecdhkex_w_getpublic(ecdh_key *dh, BinarySink *bs)
 {
-    put_wpoint(bs, dh->w_public, dh->curve, true);
+    ecdh_key_w *dhw = container_of(dh, ecdh_key_w, ek);
+    put_wpoint(bs, dhw->w_public, dhw->curve, true);
 }
 
 static void ssh_ecdhkex_m_getpublic(ecdh_key *dh, BinarySink *bs)
 {
+    ecdh_key_m *dhm = container_of(dh, ecdh_key_m, ek);
     mp_int *x;
-    ecc_montgomery_get_affine(dh->m_public, &x);
-    for (size_t i = 0; i < dh->curve->fieldBytes; ++i)
+    ecc_montgomery_get_affine(dhm->m_public, &x);
+    for (size_t i = 0; i < dhm->curve->fieldBytes; ++i)
         put_byte(bs, mp_get_byte(x, i));
     mp_free(x);
 }
 
-void ssh_ecdhkex_getpublic(ecdh_key *dh, BinarySink *bs)
+static bool ssh_ecdhkex_w_getkey(ecdh_key *dh, ptrlen remoteKey,
+                                 BinarySink *bs)
 {
-    dh->extra->getpublic(dh, bs);
-}
+    ecdh_key_w *dhw = container_of(dh, ecdh_key_w, ek);
 
-static mp_int *ssh_ecdhkex_w_getkey(ecdh_key *dh, ptrlen remoteKey)
-{
-    WeierstrassPoint *remote_p = ecdsa_decode(remoteKey, dh->curve);
+    WeierstrassPoint *remote_p = ecdsa_decode(remoteKey, dhw->curve);
     if (!remote_p)
-        return NULL;
+        return false;
 
     if (ecc_weierstrass_is_identity(remote_p)) {
         /* Not a sensible Diffie-Hellman input value */
         ecc_weierstrass_point_free(remote_p);
-        return NULL;
+        return false;
     }
 
-    WeierstrassPoint *p = ecc_weierstrass_multiply(remote_p, dh->private);
+    WeierstrassPoint *p = ecc_weierstrass_multiply(remote_p, dhw->private);
 
     mp_int *x;
     ecc_weierstrass_get_affine(p, &x, NULL);
+    put_mp_ssh2(bs, x);
+    mp_free(x);
 
     ecc_weierstrass_point_free(remote_p);
     ecc_weierstrass_point_free(p);
 
-    return x;
+    return true;
 }
 
-static mp_int *ssh_ecdhkex_m_getkey(ecdh_key *dh, ptrlen remoteKey)
+static bool ssh_ecdhkex_m_getkey(ecdh_key *dh, ptrlen remoteKey,
+                                 BinarySink *bs)
 {
+    ecdh_key_m *dhm = container_of(dh, ecdh_key_m, ek);
+
     mp_int *remote_x = mp_from_bytes_le(remoteKey);
 
     /* Per RFC 7748 section 5, discard any set bits of the other
@@ -1489,18 +1551,18 @@ static mp_int *ssh_ecdhkex_m_getkey(ecdh_key *dh, ptrlen remoteKey)
      * to represent all valid values. However, an overlarge value that
      * still fits into the remaining number of bits is accepted, and
      * will be reduced mod p. */
-    mp_reduce_mod_2to(remote_x, dh->curve->fieldBits);
+    mp_reduce_mod_2to(remote_x, dhm->curve->fieldBits);
 
     MontgomeryPoint *remote_p = ecc_montgomery_point_new(
-        dh->curve->m.mc, remote_x);
+        dhm->curve->m.mc, remote_x);
     mp_free(remote_x);
 
-    MontgomeryPoint *p = ecc_montgomery_multiply(remote_p, dh->private);
+    MontgomeryPoint *p = ecc_montgomery_multiply(remote_p, dhm->private);
 
     if (ecc_montgomery_is_identity(p)) {
         ecc_montgomery_point_free(remote_p);
         ecc_montgomery_point_free(p);
-        return NULL;
+        return false;
     }
 
     mp_int *x;
@@ -1524,100 +1586,138 @@ static mp_int *ssh_ecdhkex_m_getkey(ecdh_key *dh, ptrlen remoteKey)
      * with the _low_ byte zero, i.e. a multiple of 256.
      */
     strbuf *sb = strbuf_new();
-    for (size_t i = 0; i < dh->curve->fieldBytes; ++i)
+    for (size_t i = 0; i < dhm->curve->fieldBytes; ++i)
         put_byte(sb, mp_get_byte(x, i));
     mp_free(x);
     x = mp_from_bytes_be(ptrlen_from_strbuf(sb));
     strbuf_free(sb);
+    put_mp_ssh2(bs, x);
+    mp_free(x);
 
-    return x;
+    return true;
 }
 
-mp_int *ssh_ecdhkex_getkey(ecdh_key *dh, ptrlen remoteKey)
+static void ssh_ecdhkex_w_free(ecdh_key *dh)
 {
-    return dh->extra->getkey(dh, remoteKey);
+    ecdh_key_w *dhw = container_of(dh, ecdh_key_w, ek);
+    mp_free(dhw->private);
+    ecc_weierstrass_point_free(dhw->w_public);
+    sfree(dhw);
 }
 
-static void ssh_ecdhkex_w_cleanup(ecdh_key *dh)
+static void ssh_ecdhkex_m_free(ecdh_key *dh)
 {
-    ecc_weierstrass_point_free(dh->w_public);
+    ecdh_key_m *dhm = container_of(dh, ecdh_key_m, ek);
+    mp_free(dhm->private);
+    ecc_montgomery_point_free(dhm->m_public);
+    sfree(dhm);
 }
 
-static void ssh_ecdhkex_m_cleanup(ecdh_key *dh)
+static char *ssh_ecdhkex_description(const ssh_kex *kex)
 {
-    ecc_montgomery_point_free(dh->m_public);
+    const struct eckex_extra *extra = (const struct eckex_extra *)kex->extra;
+    const struct ec_curve *curve = extra->curve();
+    return dupprintf("ECDH key exchange with curve %s", curve->textname);
 }
 
-void ssh_ecdhkex_freekey(ecdh_key *dh)
-{
-    mp_free(dh->private);
-    dh->extra->cleanup(dh);
-    sfree(dh);
-}
+static const struct eckex_extra kex_extra_curve25519 = { ec_curve25519 };
 
-static const struct eckex_extra kex_extra_curve25519 = {
-    ec_curve25519,
-    ssh_ecdhkex_m_setup,
-    ssh_ecdhkex_m_cleanup,
-    ssh_ecdhkex_m_getpublic,
-    ssh_ecdhkex_m_getkey,
+static const ecdh_keyalg ssh_ecdhkex_m_alg = {
+    .new = ssh_ecdhkex_m_new,
+    .free = ssh_ecdhkex_m_free,
+    .getpublic = ssh_ecdhkex_m_getpublic,
+    .getkey = ssh_ecdhkex_m_getkey,
+    .description = ssh_ecdhkex_description,
 };
 const ssh_kex ssh_ec_kex_curve25519 = {
-    "curve25519-sha256", NULL, KEXTYPE_ECDH,
-    &ssh_sha256, &kex_extra_curve25519,
+    .name = "curve25519-sha256",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &ssh_ecdhkex_m_alg,
+    .extra = &kex_extra_curve25519,
 };
 /* Pre-RFC alias */
-const ssh_kex ssh_ec_kex_curve25519_libssh = {
-    "[email protected]", NULL, KEXTYPE_ECDH,
-    &ssh_sha256, &kex_extra_curve25519,
+static const ssh_kex ssh_ec_kex_curve25519_libssh = {
+    .name = "[email protected]",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &ssh_ecdhkex_m_alg,
+    .extra = &kex_extra_curve25519,
 };
-
-static const struct eckex_extra kex_extra_curve448 = {
-    ec_curve448,
-    ssh_ecdhkex_m_setup,
-    ssh_ecdhkex_m_cleanup,
-    ssh_ecdhkex_m_getpublic,
-    ssh_ecdhkex_m_getkey,
+/* GSSAPI variant */
+static const ssh_kex ssh_ec_kex_curve25519_gss = {
+    .name = "gss-curve25519-sha256-" GSS_KRB5_OID_HASH,
+    .main_type = KEXTYPE_GSS_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &ssh_ecdhkex_m_alg,
+    .extra = &kex_extra_curve25519,
 };
+
+static const struct eckex_extra kex_extra_curve448 = { ec_curve448 };
 const ssh_kex ssh_ec_kex_curve448 = {
-    "curve448-sha512", NULL, KEXTYPE_ECDH,
-    &ssh_sha512, &kex_extra_curve448,
+    .name = "curve448-sha512",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha512,
+    .ecdh_vt = &ssh_ecdhkex_m_alg,
+    .extra = &kex_extra_curve448,
 };
 
-static const struct eckex_extra kex_extra_nistp256 = {
-    ec_p256,
-    ssh_ecdhkex_w_setup,
-    ssh_ecdhkex_w_cleanup,
-    ssh_ecdhkex_w_getpublic,
-    ssh_ecdhkex_w_getkey,
+static const ecdh_keyalg ssh_ecdhkex_w_alg = {
+    .new = ssh_ecdhkex_w_new,
+    .free = ssh_ecdhkex_w_free,
+    .getpublic = ssh_ecdhkex_w_getpublic,
+    .getkey = ssh_ecdhkex_w_getkey,
+    .description = ssh_ecdhkex_description,
 };
+static const struct eckex_extra kex_extra_nistp256 = { ec_p256 };
 const ssh_kex ssh_ec_kex_nistp256 = {
-    "ecdh-sha2-nistp256", NULL, KEXTYPE_ECDH,
-    &ssh_sha256, &kex_extra_nistp256,
+    .name = "ecdh-sha2-nistp256",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &ssh_ecdhkex_w_alg,
+    .extra = &kex_extra_nistp256,
 };
-
-static const struct eckex_extra kex_extra_nistp384 = {
-    ec_p384,
-    ssh_ecdhkex_w_setup,
-    ssh_ecdhkex_w_cleanup,
-    ssh_ecdhkex_w_getpublic,
-    ssh_ecdhkex_w_getkey,
+/* GSSAPI variant */
+static const ssh_kex ssh_ec_kex_nistp256_gss = {
+    .name = "gss-nistp256-sha256-" GSS_KRB5_OID_HASH,
+    .main_type = KEXTYPE_GSS_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &ssh_ecdhkex_w_alg,
+    .extra = &kex_extra_nistp256,
 };
+
+static const struct eckex_extra kex_extra_nistp384 = { ec_p384 };
 const ssh_kex ssh_ec_kex_nistp384 = {
-    "ecdh-sha2-nistp384", NULL, KEXTYPE_ECDH,
-    &ssh_sha384, &kex_extra_nistp384,
+    .name = "ecdh-sha2-nistp384",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha384,
+    .ecdh_vt = &ssh_ecdhkex_w_alg,
+    .extra = &kex_extra_nistp384,
 };
-
-static const struct eckex_extra kex_extra_nistp521 = {
-    ec_p521,
-    ssh_ecdhkex_w_setup,
-    ssh_ecdhkex_w_cleanup,
-    ssh_ecdhkex_w_getpublic,
-    ssh_ecdhkex_w_getkey,
+/* GSSAPI variant */
+static const ssh_kex ssh_ec_kex_nistp384_gss = {
+    .name = "gss-nistp384-sha384-" GSS_KRB5_OID_HASH,
+    .main_type = KEXTYPE_GSS_ECDH,
+    .hash = &ssh_sha384,
+    .ecdh_vt = &ssh_ecdhkex_w_alg,
+    .extra = &kex_extra_nistp384,
 };
+
+static const struct eckex_extra kex_extra_nistp521 = { ec_p521 };
 const ssh_kex ssh_ec_kex_nistp521 = {
-    "ecdh-sha2-nistp521", NULL, KEXTYPE_ECDH,
-    &ssh_sha512, &kex_extra_nistp521,
+    .name = "ecdh-sha2-nistp521",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha512,
+    .ecdh_vt = &ssh_ecdhkex_w_alg,
+    .extra = &kex_extra_nistp521,
+};
+/* GSSAPI variant */
+static const ssh_kex ssh_ec_kex_nistp521_gss = {
+    .name = "gss-nistp521-sha512-" GSS_KRB5_OID_HASH,
+    .main_type = KEXTYPE_GSS_ECDH,
+    .hash = &ssh_sha512,
+    .ecdh_vt = &ssh_ecdhkex_w_alg,
+    .extra = &kex_extra_nistp521,
 };
 
 static const ssh_kex *const ec_kex_list[] = {
@@ -1631,13 +1731,24 @@ static const ssh_kex *const ec_kex_list[] = {
 
 const ssh_kexes ssh_ecdh_kex = { lenof(ec_kex_list), ec_kex_list };
 
+static const ssh_kex *const ec_gss_kex_list[] = {
+    &ssh_ec_kex_curve25519_gss,
+    &ssh_ec_kex_nistp521_gss,
+    &ssh_ec_kex_nistp384_gss,
+    &ssh_ec_kex_nistp256_gss,
+};
+
+const ssh_kexes ssh_gssk5_ecdh_kex = {
+    lenof(ec_gss_kex_list), ec_gss_kex_list
+};
+
 /* ----------------------------------------------------------------------
  * Helper functions for finding key algorithms and returning auxiliary
  * data.
  */
 
 const ssh_keyalg *ec_alg_by_oid(int len, const void *oid,
-                                        const struct ec_curve **curve)
+                                const struct ec_curve **curve)
 {
     static const ssh_keyalg *algs_with_oid[] = {
         &ssh_ecdsa_nistp256,

+ 6 - 0
source/putty/crypto/hmac.c

@@ -167,6 +167,7 @@ const ssh2_macalg ssh_hmac_sha256 = {
     .setkey = hmac_key,
     .start = hmac_start,
     .genresult = hmac_genresult,
+    .next_message = nullmac_next_message,
     .text_name = hmac_text_name,
     .name = "hmac-sha2-256",
     .etm_name = "[email protected]",
@@ -182,6 +183,7 @@ const ssh2_macalg ssh_hmac_md5 = {
     .setkey = hmac_key,
     .start = hmac_start,
     .genresult = hmac_genresult,
+    .next_message = nullmac_next_message,
     .text_name = hmac_text_name,
     .name = "hmac-md5",
     .etm_name = "[email protected]",
@@ -198,6 +200,7 @@ const ssh2_macalg ssh_hmac_sha1 = {
     .setkey = hmac_key,
     .start = hmac_start,
     .genresult = hmac_genresult,
+    .next_message = nullmac_next_message,
     .text_name = hmac_text_name,
     .name = "hmac-sha1",
     .etm_name = "[email protected]",
@@ -214,6 +217,7 @@ const ssh2_macalg ssh_hmac_sha1_96 = {
     .setkey = hmac_key,
     .start = hmac_start,
     .genresult = hmac_genresult,
+    .next_message = nullmac_next_message,
     .text_name = hmac_text_name,
     .name = "hmac-sha1-96",
     .etm_name = "[email protected]",
@@ -232,6 +236,7 @@ const ssh2_macalg ssh_hmac_sha1_buggy = {
     .setkey = hmac_key,
     .start = hmac_start,
     .genresult = hmac_genresult,
+    .next_message = nullmac_next_message,
     .text_name = hmac_text_name,
     .name = "hmac-sha1",
     .len = 20,
@@ -249,6 +254,7 @@ const ssh2_macalg ssh_hmac_sha1_96_buggy = {
     .setkey = hmac_key,
     .start = hmac_start,
     .genresult = hmac_genresult,
+    .next_message = nullmac_next_message,
     .text_name = hmac_text_name,
     .name = "hmac-sha1-96",
     .len = 12,

+ 8 - 0
source/putty/crypto/mpint.c

@@ -82,6 +82,14 @@ mp_int *mp_new(size_t maxbits)
     return mp_make_sized(words);
 }
 
+mp_int *mp_resize(mp_int *mp, size_t newmaxbits)
+{
+    mp_int *copy = mp_new(newmaxbits);
+    mp_copy_into(copy, mp);
+    mp_free(mp);
+    return copy;
+}
+
 mp_int *mp_from_integer(uintmax_t n)
 {
     mp_int *x = mp_make_sized(

+ 1160 - 0
source/putty/crypto/openssh-certs.c

@@ -0,0 +1,1160 @@
+/*
+ * Public key type for OpenSSH certificates.
+ */
+
+#include "ssh.h"
+#include "putty.h"
+
+enum {
+    SSH_CERT_TYPE_USER = 1,
+    SSH_CERT_TYPE_HOST = 2,
+};
+
+typedef struct opensshcert_key {
+    strbuf *nonce;
+    uint64_t serial;
+    uint32_t type;
+    strbuf *key_id;
+    strbuf *valid_principals;
+    uint64_t valid_after, valid_before;
+    strbuf *critical_options;
+    strbuf *extensions;
+    strbuf *reserved;
+    strbuf *signature_key;
+    strbuf *signature;
+
+    ssh_key *basekey;
+
+    ssh_key sshk;
+} opensshcert_key;
+
+typedef struct blob_fmt {
+    const unsigned *fmt;
+    size_t len;
+} blob_fmt;
+
+typedef struct opensshcert_extra {
+    /*
+     * OpenSSH certificate formats aren't completely consistent about
+     * the relationship between the public+private blob uploaded to
+     * the agent for the certified key type, and the one for the base
+     * key type. Here we specify the mapping.
+     *
+     * Each of these foo_fmt strings indicates the layout of a
+     * particular version of the key, in the form of an array of
+     * integers together with a length, with each integer describing
+     * one of the components of the key. The integers are defined by
+     * enums, so that they're tightly packed; the general idea is that
+     * if you're converting from one form to another, then you use the
+     * format list for the source format to read out a succession of
+     * SSH strings from the source data and put them in an array
+     * indexed by the integer ids, and then use the list for the
+     * destination format to write the strings out to the destination
+     * in the right (maybe different) order.
+     *
+     * pub_fmt describes the format of the public-key blob for the
+     * base key type, not counting the initial string giving the key
+     * type identifier itself. As far as I know, this always matches
+     * the format of the public-key data appearing in the middle of
+     * the certificate.
+     *
+     * base_ossh_fmt describes the format of the full OpenSSH blob
+     * appearing in the ssh-agent protocol for the base key,
+     * containing the public and private key data.
+     *
+     * cert_ossh_fmt describes the format of the OpenSSH blob for the
+     * certificate key format, beginning just after the certificate
+     * string itself.
+     */
+    blob_fmt pub_fmt, base_ossh_fmt, cert_ossh_fmt;
+
+    /*
+     * The RSA-SHA2 algorithm names have their SSH id set to names
+     * like "rsa-sha2-512-cert-...", which is what will be received in
+     * the KEXINIT algorithm list if a host key in one of those
+     * algorithms is presented. But the _key_ type id that will appear
+     * in the public key blob is "ssh-rsa-cert-...". So we need a
+     * separate field to indicate the key type id we expect to see in
+     * certified public keys, and also the one we want to put back
+     * into the artificial public blob we make to pass to the
+     * constructor for the underlying key.
+     *
+     * (In rsa.c this is managed much more simply, because everything
+     * sharing the same vtable wants the same key type id.)
+     */
+    const char *cert_key_ssh_id, *base_key_ssh_id;
+} opensshcert_extra;
+
+/*
+ * The actual integer arrays defining the per-key blob formats.
+ */
+
+/* DSA is the most orthodox: only the obviously necessary public key
+ * info appears at all, it's in the same order everywhere, and none of
+ * it is repeated unnecessarily */
+enum { DSA_p, DSA_q, DSA_g, DSA_y, DSA_x };
+static const unsigned dsa_pub_fmt[] = { DSA_p, DSA_q, DSA_g, DSA_y };
+static const unsigned dsa_base_ossh_fmt[] = {
+    DSA_p, DSA_q, DSA_g, DSA_y, DSA_x };
+static const unsigned dsa_cert_ossh_fmt[] = { DSA_x };
+
+/* ECDSA is almost as nice, except that it pointlessly mentions the
+ * curve name in the public data, which shouldn't be necessary given
+ * that the SSH key id has already implied it. But at least that's
+ * consistent everywhere. */
+enum { ECDSA_curve, ECDSA_point, ECDSA_exp };
+static const unsigned ecdsa_pub_fmt[] = { ECDSA_curve, ECDSA_point };
+static const unsigned ecdsa_base_ossh_fmt[] = {
+    ECDSA_curve, ECDSA_point, ECDSA_exp };
+static const unsigned ecdsa_cert_ossh_fmt[] = { ECDSA_exp };
+
+/* Ed25519 has the oddity that the private data following the
+ * certificate in the OpenSSH blob is preceded by an extra copy of the
+ * public data, for no obviously necessary reason since that doesn't
+ * happen in any of the rest of these formats */
+enum { EDDSA_point, EDDSA_exp };
+static const unsigned eddsa_pub_fmt[] = { EDDSA_point };
+static const unsigned eddsa_base_ossh_fmt[] = { EDDSA_point, EDDSA_exp };
+static const unsigned eddsa_cert_ossh_fmt[] = { EDDSA_point, EDDSA_exp };
+
+/* And RSA has the quirk that the modulus and exponent are reversed in
+ * the base key type's OpenSSH blob! */
+enum { RSA_e, RSA_n, RSA_d, RSA_p, RSA_q, RSA_iqmp };
+static const unsigned rsa_pub_fmt[] = { RSA_e, RSA_n };
+static const unsigned rsa_base_ossh_fmt[] = {
+    RSA_n, RSA_e, RSA_d, RSA_p, RSA_q, RSA_iqmp };
+static const unsigned rsa_cert_ossh_fmt[] = { RSA_d, RSA_p, RSA_q, RSA_iqmp };
+
+/*
+ * Routines to transform one kind of blob into another based on those
+ * foo_fmt integer arrays.
+ */
+typedef struct BlobTransformer {
+    ptrlen *parts;
+    size_t nparts;
+} BlobTransformer;
+
+#define BLOBTRANS_DECLARE(bt) BlobTransformer bt[1] = { { NULL, 0 } }
+
+static inline void blobtrans_clear(BlobTransformer *bt)
+{
+    sfree(bt->parts);
+    bt->parts = NULL;
+    bt->nparts = 0;
+}
+
+static inline bool blobtrans_read(BlobTransformer *bt, BinarySource *src,
+                                  blob_fmt blob)
+{
+    size_t nparts = bt->nparts;
+    for (size_t i = 0; i < blob.len; i++)
+        if (nparts < blob.fmt[i]+1)
+            nparts = blob.fmt[i]+1;
+
+    if (nparts > bt->nparts) {
+        bt->parts = sresize(bt->parts, nparts, ptrlen);
+        while (bt->nparts < nparts)
+            bt->parts[bt->nparts++] = make_ptrlen(NULL, 0);
+    }
+
+    for (size_t i = 0; i < blob.len; i++) {
+        size_t j = blob.fmt[i];
+        ptrlen part = get_string(src);
+        if (bt->parts[j].ptr) {
+            /*
+             * If the same string appears in both the public blob and
+             * the private data, check they match. (This happens in
+             * Ed25519: an extra copy of the public point string
+             * appears in the certified OpenSSH data after the
+             * certificate and before the private key.)
+             */
+            if (!ptrlen_eq_ptrlen(bt->parts[j], part))
+                return false;
+        }
+        bt->parts[j] = part;
+    }
+
+    return true;
+}
+
+static inline void blobtrans_write(BlobTransformer *bt, BinarySink *bs,
+                                   blob_fmt blob)
+{
+    for (size_t i = 0; i < blob.len; i++) {
+        assert(i < bt->nparts);
+        ptrlen part = bt->parts[blob.fmt[i]];
+        assert(part.ptr);
+        put_stringpl(bs, part);
+    }
+}
+
+/*
+ * Forward declarations.
+ */
+static ssh_key *opensshcert_new_pub(const ssh_keyalg *self, ptrlen pub);
+static ssh_key *opensshcert_new_priv(
+    const ssh_keyalg *self, ptrlen pub, ptrlen priv);
+static ssh_key *opensshcert_new_priv_openssh(
+    const ssh_keyalg *self, BinarySource *src);
+static void opensshcert_freekey(ssh_key *key);
+static char *opensshcert_invalid(ssh_key *key, unsigned flags);
+static void opensshcert_sign(ssh_key *key, ptrlen data, unsigned flags,
+                             BinarySink *bs);
+static bool opensshcert_verify(ssh_key *key, ptrlen sig, ptrlen data);
+static void opensshcert_public_blob(ssh_key *key, BinarySink *bs);
+static void opensshcert_private_blob(ssh_key *key, BinarySink *bs);
+static void opensshcert_openssh_blob(ssh_key *key, BinarySink *bs);
+static void opensshcert_ca_public_blob(ssh_key *key, BinarySink *bs);
+static void opensshcert_cert_id_string(ssh_key *key, BinarySink *bs);
+static SeatDialogText *opensshcert_cert_info(ssh_key *key);
+static bool opensshcert_has_private(ssh_key *key);
+static char *opensshcert_cache_str(ssh_key *key);
+static key_components *opensshcert_components(ssh_key *key);
+static ssh_key *opensshcert_base_key(ssh_key *key);
+static bool opensshcert_check_cert(
+    ssh_key *key, bool host, ptrlen principal, uint64_t time,
+    const ca_options *opts, BinarySink *error);
+static int opensshcert_pubkey_bits(const ssh_keyalg *self, ptrlen blob);
+static unsigned opensshcert_supported_flags(const ssh_keyalg *self);
+static const char *opensshcert_alternate_ssh_id(const ssh_keyalg *self,
+                                                unsigned flags);
+static char *opensshcert_alg_desc(const ssh_keyalg *self);
+static bool opensshcert_variable_size(const ssh_keyalg *self);
+static const ssh_keyalg *opensshcert_related_alg(const ssh_keyalg *self,
+                                                 const ssh_keyalg *base);
+
+/*
+ * Top-level vtables for the certified key formats, defined via a list
+ * macro so I can also make an array of them all.
+ */
+
+#define KEYALG_LIST(X)                                                  \
+    X(ssh_dsa, "ssh-dss", "ssh-dss", dsa)                               \
+    X(ssh_rsa, "ssh-rsa", "ssh-rsa", rsa)                               \
+    X(ssh_rsa_sha256, "rsa-sha2-256", "ssh-rsa", rsa)                   \
+    X(ssh_rsa_sha512, "rsa-sha2-512", "ssh-rsa", rsa)                   \
+    X(ssh_ecdsa_ed25519, "ssh-ed25519", "ssh-ed25519", eddsa)           \
+    X(ssh_ecdsa_nistp256, "ecdsa-sha2-nistp256","ecdsa-sha2-nistp256", ecdsa) \
+    X(ssh_ecdsa_nistp384, "ecdsa-sha2-nistp384","ecdsa-sha2-nistp384", ecdsa) \
+    X(ssh_ecdsa_nistp521, "ecdsa-sha2-nistp521","ecdsa-sha2-nistp521", ecdsa) \
+    /* end of list */
+
+#define KEYALG_DEF(name, ssh_alg_id_prefix, ssh_key_id_prefix, fmt_prefix) \
+    static const struct opensshcert_extra opensshcert_##name##_extra = { \
+        .pub_fmt = { .fmt = fmt_prefix ## _pub_fmt,                     \
+                     .len = lenof(fmt_prefix ## _pub_fmt) },            \
+        .base_ossh_fmt = { .fmt = fmt_prefix ## _base_ossh_fmt,         \
+                           .len = lenof(fmt_prefix ## _base_ossh_fmt) }, \
+        .cert_ossh_fmt = { .fmt = fmt_prefix ## _cert_ossh_fmt,         \
+                           .len = lenof(fmt_prefix ## _cert_ossh_fmt) }, \
+        .cert_key_ssh_id = ssh_key_id_prefix "[email protected]",   \
+        .base_key_ssh_id = ssh_key_id_prefix,                           \
+    };                                                                  \
+                                                                        \
+    const ssh_keyalg opensshcert_##name = {                             \
+        .new_pub = opensshcert_new_pub,                                 \
+        .new_priv = opensshcert_new_priv,                               \
+        .new_priv_openssh = opensshcert_new_priv_openssh,               \
+        .freekey = opensshcert_freekey,                                 \
+        .invalid = opensshcert_invalid,                                 \
+        .sign = opensshcert_sign,                                       \
+        .verify = opensshcert_verify,                                   \
+        .public_blob = opensshcert_public_blob,                         \
+        .private_blob = opensshcert_private_blob,                       \
+        .openssh_blob = opensshcert_openssh_blob,                       \
+        .has_private = opensshcert_has_private,                         \
+        .cache_str = opensshcert_cache_str,                             \
+        .components = opensshcert_components,                           \
+        .base_key = opensshcert_base_key,                               \
+        .ca_public_blob = opensshcert_ca_public_blob,                   \
+        .check_cert = opensshcert_check_cert,                           \
+        .cert_id_string = opensshcert_cert_id_string,                   \
+        .cert_info = opensshcert_cert_info,                             \
+        .pubkey_bits = opensshcert_pubkey_bits,                         \
+        .supported_flags = opensshcert_supported_flags,                 \
+        .alternate_ssh_id = opensshcert_alternate_ssh_id,               \
+        .alg_desc = opensshcert_alg_desc,                               \
+        .variable_size = opensshcert_variable_size,                     \
+        .related_alg = opensshcert_related_alg,                         \
+        .ssh_id = ssh_alg_id_prefix "[email protected]",            \
+        .cache_id = "opensshcert-" ssh_key_id_prefix,                   \
+        .extra = &opensshcert_##name##_extra,                           \
+        .is_certificate = true,                                         \
+        .base_alg = &name,                                              \
+    };
+KEYALG_LIST(KEYALG_DEF)
+#undef KEYALG_DEF
+
+#define KEYALG_LIST_ENTRY(name, algid, keyid, fmt) &opensshcert_##name,
+static const ssh_keyalg *const opensshcert_all_keyalgs[] = {
+    KEYALG_LIST(KEYALG_LIST_ENTRY)
+};
+#undef KEYALG_LIST_ENTRY
+
+static strbuf *get_base_public_blob(BinarySource *src,
+                                    const opensshcert_extra *extra)
+{
+    strbuf *basepub = strbuf_new();
+    put_stringz(basepub, extra->base_key_ssh_id);
+
+    /* Make the base public key blob out of the public key
+     * material in the certificate. This invocation of the
+     * blobtrans system doesn't do any format translation, but it
+     * does ensure that the right amount of data is copied so that
+     * src ends up in the right position to read the remaining
+     * certificate fields. */
+    BLOBTRANS_DECLARE(bt);
+    blobtrans_read(bt, src, extra->pub_fmt);
+    blobtrans_write(bt, BinarySink_UPCAST(basepub), extra->pub_fmt);
+    blobtrans_clear(bt);
+
+    return basepub;
+}
+
+static opensshcert_key *opensshcert_new_shared(
+    const ssh_keyalg *self, ptrlen blob, strbuf **basepub_out)
+{
+    const opensshcert_extra *extra = self->extra;
+
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, blob);
+
+    /* Check the initial key-type string */
+    if (!ptrlen_eq_string(get_string(src), extra->cert_key_ssh_id))
+        return NULL;
+
+    opensshcert_key *ck = snew(opensshcert_key);
+    memset(ck, 0, sizeof(*ck));
+    ck->sshk.vt = self;
+
+    ck->nonce = strbuf_dup(get_string(src));
+    strbuf *basepub = get_base_public_blob(src, extra);
+    ck->serial = get_uint64(src);
+    ck->type = get_uint32(src);
+    ck->key_id = strbuf_dup(get_string(src));
+    ck->valid_principals = strbuf_dup(get_string(src));
+    ck->valid_after = get_uint64(src);
+    ck->valid_before = get_uint64(src);
+    ck->critical_options = strbuf_dup(get_string(src));
+    ck->extensions = strbuf_dup(get_string(src));
+    ck->reserved = strbuf_dup(get_string(src));
+    ck->signature_key = strbuf_dup(get_string(src));
+    ck->signature = strbuf_dup(get_string(src));
+
+
+    if (get_err(src)) {
+        ssh_key_free(&ck->sshk);
+        strbuf_free(basepub);
+        return NULL;
+    }
+
+    *basepub_out = basepub;
+    return ck;
+}
+
+static ssh_key *opensshcert_new_pub(const ssh_keyalg *self, ptrlen pub)
+{
+    strbuf *basepub;
+    opensshcert_key *ck = opensshcert_new_shared(self, pub, &basepub);
+    if (!ck)
+        return NULL;
+
+    ck->basekey = ssh_key_new_pub(self->base_alg, ptrlen_from_strbuf(basepub));
+    strbuf_free(basepub);
+
+    if (!ck->basekey) {
+        ssh_key_free(&ck->sshk);
+        return NULL;
+    }
+
+    return &ck->sshk;
+}
+
+static ssh_key *opensshcert_new_priv(
+    const ssh_keyalg *self, ptrlen pub, ptrlen priv)
+{
+    strbuf *basepub;
+    opensshcert_key *ck = opensshcert_new_shared(self, pub, &basepub);
+    if (!ck)
+        return NULL;
+
+    ck->basekey = ssh_key_new_priv(self->base_alg,
+                                   ptrlen_from_strbuf(basepub), priv);
+    strbuf_free(basepub);
+
+    if (!ck->basekey) {
+        ssh_key_free(&ck->sshk);
+        return NULL;
+    }
+
+    return &ck->sshk;
+}
+
+static ssh_key *opensshcert_new_priv_openssh(
+    const ssh_keyalg *self, BinarySource *src)
+{
+    const opensshcert_extra *extra = self->extra;
+
+    ptrlen cert = get_string(src);
+
+    strbuf *basepub;
+    opensshcert_key *ck = opensshcert_new_shared(self, cert, &basepub);
+    if (!ck)
+        return NULL;
+
+    strbuf *baseossh = strbuf_new();
+
+    /* Make the base OpenSSH key blob out of the public key blob
+     * returned from opensshcert_new_shared, and the trailing
+     * private data following the certificate */
+    BLOBTRANS_DECLARE(bt);
+
+    BinarySource pubsrc[1];
+    BinarySource_BARE_INIT_PL(pubsrc, ptrlen_from_strbuf(basepub));
+    get_string(pubsrc);            /* skip key type id */
+
+    /* blobtrans_read might fail in this case, because we're reading
+     * from two sources and they might fail to match */
+    bool success = blobtrans_read(bt, pubsrc, extra->pub_fmt) &&
+        blobtrans_read(bt, src, extra->cert_ossh_fmt);
+
+    blobtrans_write(bt, BinarySink_UPCAST(baseossh), extra->base_ossh_fmt);
+    blobtrans_clear(bt);
+
+    if (!success) {
+        ssh_key_free(&ck->sshk);
+        strbuf_free(basepub);
+        strbuf_free(baseossh);
+        return NULL;
+    }
+
+    strbuf_free(basepub);
+
+    BinarySource osshsrc[1];
+    BinarySource_BARE_INIT_PL(osshsrc, ptrlen_from_strbuf(baseossh));
+    ck->basekey = ssh_key_new_priv_openssh(self->base_alg, osshsrc);
+    strbuf_free(baseossh);
+
+    if (!ck->basekey) {
+        ssh_key_free(&ck->sshk);
+        return NULL;
+    }
+
+    return &ck->sshk;
+}
+
+static void opensshcert_freekey(ssh_key *key)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+
+    /* If this function is called from one of the above constructors
+     * because it failed part way through, we might not have managed
+     * to construct ck->basekey, so it might be NULL. */
+    if (ck->basekey)
+        ssh_key_free(ck->basekey);
+
+    strbuf_free(ck->nonce);
+    strbuf_free(ck->key_id);
+    strbuf_free(ck->valid_principals);
+    strbuf_free(ck->critical_options);
+    strbuf_free(ck->extensions);
+    strbuf_free(ck->reserved);
+    strbuf_free(ck->signature_key);
+    strbuf_free(ck->signature);
+
+    sfree(ck);
+}
+
+static ssh_key *opensshcert_base_key(ssh_key *key)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    return ck->basekey;
+}
+
+/*
+ * Make a public key object from the CA public blob, potentially
+ * taking into account that the signature might override the algorithm
+ * name
+ */
+static ssh_key *opensshcert_ca_pub_key(
+    opensshcert_key *ck, ptrlen sig, ptrlen *algname)
+{
+    ptrlen ca_keyblob = ptrlen_from_strbuf(ck->signature_key);
+
+    ptrlen alg_source = sig.ptr ? sig : ca_keyblob;
+    if (algname)
+        *algname = pubkey_blob_to_alg_name(alg_source);
+
+    const ssh_keyalg *ca_alg = pubkey_blob_to_alg(alg_source);
+    if (!ca_alg)
+        return NULL;  /* don't even recognise the certifying key type */
+
+    return ssh_key_new_pub(ca_alg, ca_keyblob);
+}
+
+static void opensshcert_signature_preimage(opensshcert_key *ck, BinarySink *bs)
+{
+    const opensshcert_extra *extra = ck->sshk.vt->extra;
+    put_stringz(bs, extra->cert_key_ssh_id);
+    put_stringpl(bs, ptrlen_from_strbuf(ck->nonce));
+
+    strbuf *basepub = strbuf_new();
+    ssh_key_public_blob(ck->basekey, BinarySink_UPCAST(basepub));
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(basepub));
+    get_string(src); /* skip initial key type string */
+    put_data(bs, get_ptr(src), get_avail(src));
+    strbuf_free(basepub);
+
+    put_uint64(bs, ck->serial);
+    put_uint32(bs, ck->type);
+    put_stringpl(bs, ptrlen_from_strbuf(ck->key_id));
+    put_stringpl(bs, ptrlen_from_strbuf(ck->valid_principals));
+    put_uint64(bs, ck->valid_after);
+    put_uint64(bs, ck->valid_before);
+    put_stringpl(bs, ptrlen_from_strbuf(ck->critical_options));
+    put_stringpl(bs, ptrlen_from_strbuf(ck->extensions));
+    put_stringpl(bs, ptrlen_from_strbuf(ck->reserved));
+    put_stringpl(bs, ptrlen_from_strbuf(ck->signature_key));
+}
+
+static void opensshcert_public_blob(ssh_key *key, BinarySink *bs)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+
+    opensshcert_signature_preimage(ck, bs);
+    put_stringpl(bs, ptrlen_from_strbuf(ck->signature));
+}
+
+static void opensshcert_private_blob(ssh_key *key, BinarySink *bs)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    ssh_key_private_blob(ck->basekey, bs);
+}
+
+static void opensshcert_openssh_blob(ssh_key *key, BinarySink *bs)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    const opensshcert_extra *extra = key->vt->extra;
+
+    strbuf *cert = strbuf_new();
+    ssh_key_public_blob(key, BinarySink_UPCAST(cert));
+    put_stringsb(bs, cert);
+
+    strbuf *baseossh = strbuf_new_nm();
+    ssh_key_openssh_blob(ck->basekey, BinarySink_UPCAST(baseossh));
+    BinarySource basesrc[1];
+    BinarySource_BARE_INIT_PL(basesrc, ptrlen_from_strbuf(baseossh));
+
+    BLOBTRANS_DECLARE(bt);
+    blobtrans_read(bt, basesrc, extra->base_ossh_fmt);
+    blobtrans_write(bt, bs, extra->cert_ossh_fmt);
+    blobtrans_clear(bt);
+
+    strbuf_free(baseossh);
+}
+
+static void opensshcert_ca_public_blob(ssh_key *key, BinarySink *bs)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    put_datapl(bs, ptrlen_from_strbuf(ck->signature_key));
+}
+
+static void opensshcert_cert_id_string(ssh_key *key, BinarySink *bs)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    put_datapl(bs, ptrlen_from_strbuf(ck->key_id));
+}
+
+static bool opensshcert_has_private(ssh_key *key)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    return ssh_key_has_private(ck->basekey);
+}
+
+static char *opensshcert_cache_str(ssh_key *key)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    return ssh_key_cache_str(ck->basekey);
+}
+
+static void opensshcert_time_to_iso8601(BinarySink *bs, uint64_t time)
+{
+    time_t t = time;
+    char buf[256];
+    put_data(bs, buf, strftime(buf, sizeof(buf),
+                               "%Y-%m-%d %H:%M:%S UTC", gmtime(&t)));
+}
+
+static void opensshcert_string_list_key_components(
+    key_components *kc, strbuf *input, const char *title, const char *title2)
+{
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(input));
+
+    const char *titles[2] = { title, title2 };
+    size_t ntitles = (title2 ? 2 : 1);
+
+    unsigned index = 0;
+    while (get_avail(src)) {
+        for (size_t ti = 0; ti < ntitles; ti++) {
+            ptrlen value = get_string(src);
+            if (get_err(src))
+                break;
+            char *name = dupprintf("%s_%u", titles[ti], index);
+            key_components_add_text_pl(kc, name, value);
+            sfree(name);
+        }
+        index++;
+    }
+}
+
+static key_components *opensshcert_components(ssh_key *key)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    key_components *kc = ssh_key_components(ck->basekey);
+    key_components_add_binary(kc, "cert_nonce", ptrlen_from_strbuf(ck->nonce));
+    key_components_add_uint(kc, "cert_serial", ck->serial);
+    switch (ck->type) {
+      case SSH_CERT_TYPE_HOST:
+        key_components_add_text(kc, "cert_type", "host");
+        break;
+      case SSH_CERT_TYPE_USER:
+        key_components_add_text(kc, "cert_type", "user");
+        break;
+      default:
+        key_components_add_uint(kc, "cert_type", ck->type);
+        break;
+    }
+    key_components_add_text(kc, "cert_key_id", ck->key_id->s);
+    opensshcert_string_list_key_components(kc, ck->valid_principals,
+                                           "cert_valid_principal", NULL);
+    key_components_add_uint(kc, "cert_valid_after", ck->valid_after);
+    key_components_add_uint(kc, "cert_valid_before", ck->valid_before);
+    /* Translate the validity period into human-legible dates, but
+     * only if they're not the min/max integer. Rationale: if you see
+     * "584554051223-11-09 07:00:15 UTC" as the expiry time you'll be
+     * as likely to think it's a weird buffer overflow as half a
+     * trillion years in the future! */
+    if (ck->valid_after != 0) {
+        strbuf *date = strbuf_new();
+        opensshcert_time_to_iso8601(BinarySink_UPCAST(date), ck->valid_after);
+        key_components_add_text_pl(kc, "cert_valid_after_date",
+                                   ptrlen_from_strbuf(date));
+        strbuf_free(date);
+    }
+    if (ck->valid_before != 0xFFFFFFFFFFFFFFFF) {
+        strbuf *date = strbuf_new();
+        opensshcert_time_to_iso8601(BinarySink_UPCAST(date), ck->valid_before);
+        key_components_add_text_pl(kc, "cert_valid_before_date",
+                                   ptrlen_from_strbuf(date));
+        strbuf_free(date);
+    }
+    opensshcert_string_list_key_components(kc, ck->critical_options,
+                                           "cert_critical_option",
+                                           "cert_critical_option_data");
+    opensshcert_string_list_key_components(kc, ck->extensions,
+                                           "cert_extension",
+                                           "cert_extension_data");
+    key_components_add_binary(kc, "cert_ca_key", ptrlen_from_strbuf(
+                                  ck->signature_key));
+
+    ptrlen ca_algname;
+    ssh_key *ca_key = opensshcert_ca_pub_key(ck, make_ptrlen(NULL, 0),
+                                             &ca_algname);
+    key_components_add_text_pl(kc, "cert_ca_key_algorithm_id", ca_algname);
+
+    if (ca_key) {
+        key_components *kc_ca_key = ssh_key_components(ca_key);
+        for (size_t i = 0; i < kc_ca_key->ncomponents; i++) {
+            key_component *comp = &kc_ca_key->components[i];
+            char *subname = dupcat("cert_ca_key_", comp->name);
+            key_components_add_copy(kc, subname, comp);
+            sfree(subname);
+        }
+        key_components_free(kc_ca_key);
+        ssh_key_free(ca_key);
+    }
+
+    key_components_add_binary(kc, "cert_ca_sig", ptrlen_from_strbuf(
+                                  ck->signature));
+    return kc;
+}
+
+static SeatDialogText *opensshcert_cert_info(ssh_key *key)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    SeatDialogText *text = seat_dialog_text_new();
+    strbuf *tmp = strbuf_new();
+
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Certificate type");
+    switch (ck->type) {
+      case SSH_CERT_TYPE_HOST:
+        seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                                "host key");
+        seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                "Valid host names");
+        break;
+      case SSH_CERT_TYPE_USER:
+        seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                                "user authentication key");
+        seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                "Valid user names");
+        break;
+      default:
+        seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                                "unknown type %" PRIu32, ck->type);
+        seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                "Valid principals");
+        break;
+    }
+
+    {
+        BinarySource src[1];
+        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(
+                                      ck->valid_principals));
+        const char *sep = "";
+        strbuf_clear(tmp);
+        while (get_avail(src)) {
+            ptrlen principal = get_string(src);
+            if (get_err(src))
+                break;
+            put_dataz(tmp, sep);
+            sep = ",";
+            put_datapl(tmp, principal);
+        }
+        seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                                "%s", tmp->s);
+    }
+
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Validity period");
+    strbuf_clear(tmp);
+    if (ck->valid_after == 0) {
+        if (ck->valid_before == 0xFFFFFFFFFFFFFFFF) {
+            put_dataz(tmp, "forever");
+        } else {
+            put_dataz(tmp, "until ");
+            opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp),
+                                        ck->valid_before);
+        }
+    } else {
+        if (ck->valid_before == 0xFFFFFFFFFFFFFFFF) {
+            put_dataz(tmp, "after ");
+            opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp),
+                                        ck->valid_after);
+        } else {
+            opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp),
+                                        ck->valid_after);
+            put_dataz(tmp, " - ");
+            opensshcert_time_to_iso8601(BinarySink_UPCAST(tmp),
+                                        ck->valid_before);
+        }
+    }
+    seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s", tmp->s);
+
+    /*
+     * List critical options we know about. (This is everything listed
+     * in PROTOCOL.certkeys that isn't specific to U2F/FIDO key types
+     * that PuTTY doesn't currently support.)
+     */
+    {
+        BinarySource src[1];
+        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(
+                                      ck->critical_options));
+        strbuf_clear(tmp);
+        while (get_avail(src)) {
+            ptrlen key = get_string(src);
+            ptrlen value = get_string(src);
+            if (get_err(src))
+                break;
+            if (ck->type == SSH_CERT_TYPE_USER &&
+                ptrlen_eq_string(key, "source-address")) {
+                BinarySource src2[1];
+                BinarySource_BARE_INIT_PL(src2, value);
+                ptrlen addresslist = get_string(src2);
+                seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                        "Permitted client IP addresses");
+                seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                                        "%.*s", PTRLEN_PRINTF(addresslist));
+            } else if (ck->type == SSH_CERT_TYPE_USER &&
+                       ptrlen_eq_string(key, "force-command")) {
+                BinarySource src2[1];
+                BinarySource_BARE_INIT_PL(src2, value);
+                ptrlen command = get_string(src2);
+                seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                        "Forced remote command");
+                seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                                        "%.*s", PTRLEN_PRINTF(command));
+            }
+        }
+    }
+
+    /*
+     * List certificate extensions. Again, we go through everything in
+     * PROTOCOL.certkeys that isn't specific to U2F/FIDO key types.
+     * But we also flip the sense round for user-readability: I think
+     * it's more likely that the typical key will permit all these
+     * things, so we emit no output in that case, and only mention the
+     * things that _aren't_ enabled.
+     */
+
+    bool x11_ok = false, agent_ok = false, portfwd_ok = false;
+    bool pty_ok = false, user_rc_ok = false;
+
+    {
+        BinarySource src[1];
+        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(
+                                      ck->extensions));
+        while (get_avail(src)) {
+            ptrlen key = get_string(src);
+            /* ptrlen value = */ get_string(src); // nothing needs this yet
+            if (get_err(src))
+                break;
+            if (ptrlen_eq_string(key, "permit-X11-forwarding")) {
+                x11_ok = true;
+            } else if (ptrlen_eq_string(key, "permit-agent-forwarding")) {
+                agent_ok = true;
+            } else if (ptrlen_eq_string(key, "permit-port-forwarding")) {
+                portfwd_ok = true;
+            } else if (ptrlen_eq_string(key, "permit-pty")) {
+                pty_ok = true;
+            } else if (ptrlen_eq_string(key, "permit-user-rc")) {
+                user_rc_ok = true;
+            }
+        }
+    }
+
+    if (ck->type == SSH_CERT_TYPE_USER) {
+        if (!x11_ok) {
+            seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                    "X11 forwarding permitted");
+            seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no");
+        }
+        if (!agent_ok) {
+            seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                    "Agent forwarding permitted");
+            seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no");
+        }
+        if (!portfwd_ok) {
+            seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                    "Port forwarding permitted");
+            seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no");
+        }
+        if (!pty_ok) {
+            seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                    "PTY allocation permitted");
+            seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no");
+        }
+        if (!user_rc_ok) {
+            seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                                    "Running user ~/.ssh.rc permitted");
+            seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "no");
+        }
+    }
+
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Certificate ID string");
+    seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                            "%s", ck->key_id->s);
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Certificate serial number");
+    seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT,
+                            "%" PRIu64, ck->serial);
+
+    char *fp = ssh2_fingerprint_blob(ptrlen_from_strbuf(ck->signature_key),
+                                     SSH_FPTYPE_DEFAULT);
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Fingerprint of signing CA key");
+    seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s", fp);
+    sfree(fp);
+
+    fp = ssh2_fingerprint(key, ssh_fptype_to_cert(SSH_FPTYPE_DEFAULT));
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Fingerprint including certificate");
+    seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s", fp);
+    sfree(fp);
+
+    strbuf_free(tmp);
+    return text;
+}
+
+static int opensshcert_pubkey_bits(const ssh_keyalg *self, ptrlen blob)
+{
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, blob);
+
+    get_string(src);                   /* key type */
+    get_string(src);                   /* nonce */
+    strbuf *basepub = get_base_public_blob(src, self->extra);
+    int bits = ssh_key_public_bits(
+        self->base_alg, ptrlen_from_strbuf(basepub));
+    strbuf_free(basepub);
+    return bits;
+}
+
+static unsigned opensshcert_supported_flags(const ssh_keyalg *self)
+{
+    return ssh_keyalg_supported_flags(self->base_alg);
+}
+
+static const char *opensshcert_alternate_ssh_id(const ssh_keyalg *self,
+                                                unsigned flags)
+{
+    const char *base_id = ssh_keyalg_alternate_ssh_id(self->base_alg, flags);
+
+    for (size_t i = 0; i < lenof(opensshcert_all_keyalgs); i++) {
+        const ssh_keyalg *alg_i = opensshcert_all_keyalgs[i];
+        if (!strcmp(base_id, alg_i->base_alg->ssh_id))
+            return alg_i->ssh_id;
+    }
+
+    return self->ssh_id;
+}
+
+static char *opensshcert_alg_desc(const ssh_keyalg *self)
+{
+    char *base_desc = ssh_keyalg_desc(self->base_alg);
+    char *our_desc = dupcat(base_desc, " cert");
+    sfree(base_desc);
+    return our_desc;
+}
+
+static bool opensshcert_variable_size(const ssh_keyalg *self)
+{
+    return ssh_keyalg_variable_size(self->base_alg);
+}
+
+static const ssh_keyalg *opensshcert_related_alg(const ssh_keyalg *self,
+                                                 const ssh_keyalg *base)
+{
+    for (size_t i = 0; i < lenof(opensshcert_all_keyalgs); i++) {
+        const ssh_keyalg *alg_i = opensshcert_all_keyalgs[i];
+        if (base == alg_i->base_alg)
+            return alg_i;
+    }
+
+    return self;
+}
+
+static char *opensshcert_invalid(ssh_key *key, unsigned flags)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    return ssh_key_invalid(ck->basekey, flags);
+}
+
+static bool opensshcert_check_cert(
+    ssh_key *key, bool host, ptrlen principal, uint64_t time,
+    const ca_options *opts, BinarySink *error)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    bool result = false;
+    ssh_key *ca_key = NULL;
+    strbuf *preimage = strbuf_new();
+    BinarySource src[1];
+
+    ptrlen signature = ptrlen_from_strbuf(ck->signature);
+
+    /*
+     * The OpenSSH certificate spec is one-layer only: it explicitly
+     * forbids using a certified key in turn as the CA.
+     *
+     * If it did not, then we'd also have to recursively verify
+     * everything up the CA chain until we reached the ultimate root,
+     * and then make sure _that_ was something we trusted. (Not to
+     * mention that there'd probably be an additional SSH_CERT_TYPE_CA
+     * or some such, and certificate options saying what kinds of
+     * certificate a CA was trusted to sign for, and ...)
+     */
+    ca_key = opensshcert_ca_pub_key(ck, make_ptrlen(NULL, 0), NULL);
+    if (!ca_key) {
+        put_fmt(error, "Certificate's signing key is invalid");
+        goto out;
+    }
+    if (ssh_key_alg(ca_key)->is_certificate) {
+        put_fmt(error, "Certificate is signed with a certified key "
+                "(forbidden by OpenSSH certificate specification)");
+        goto out;
+    }
+
+    /*
+     * Now re-instantiate the key in a way that matches the signature
+     * (i.e. so that if the key is an RSA one we get the right subtype
+     * of RSA).
+     */
+    ssh_key_free(ca_key);
+    ca_key = opensshcert_ca_pub_key(ck, signature, NULL);
+    if (!ca_key) {
+        put_fmt(error, "Certificate's signing key does not match "
+                "signature type");
+        goto out;
+    }
+
+    /* Check which signature algorithm is actually in use, because
+     * that might be a reason to reject the certificate (e.g. ssh-rsa
+     * when we wanted rsa-sha2-*). */
+    const ssh_keyalg *sig_alg = ssh_key_alg(ca_key);
+    if ((sig_alg == &ssh_rsa && !opts->permit_rsa_sha1) ||
+        (sig_alg == &ssh_rsa_sha256 && !opts->permit_rsa_sha256) ||
+        (sig_alg == &ssh_rsa_sha512 && !opts->permit_rsa_sha512)) {
+        put_fmt(error, "Certificate signature uses '%s' signature type "
+                "(forbidden by user configuration)", sig_alg->ssh_id);
+        goto out;
+    }
+
+    opensshcert_signature_preimage(ck, BinarySink_UPCAST(preimage));
+
+    if (!ssh_key_verify(ca_key, signature, ptrlen_from_strbuf(preimage))) {
+        put_fmt(error, "Certificate's signature is invalid");
+        goto out;
+    }
+
+    uint32_t expected_type = host ? SSH_CERT_TYPE_HOST : SSH_CERT_TYPE_USER;
+    if (ck->type != expected_type) {
+        put_fmt(error, "Certificate type is ");
+        switch (ck->type) {
+          case SSH_CERT_TYPE_HOST:
+            put_fmt(error, "host");
+            break;
+          case SSH_CERT_TYPE_USER:
+            put_fmt(error, "user");
+            break;
+          default:
+            put_fmt(error, "unknown value %" PRIu32, ck->type);
+            break;
+        }
+        put_fmt(error, "; expected %s", host ? "host" : "user");
+        goto out;
+    }
+
+    /*
+     * Check the time bounds on the certificate.
+     */
+    if (time < ck->valid_after) {
+        put_fmt(error, "Certificate is not valid until ");
+        opensshcert_time_to_iso8601(BinarySink_UPCAST(error), time);
+        goto out;
+    }
+    if (time >= ck->valid_before) {
+        put_fmt(error, "Certificate expired at ");
+        opensshcert_time_to_iso8601(BinarySink_UPCAST(error), time);
+        goto out;
+    }
+
+    /*
+     * Check that this certificate is for the right thing.
+     *
+     * If valid_principals is a zero-length string then this is
+     * specified to be a carte-blanche certificate valid for any
+     * principal (at least, provided you trust the CA that issued it).
+     */
+    if (ck->valid_principals->len != 0) {
+        BinarySource_BARE_INIT_PL(
+            src, ptrlen_from_strbuf(ck->valid_principals));
+
+        while (get_avail(src)) {
+            ptrlen valid_principal = get_string(src);
+            if (get_err(src)) {
+                put_fmt(error, "Certificate's valid principals list is "
+                        "incorrectly formatted");
+                goto out;
+            }
+            if (ptrlen_eq_ptrlen(valid_principal, principal))
+                goto principal_ok;
+        }
+
+        /*
+         * No valid principal matched. Now go through the list a
+         * second time writing the cert contents into the error
+         * message, so that the user can see at a glance what went
+         * wrong.
+         *
+         * (If you've typed the wrong spelling of the host name, you
+         * really need to see "This cert is for 'foo.example.com' and
+         * I was trying to match it against 'foo'", rather than just
+         * "Computer says no".)
+         */
+        put_fmt(error, "Certificate's %s list [",
+                host ? "hostname" : "username");
+        BinarySource_BARE_INIT_PL(
+            src, ptrlen_from_strbuf(ck->valid_principals));
+        const char *sep = "";
+        while (get_avail(src)) {
+            ptrlen valid_principal = get_string(src);
+            put_fmt(error, "%s\"", sep);
+            put_c_string_literal(error, valid_principal);
+            put_fmt(error, "\"");
+            sep = ", ";
+        }
+        put_fmt(error, "] does not contain expected %s \"",
+                host ? "hostname" : "username");
+        put_c_string_literal(error, principal);
+        put_fmt(error, "\"");
+        goto out;
+      principal_ok:;
+    }
+
+    /*
+     * Check for critical options.
+     */
+    {
+        BinarySource_BARE_INIT_PL(
+            src, ptrlen_from_strbuf(ck->critical_options));
+
+        while (get_avail(src)) {
+            ptrlen option = get_string(src);
+            ptrlen data = get_string(src);
+            if (get_err(src)) {
+                put_fmt(error, "Certificate's critical options list is "
+                        "incorrectly formatted");
+                goto out;
+            }
+
+            /*
+             * If we ever do support any options, this will be where
+             * we insert code to recognise and validate them.
+             *
+             * At present, we implement no critical options at all.
+             * (For host certs, as of 2022-04-20, OpenSSH hasn't
+             * defined any. For user certs, the only SSH server using
+             * this is Uppity, which doesn't support key restrictions
+             * in general.)
+             */
+            (void)data; /* no options supported => no use made of the data */
+
+            /*
+             * Report an unrecognised literal.
+             */
+            put_fmt(error, "Certificate specifies an unsupported critical "
+                    "option \"");
+            put_c_string_literal(error, option);
+            put_fmt(error, "\"");
+            goto out;
+        }
+    }
+
+    /* If we get here without failing any check, accept the certificate! */
+    result = true;
+
+  out:
+    if (ca_key)
+        ssh_key_free(ca_key);
+    strbuf_free(preimage);
+    return result;
+}
+
+static bool opensshcert_verify(ssh_key *key, ptrlen sig, ptrlen data)
+{
+    /* This method is pure *signature* verification; checking the
+     * certificate is done elsewhere. */
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    return ssh_key_verify(ck->basekey, sig, data);
+}
+
+static void opensshcert_sign(ssh_key *key, ptrlen data, unsigned flags,
+                             BinarySink *bs)
+{
+    opensshcert_key *ck = container_of(key, opensshcert_key, sshk);
+    ssh_key_sign(ck->basekey, data, flags, bs);
+}

+ 57 - 8
source/putty/crypto/rsa.c

@@ -76,6 +76,21 @@ RSAKey *BinarySource_get_rsa_ssh1_priv_agent(BinarySource *src)
     return rsa;
 }
 
+void duprsakey(RSAKey *dst, const RSAKey *src)
+{
+    dst->bits = src->bits;
+    dst->bytes = src->bytes;
+    dst->modulus = mp_copy(src->modulus);
+    dst->exponent = mp_copy(src->exponent);
+    dst->private_exponent = src->private_exponent ?
+        mp_copy(src->private_exponent) : NULL;
+    dst->p = mp_copy(src->p);
+    dst->q = mp_copy(src->q);
+    dst->iqmp = mp_copy(src->iqmp);
+    dst->comment = src->comment ? dupstr(src->comment) : NULL;
+    dst->sshk.vt = src->sshk.vt;
+}
+
 bool rsa_ssh1_encrypt(unsigned char *data, int length, RSAKey *key)
 {
     mp_int *b1, *b2;
@@ -522,6 +537,12 @@ static key_components *rsa2_components(ssh_key *key)
     return rsa_components(rsa);
 }
 
+static bool rsa2_has_private(ssh_key *key)
+{
+    RSAKey *rsa = container_of(key, RSAKey, sshk);
+    return rsa->private_exponent != NULL;
+}
+
 static void rsa2_public_blob(ssh_key *key, BinarySink *bs)
 {
     RSAKey *rsa = container_of(key, RSAKey, sshk);
@@ -542,7 +563,7 @@ static void rsa2_private_blob(ssh_key *key, BinarySink *bs)
 }
 
 static ssh_key *rsa2_new_priv(const ssh_keyalg *self,
-                               ptrlen pub, ptrlen priv)
+                              ptrlen pub, ptrlen priv)
 {
     BinarySource src[1];
     ssh_key *sshk;
@@ -839,6 +860,23 @@ static char *rsa2_invalid(ssh_key *key, unsigned flags)
     return NULL;
 }
 
+static unsigned ssh_rsa_supported_flags(const ssh_keyalg *self)
+{
+    return SSH_AGENT_RSA_SHA2_256 | SSH_AGENT_RSA_SHA2_512;
+}
+
+static const char *ssh_rsa_alternate_ssh_id(
+    const ssh_keyalg *self, unsigned flags)
+{
+    if (flags & SSH_AGENT_RSA_SHA2_512)
+        return ssh_rsa_sha512.ssh_id;
+    if (flags & SSH_AGENT_RSA_SHA2_256)
+        return ssh_rsa_sha256.ssh_id;
+    return self->ssh_id;
+}
+
+static char *rsa2_alg_desc(const ssh_keyalg *self) { return dupstr("RSA"); }
+
 static const struct ssh2_rsa_extra
     rsa_extra = { 0 },
     rsa_sha256_extra = { SSH_AGENT_RSA_SHA2_256 },
@@ -855,29 +893,36 @@ static const struct ssh2_rsa_extra
     .public_blob = rsa2_public_blob,            \
     .private_blob = rsa2_private_blob,          \
     .openssh_blob = rsa2_openssh_blob,          \
+    .has_private = rsa2_has_private,            \
     .cache_str = rsa2_cache_str,                \
     .components = rsa2_components,              \
+    .base_key = nullkey_base_key,               \
     .pubkey_bits = rsa2_pubkey_bits,            \
+    .alg_desc = rsa2_alg_desc,                  \
+    .variable_size = nullkey_variable_size_yes, \
     .cache_id = "rsa2"
 
 const ssh_keyalg ssh_rsa = {
     COMMON_KEYALG_FIELDS,
     .ssh_id = "ssh-rsa",
-    .supported_flags = SSH_AGENT_RSA_SHA2_256 | SSH_AGENT_RSA_SHA2_512,
+    .supported_flags = ssh_rsa_supported_flags,
+    .alternate_ssh_id = ssh_rsa_alternate_ssh_id,
     .extra = &rsa_extra,
 };
 
 const ssh_keyalg ssh_rsa_sha256 = {
     COMMON_KEYALG_FIELDS,
     .ssh_id = "rsa-sha2-256",
-    .supported_flags = 0,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
     .extra = &rsa_sha256_extra,
 };
 
 const ssh_keyalg ssh_rsa_sha512 = {
     COMMON_KEYALG_FIELDS,
     .ssh_id = "rsa-sha2-512",
-    .supported_flags = 0,
+    .supported_flags = nullkey_supported_flags,
+    .alternate_ssh_id = nullkey_alternate_ssh_id,
     .extra = &rsa_sha512_extra,
 };
 
@@ -1092,13 +1137,17 @@ static const struct ssh_rsa_kex_extra ssh_rsa_kex_extra_sha1 = { 1024 };
 static const struct ssh_rsa_kex_extra ssh_rsa_kex_extra_sha256 = { 2048 };
 
 static const ssh_kex ssh_rsa_kex_sha1 = {
-    "rsa1024-sha1", NULL, KEXTYPE_RSA,
-    &ssh_sha1, &ssh_rsa_kex_extra_sha1,
+    .name = "rsa1024-sha1",
+    .main_type = KEXTYPE_RSA,
+    .hash = &ssh_sha1,
+    .extra = &ssh_rsa_kex_extra_sha1,
 };
 
 static const ssh_kex ssh_rsa_kex_sha256 = {
-    "rsa2048-sha256", NULL, KEXTYPE_RSA,
-    &ssh_sha256, &ssh_rsa_kex_extra_sha256,
+    .name = "rsa2048-sha256",
+    .main_type = KEXTYPE_RSA,
+    .hash = &ssh_sha256,
+    .extra = &ssh_rsa_kex_extra_sha256,
 };
 
 static const ssh_kex *const rsa_kex_list[] = {

+ 22 - 0
source/putty/defs.h

@@ -116,6 +116,9 @@ typedef struct LogPolicyVtable LogPolicyVtable;
 
 typedef struct Seat Seat;
 typedef struct SeatVtable SeatVtable;
+typedef struct SeatDialogText SeatDialogText;
+typedef struct SeatDialogTextItem SeatDialogTextItem;
+typedef struct SeatDialogPromptDescriptions SeatDialogPromptDescriptions;
 typedef struct SeatPromptResult SeatPromptResult;
 
 typedef struct cmdline_get_passwd_input_state cmdline_get_passwd_input_state;
@@ -143,6 +146,8 @@ typedef struct Channel Channel;
 typedef struct SshChannel SshChannel;
 typedef struct mainchan mainchan;
 
+typedef struct CertExprBuilder CertExprBuilder;
+
 typedef struct ssh_sharing_state ssh_sharing_state;
 typedef struct ssh_sharing_connstate ssh_sharing_connstate;
 typedef struct share_channel share_channel;
@@ -169,12 +174,19 @@ typedef struct ssh_cipher ssh_cipher;
 typedef struct ssh2_ciphers ssh2_ciphers;
 typedef struct dh_ctx dh_ctx;
 typedef struct ecdh_key ecdh_key;
+typedef struct ecdh_keyalg ecdh_keyalg;
+typedef struct NTRUKeyPair NTRUKeyPair;
+typedef struct NTRUEncodeSchedule NTRUEncodeSchedule;
 
 typedef struct dlgparam dlgparam;
+typedef struct dlgcontrol dlgcontrol;
 
 typedef struct settings_w settings_w;
 typedef struct settings_r settings_r;
 typedef struct settings_e settings_e;
+typedef struct ca_options ca_options;
+typedef struct host_ca host_ca;
+typedef struct host_ca_enum host_ca_enum;
 
 typedef struct SessionSpecial SessionSpecial;
 
@@ -243,4 +255,14 @@ struct unicode_data;
 #define CAT_INNER(x,y) x ## y
 #define CAT(x,y) CAT_INNER(x,y)
 
+/*
+ * Structure shared between ssh.h and storage.h, giving strictness
+ * options relating to checking of an OpenSSH certificate. It's a bit
+ * cheaty to put something so specific in here, but more painful to
+ * put it in putty.h.
+ */
+struct ca_options {
+    bool permit_rsa_sha1, permit_rsa_sha256, permit_rsa_sha512;
+};
+
 #endif /* PUTTY_DEFS_H */

+ 519 - 0
source/putty/doc/authplugin.but

@@ -0,0 +1,519 @@
+\A{authplugin} PuTTY authentication plugin protocol
+
+This appendix contains the specification for the protocol spoken over
+local IPC between PuTTY and an authentication helper plugin.
+
+If you already have an authentication plugin and want to configure
+PuTTY to use it, see \k{config-ssh-authplugin} for how to do that.
+This appendix is for people writing new authentication plugins.
+
+\H{authplugin-req} Requirements
+
+The following requirements informed the specification of this protocol.
+
+\s{Automate keyboard-interactive authentication.} We're motivated in
+the first place by the observation that the general SSH userauth
+method \cq{keyboard-interactive} (defined in \k{authplugin-ref-ki})
+can be used for many kinds of challenge/response or one-time-password
+styles of authentication, and in more than one of those, the necessary
+responses might be obtained from an auxiliary network connection, such
+as an HTTPS transaction. So it's useful if a user doesn't have to
+manually copy-type or copy-paste from their web browser into their SSH
+client, but instead, the process can be automated.
+
+\s{Be able to pass prompts on to the user.} On the other hand, some
+userauth methods can be only \e{partially} automated; some of the
+server's prompts might still require human input. Also, the plugin
+automating the authentication might need to ask its own questions that
+are not provided by the SSH server. (For example, \q{please enter the
+master key that the real response will be generated by hashing}.) So
+after the plugin intercepts the server's questions, it needs to be
+able to ask its own questions of the user, which may or may not be the
+same questions sent by the server.
+
+\s{Allow automatic generation of the username.} Sometimes, the
+authentication method comes with a mechanism for discovering the
+username to be used in the SSH login. So the plugin has to start up
+early enough that the client hasn't committed to a username yet.
+
+\s{Future expansion route to other SSH userauth flavours.} The initial
+motivation for this protocol is specific to keyboard-interactive. But
+other SSH authentication methods exist, and they may also benefit from
+automation in future. We're making no attempt here to predict what
+those methods might be or how they might be automated, but we do need
+to leave a space where they can be slotted in later if necessary.
+
+\s{Minimal information loss.} Keyboard-interactive prompts and replies
+should be passed to and from the plugin in a form as close as possible
+to the way they look on the wire in SSH itself. Therefore, the
+protocol resembles SSH in its data formats and marshalling (instead
+of, for example, translating from SSH binary packet style to another
+well-known format such as JSON, which would introduce edge cases in
+character encoding).
+
+\s{Half-duplex.} Simultaneously trying to read one I/O stream and
+write another adds a lot of complexity to software. It becomes
+necessary to have an organised event loop containing \cw{select} or
+\cw{WaitForMultipleObjects} or similar, which can invoke the handler
+for whichever event happens soonest. There's no need to add that
+complexity in an application like this, which isn't transferring large
+amounts of bulk data or multiplexing unrelated activities. So, to keep
+life simple for plugin authors, we set the ground rule that it must
+always be 100% clear which side is supposed to be sending a message
+next. That way, the plugin can be written as sequential code
+progressing through the protocol, making simple read and write calls
+to receive or send each message.
+
+\s{Communicate success/failure, to facilitate caching in the plugin.}
+A plugin might want to cache recently used data for next time, but
+only in the case where authentication using that data was actually
+successful. So the client has to tell the plugin what the outcome was,
+if it's known. (But this is best-effort only. Obviously the plugin
+cannot \e{depend} on hearing the answer, because any IPC protocol at
+all carries the risk that the other end might crash or be killed by
+things outside its control.)
+
+\H{authplugin-transport} Transport and configuration
+
+Plugins are executable programs on the client platform.
+
+The SSH client must be manually configured to use a plugin for a
+particular connection. The configuration takes the form of a command
+line, including the location of the plugin executable, and optionally
+command-line arguments that are meaningful to the particular plugin.
+
+The client invokes the plugin as a subprocess, passing it a pair of
+8-bit-clean pipes as its standard input and output. On those pipes,
+the client and plugin will communicate via the protocol specified
+below.
+
+\H{authplugin-formats} Data formats and marshalling
+
+This protocol borrows the low-level data formatting from SSH itself,
+in particular the following wire encodings from
+\k{authplugin-ref-arch} section 5:
+
+\dt \s{byte}
+
+\dd An integer between 0 and 0xFF inclusive, transmitted as a single
+byte of binary data.
+
+\dt \s{boolean}
+
+\dd The values \q{true} or \q{false}, transmitted as the bytes 1 and 0
+respectively.
+
+\dt \s{uint32}
+
+\dd An integer between 0 and 0xFFFFFFFF inclusive, transmitted as 4
+bytes of binary data, in big-endian (\q{network}) byte order.
+
+\dt \s{string}
+
+\dd A sequence of bytes, preceded by a \s{uint32} giving the number of
+bytes in the sequence. The length field does not include itself. For
+example, the empty string is represented by four zero bytes (the
+\s{uint32} encoding of 0); the string "AB" is represented by the six
+bytes 0,0,0,2,'A','B'.
+
+Unlike SSH itself, the protocol spoken between the client and the
+plugin is unencrypted, because local inter-process pipes are assumed
+to be secured by the OS kernel. So the binary packet protocol is much
+simpler than SSH proper, and is similar to SFTP and the OpenSSH agent
+protocol.
+
+The data sent in each direction of the conversation consists of a
+sequence of \s{messages} exchanged between the SSH client and the
+plugin. Each message is encoded as a \s{string}. The contents of the
+string begin with a \s{byte} giving the message type, which determines
+the format of the rest of the message.
+
+\H{authplugin-version} Protocol versioning
+
+This protocol itself is versioned. At connection setup, the client
+states the highest version number it knows how to speak, and then the
+plugin responds by choosing the version number that will actually be
+spoken (which may not be higher than the client's value).
+
+Including a version number makes it possible to make breaking changes
+to the protocol later.
+
+Even version numbers represent released versions of this spec. Odd
+numbers represent drafts or development versions in between releases.
+A client and plugin negotiating an odd version number are not
+guaranteed to interoperate; the developer testing the combination is
+responsible for ensuring the two are compatible.
+
+This document describes version 2 of the protocol, the first released
+version. (The initial drafts had version 1.)
+
+\H{authplugin-overview} Overview and sequence of events
+
+At the very beginning of the user authentication phase of SSH, the
+client launches the plugin subprocess, if one is configured. It
+immediately sends the \cw{PLUGIN_INIT} message, telling the plugin
+some initial information about where the SSH connection is to.
+
+The plugin responds with \cw{PLUGIN_INIT_RESPONSE}, which may
+optionally tell the SSH client what username to use.
+
+The client begins trying to authenticate with the SSH server in the
+usual way, using the username provided by the plugin (if any) or
+alternatively one obtained via its normal (non-plugin) policy.
+
+The client follows its normal policy for selecting authentication
+methods to attempt. If it chooses a method that this protocol does not
+cover, then the client will perform that method in its own way without
+consulting the plugin.
+
+However, if the client and server decide to attempt a method that this
+protocol \e{does} cover, then the client sends \cw{PLUGIN_PROTOCOL}
+specifying the SSH protocol id for the authentication method being
+used. The plugin responds with \cw{PLUGIN_PROTOCOL_ACCEPT} if it's
+willing to assist with this auth method, or
+\cw{PLUGIN_PROTOCOL_REJECT} if it isn't.
+
+If the plugin sends \cw{PLUGIN_PROTOCOL_REJECT}, then the client will
+proceed as if the plugin were not present. Later, if another auth
+method is negotiated (either because this one failed, or because it
+succeeded but the server wants multiple auth methods), the client may
+send a further \cw{PLUGIN_PROTOCOL} and try again.
+
+If the plugin sends \cw{PLUGIN_PROTOCOL_ACCEPT}, then a protocol
+segment begins that is specific to that auth method, terminating in
+either \cw{PLUGIN_AUTH_SUCCESS} or \cw{PLUGIN_AUTH_FAILURE}. After
+that, again, the client may send a further \cw{PLUGIN_PROTOCOL}.
+
+Currently the only supported method is \cq{keyboard-interactive},
+defined in \k{authplugin-ref-ki}. Once the client has announced this
+to the server, the followup protocol is as follows:
+
+Each time the server sends an \cw{SSH_MSG_USERAUTH_INFO_REQUEST}
+message requesting authentication responses from the user, the SSH
+client translates the message into \cw{PLUGIN_KI_SERVER_REQUEST} and
+passes it on to the plugin.
+
+At this point, the plugin may optionally send back
+\cw{PLUGIN_KI_USER_REQUEST} containing prompts to be presented to the
+actual user. The client will reply with a matching
+\cw{PLUGIN_KI_USER_RESPONSE} after asking the user to reply to the
+question(s) in the request message. The plugin can repeat this cycle
+multiple times.
+
+Once the plugin has all the information it needs to respond to the
+server's authentication prompts, it sends \cw{PLUGIN_KI_SERVER_RESPONSE}
+back to the client, which translates it into
+\cw{SSH_MSG_USERAUTH_INFO_RESPONSE} to send on to the server.
+
+After that, as described in \k{authplugin-ref-ki}, the server is free
+to accept authentication, reject it, or send another
+\cw{SSH_MSG_USERAUTH_INFO_REQUEST}. Each
+\cw{SSH_MSG_USERAUTH_INFO_REQUEST} is dealt with in the same way as
+above.
+
+If the server terminates keyboard-interactive authentication with
+\cw{SSH_MSG_USERAUTH_SUCCESS} or \cw{SSH_MSG_USERAUTH_FAILURE}, the
+client informs the plugin by sending either \cw{PLUGIN_AUTH_SUCCESS}
+or \cw{PLUGIN_AUTH_FAILURE}. \cw{PLUGIN_AUTH_SUCCESS} is sent when
+\e{that particular authentication method} was successful, regardless
+of whether the SSH server chooses to request further authentication
+afterwards: in particular, \cw{SSH_MSG_USERAUTH_FAILURE} with the
+\q{partial success} flag (see \k{authplugin-ref-userauth} section 5.1) translates
+into \cw{PLUGIN_AUTH_SUCCESS}.
+
+The plugin's standard input will close when the client no longer
+requires the plugin's services, for any reason. This could be because
+authentication is complete (with overall success or overall failure),
+or because the user has manually aborted the session in
+mid-authentication, or because the client crashed.
+
+\H{authplugin-messages} Message formats
+
+This section describes the format of every message in the protocol.
+
+As described in \k{authplugin-formats}, every message starts with the same two
+fields:
+
+\b \s{uint32}: overall length of the message
+
+\b \s{byte}: message type.
+
+The length field does not include itself, but does include the type
+code.
+
+The following subsections each give the format of the remainder of the
+message, after the type code.
+
+The type codes themselves are defined here:
+
+\c #define PLUGIN_INIT                   1
+\c #define PLUGIN_INIT_RESPONSE          2
+\c #define PLUGIN_PROTOCOL               3
+\c #define PLUGIN_PROTOCOL_ACCEPT        4
+\c #define PLUGIN_PROTOCOL_REJECT        5
+\c #define PLUGIN_AUTH_SUCCESS           6
+\c #define PLUGIN_AUTH_FAILURE           7
+\c #define PLUGIN_INIT_FAILURE           8
+\c
+\c #define PLUGIN_KI_SERVER_REQUEST     20
+\c #define PLUGIN_KI_SERVER_RESPONSE    21
+\c #define PLUGIN_KI_USER_REQUEST       22
+\c #define PLUGIN_KI_USER_RESPONSE      23
+
+If this protocol is extended to be able to assist with further auth
+methods, their message type codes will also begin from 20, overlapping
+the codes for keyboard-interactive.
+
+\S{PLUGIN_INIT} \cw{PLUGIN_INIT}
+
+\s{Direction}: client to plugin
+
+\s{When}: the first message sent at connection startup
+
+\s{What happens next}: the plugin will send \cw{PLUGIN_INIT_RESPONSE}
+or \cw{PLUGIN_INIT_FAILURE}
+
+\s{Message contents after the type code}:
+
+\b \s{uint32}: the highest version number of this protocol that the
+client knows how to speak.
+
+\b \s{string}: the hostname of the server. This will be the \e{logical}
+hostname, in cases where it differs from the physical destination of
+the network connection. Whatever name would be used by the SSH client
+to cache the server's host key, that's the same name passed in this
+message.
+
+\b \s{uint32}: the port number on the server. (Together with the host
+name, this forms a primary key identifying a particular server. Port
+numbers may be vital because a single host can run two unrelated SSH
+servers with completely different authentication requirements, e.g.
+system sshd on port 22 and Gerrit on port 29418.)
+
+\b \s{string}: the username that the client will use to log in, if the
+plugin chooses not to override it. An empty string means that the
+client has no opinion about this (and might, for example, prompt the
+user).
+
+\S{PLUGIN_INIT_RESPONSE} \cw{PLUGIN_INIT_RESPONSE}
+
+\s{Direction}: plugin to client
+
+\s{When}: response to \cw{PLUGIN_INIT}
+
+\s{What happens next}: the client will send \cw{PLUGIN_PROTOCOL}, or
+perhaps terminate the session (if no auth method is ever negotiated
+that the plugin can help with)
+
+\s{Message contents after the type code}:
+
+\b \s{uint32}: the version number of this protocol that the connection
+will use. Must be no greater than the max version number sent by the
+client in \cw{PLUGIN_INIT}.
+
+\b \s{string}: the username that the plugin suggests the client use. An
+empty string means that the plugin has no opinion and the client
+should stick with the username it already had (or prompt the user, if
+it had none).
+
+\S{PLUGIN_INIT_FAILURE} \cw{PLUGIN_INIT_FAILURE}
+
+\s{Direction}: plugin to client
+
+\s{When}: response to \cw{PLUGIN_INIT}
+
+\s{What happens next}: the session is over
+
+\s{Message contents after the type code}:
+
+\b \s{string}: an error message to present to the user indicating why
+the plugin was unable to start up.
+
+\S{PLUGIN_PROTOCOL} \cw{PLUGIN_PROTOCOL}
+
+\s{Direction}: client to plugin
+
+\s{When}: sent after \cw{PLUGIN_INIT_RESPONSE}, or after a previous
+auth phase terminates with \cw{PLUGIN_AUTH_SUCCESS} or
+\cw{PLUGIN_AUTH_FAILURE}
+
+\s{What happens next}: the plugin will send
+\cw{PLUGIN_PROTOCOL_ACCEPT} or \cw{PLUGIN_PROTOCOL_REJECT}
+
+\s{Message contents after the type code}:
+
+\b \s{string}: the SSH protocol id of the auth method the client
+intends to attempt. Currently the only method specified for use in
+this protocol is \cq{keyboard-interactive}.
+
+\S{PLUGIN_PROTOCOL_REJECT} \cw{PLUGIN_PROTOCOL_REJECT}
+
+\s{Direction}: plugin to client
+
+\s{When}: sent after \cw{PLUGIN_PROTOCOL}
+
+\s{What happens next}: the client will either send another
+\cw{PLUGIN_PROTOCOL} or terminate the session
+
+\s{Message contents after the type code}:
+
+\b \s{string}: an error message to present to the user, explaining why
+the plugin cannot help with this authentication protocol.
+
+\lcont{
+
+An example might be \q{unable to open <config file>: <OS error
+message>}, if the plugin depends on some configuration that the user
+has not set up.
+
+If the plugin does not support this this particular authentication
+protocol at all, this string should be left blank, so that no message
+will be presented to the user at all.
+
+}
+
+\S{PLUGIN_PROTOCOL_ACCEPT} \cw{PLUGIN_PROTOCOL_ACCEPT}
+
+\s{Direction}: plugin to client
+
+\s{When}: sent after \cw{PLUGIN_PROTOCOL}
+
+\s{What happens next}: depends on the auth protocol agreed on. For
+keyboard-interactive, the client will send
+\cw{PLUGIN_KI_SERVER_REQUEST} or \cw{PLUGIN_AUTH_SUCCESS} or
+\cw{PLUGIN_AUTH_FAILURE}. No other method is specified.
+
+\s{Message contents after the type code}: none.
+
+\S{PLUGIN_KI_SERVER_REQUEST} \cw{PLUGIN_KI_SERVER_REQUEST}
+
+\s{Direction}: client to plugin
+
+\s{When}: sent after \cw{PLUGIN_PROTOCOL}, or after a previous
+\cw{PLUGIN_KI_SERVER_RESPONSE}, when the SSH server has sent
+\cw{SSH_MSG_USERAUTH_INFO_REQUEST}
+
+\s{What happens next}: the plugin will send either
+\cw{PLUGIN_KI_USER_REQUEST} or \cw{PLUGIN_KI_SERVER_RESPONSE}
+
+\s{Message contents after the type code}: the exact contents of the
+\cw{SSH_MSG_USERAUTH_INFO_REQUEST} just sent by the server. See
+\k{authplugin-ref-ki} section 3.2 for details. The summary:
+
+\b \s{string}: name of this prompt collection (e.g. to use as a
+dialog-box title)
+
+\b \s{string}: instructions to be displayed before this prompt
+collection
+
+\b \s{string}: language tag (deprecated)
+
+\b \s{uint32}: number of prompts in this collection
+
+\b That many copies of:
+
+\lcont{
+
+\b \s{string}: prompt (in UTF-8)
+
+\b \s{boolean}: whether the response to this prompt is safe to echo to
+the screen
+
+}
+
+\S{PLUGIN_KI_SERVER_RESPONSE} \cw{PLUGIN_KI_SERVER_RESPONSE}
+
+\s{Direction}: plugin to client
+
+\s{When}: response to \cw{PLUGIN_KI_SERVER_REQUEST}, perhaps after one
+or more intervening pairs of \cw{PLUGIN_KI_USER_REQUEST} and
+\cw{PLUGIN_KI_USER_RESPONSE}
+
+\s{What happens next}: the client will send a further
+\cw{PLUGIN_KI_SERVER_REQUEST}, or \cw{PLUGIN_AUTH_SUCCESS} or
+\cw{PLUGIN_AUTH_FAILURE}
+
+\s{Message contents after the type code}: the exact contents of the
+\cw{SSH_MSG_USERAUTH_INFO_RESPONSE} that the client should send back
+to the server. See \k{authplugin-ref-ki} section 3.4 for details. The
+summary:
+
+\b \s{uint32}: number of responses (must match the \q{number of
+prompts} field from the corresponding server request)
+
+\b That many copies of:
+
+\lcont{
+
+\b \s{string}: response to the \e{n}th prompt (in UTF-8)
+
+}
+
+\S{PLUGIN_KI_USER_REQUEST} \cw{PLUGIN_KI_USER_REQUEST}
+
+\s{Direction}: plugin to client
+
+\s{When}: response to \cw{PLUGIN_KI_SERVER_REQUEST}, if the plugin
+cannot answer the server's auth prompts without presenting prompts of
+its own to the user
+
+\s{What happens next}: the client will send \cw{PLUGIN_KI_USER_RESPONSE}
+
+\s{Message contents after the type code}: exactly the same as in
+\cw{PLUGIN_KI_SERVER_REQUEST} (see \k{PLUGIN_KI_SERVER_REQUEST}).
+
+\S{PLUGIN_KI_USER_RESPONSE} \cw{PLUGIN_KI_USER_RESPONSE}
+
+\s{Direction}: client to plugin
+
+\s{When}: response to \cw{PLUGIN_KI_USER_REQUEST}
+
+\s{What happens next}: the plugin will send
+\cw{PLUGIN_KI_SERVER_RESPONSE}, or another \cw{PLUGIN_KI_USER_REQUEST}
+
+\s{Message contents after the type code}: exactly the same as in
+\cw{PLUGIN_KI_SERVER_RESPONSE} (see \k{PLUGIN_KI_SERVER_RESPONSE}).
+
+\S{PLUGIN_AUTH_SUCCESS} \cw{PLUGIN_AUTH_SUCCESS}
+
+\s{Direction}: client to plugin
+
+\s{When}: sent after \cw{PLUGIN_KI_SERVER_RESPONSE}, or (in unusual
+cases) after \cw{PLUGIN_PROTOCOL_ACCEPT}
+
+\s{What happens next}: the client will either send another
+\cw{PLUGIN_PROTOCOL} or terminate the session
+
+\s{Message contents after the type code}: none
+
+\S{PLUGIN_AUTH_FAILURE} \cw{PLUGIN_AUTH_FAILURE}
+
+\s{Direction}: client to plugin
+
+\s{When}: sent after \cw{PLUGIN_KI_SERVER_RESPONSE}, or (in unusual
+cases) after \cw{PLUGIN_PROTOCOL_ACCEPT}
+
+\s{What happens next}: the client will either send another
+\cw{PLUGIN_PROTOCOL} or terminate the session
+
+\s{Message contents after the type code}: none
+
+\H{authplugin-refs} References
+
+\B{authplugin-ref-arch} \W{https://www.rfc-editor.org/rfc/rfc4251}{RFC 4251}, \q{The Secure Shell (SSH) Protocol
+Architecture}.
+
+\B{authplugin-ref-userauth} \W{https://www.rfc-editor.org/rfc/rfc4252}{RFC
+4252}, \q{The Secure Shell (SSH) Authentication Protocol}.
+
+\B{authplugin-ref-ki}
+\W{https://www.rfc-editor.org/rfc/rfc4256}{RFC 4256},
+\q{Generic Message Exchange Authentication for the Secure Shell
+Protocol (SSH)} (better known by its wire id
+\q{keyboard-interactive}).
+
+\BR{authplugin-ref-arch} [RFC4251]
+
+\BR{authplugin-ref-userauth} [RFC4252]
+
+\BR{authplugin-ref-ki} [RFC4256]

+ 6 - 0
source/putty/doc/chmextra.but

@@ -0,0 +1,6 @@
+\# If you want to do a Halibut build of the CHM file by hand, without
+\# the help of the CMake edifice, then include this file which will
+\# refer to chm.css. The CMake edifice builds its own with a different
+\# pathname in it, for the sake of out-of-tree builds.
+
+\cfg{chm-extra-file}{chm.css}

+ 306 - 33
source/putty/doc/config.but

@@ -1943,14 +1943,14 @@ it must always be explicitly configured.
 
 \S{config-proxy-type} Setting the proxy type
 
-The \q{Proxy type} radio buttons allow you to configure what type of
+The \q{Proxy type} drop-down allows you to configure what type of
 proxy you want PuTTY to use for its network connections. The default
 setting is \q{None}; in this mode no proxy is used for any
 connection.
 
-\b Selecting \I{HTTP proxy}\q{HTTP} allows you to proxy your connections
-through a web server supporting the HTTP \cw{CONNECT} command, as documented
-in \W{http://www.ietf.org/rfc/rfc2817.txt}{RFC 2817}.
+\b Selecting \I{HTTP proxy}\q{HTTP CONNECT} allows you to proxy your
+connections through a web server supporting the HTTP \cw{CONNECT} command,
+as documented in \W{https://www.rfc-editor.org/rfc/rfc2817}{RFC 2817}.
 
 \b Selecting \q{SOCKS 4} or \q{SOCKS 5} allows you to proxy your
 connections through a \i{SOCKS server}.
@@ -1962,10 +1962,9 @@ through to an external host. Selecting \I{Telnet proxy}\q{Telnet}
 allows you to tell PuTTY to use this type of proxy, with the precise           
 command specified as described in \k{config-proxy-command}.
 
-\b Selecting \q{SSH} causes PuTTY to make a secondary SSH connection
-to the proxy host (sometimes called a \q{\i{jump host}} in this
-context), and then open a port-forwarding channel to the
-final destination host. 
+\b There are several ways to use a SSH server as a proxy. All of
+these cause PuTTY to make a secondary SSH connection to the proxy host
+(sometimes called a \q{\i{jump host}} in this context).
 
 \lcont{
 The \q{Proxy hostname} field will be interpreted as the name of a
@@ -1973,6 +1972,20 @@ PuTTY saved session if one exists, or a hostname if not. This
 allows multi-hop jump paths, if the referenced saved session is
 itself configured to use an SSH proxy; and it allows combining SSH
 and non-SSH proxying.
+
+\b \q{SSH to proxy and use port forwarding} causes PuTTY to use the
+secondary SSH connection to open a port-forwarding channel to the
+final destination host (similar to OpenSSH's \cw{-J} option).
+
+\b \q{SSH to proxy and execute a command} causes PuTTY to run an
+arbitrary remote command on the proxy SSH server and use that
+command's standard input and output streams to run the primary
+connection over. The remote command line is specified as described in
+\k{config-proxy-command}.
+
+\b \q{SSH to proxy and invoke a subsystem} is similar but causes PuTTY
+to start an SSH \q{\i{subsystem}} rather than an ordinary command line.
+This might be useful with a specially set up SSH proxy server.
 }
 
 \b Selecting \I{Local proxy}\q{Local} allows you to specify an arbitrary
@@ -2116,16 +2129,21 @@ Telnet/Local proxy command (see \k{config-proxy-command}). If you do
 so, and don't also specify the actual username and/or password in the
 configuration, PuTTY will interactively prompt for them.
 
-\S{config-proxy-command} Specifying the Telnet or Local proxy command
+\S{config-proxy-command} Specifying the Telnet, SSH, or Local proxy command
 
 If you are using the \i{Telnet proxy} type, the usual command required
 by the firewall's Telnet server is \c{connect}, followed by a host
 name and a port number. If your proxy needs a different command,
-you can enter an alternative here.
+you can enter an alternative in the \q{Command to send to proxy} box.
 
 If you are using the \i{Local proxy} type, the local command to run
 is specified here.
 
+If you are using the \q{SSH to proxy and execute a command} type, the
+command to run on the SSH proxy server is specified here. Similarly, if
+you are using \q{SSH to proxy and invoke a subsystem}, the subsystem
+name is constructed as specified here.
+
 In this string, you can use \c{\\n} to represent a new-line, \c{\\r}
 to represent a carriage return, \c{\\t} to represent a tab
 character, and \c{\\x} followed by two hex digits to represent any
@@ -2133,13 +2151,15 @@ other character. \c{\\\\} is used to encode the \c{\\} character
 itself.
 
 Also, the special strings \c{%host} and \c{%port} will be replaced
-by the host name and port number you want to connect to. The strings
-\c{%user} and \c{%pass} will be replaced by the proxy username and
-password (which, if not specified in the configuration, will be
-prompted for). The strings \c{%proxyhost} and \c{%proxyport}
+by the host name and port number you want to connect to. For Telnet
+and Local proxy types, the strings \c{%user} and \c{%pass} will be
+replaced by the proxy username and password (which, if not specified
+in the configuration, will be prompted for) \dash this does not happen
+with SSH proxy types (because the proxy username/password are used
+for SSH authentication). The strings \c{%proxyhost} and \c{%proxyport}
 will be replaced by the host details specified on the \e{Proxy} panel,
-if any (this is most likely to be useful for the Local proxy type).
-To get a literal \c{%} sign, enter \c{%%}.
+if any (this is most likely to be useful for proxy types using a
+local or remote command). To get a literal \c{%} sign, enter \c{%%}.
 
 If a Telnet proxy server prompts for a username and password
 before commands can be sent, you can use a command such as:
@@ -2327,24 +2347,51 @@ cipher selection (see \k{config-ssh-encryption}).
 
 PuTTY currently supports the following key exchange methods:
 
-\b \q{ECDH}: \i{elliptic curve} \i{Diffie-Hellman key exchange}.
+\b \q{NTRU Prime / Curve25519 hybrid}: \q{\i{Streamlined NTRU Prime}}
+is a lattice-based algorithm intended to resist \i{quantum attacks}.
+In this key exchange method, it is run in parallel with a conventional
+Curve25519-based method (one of those included in \q{ECDH}), in such
+a way that it should be no \e{less} secure than that commonly-used
+method, and hopefully also resistant to a new class of attacks.
+
+\b \q{\i{ECDH}}: elliptic curve Diffie-Hellman key exchange,
+with a variety of standard curves and hash algorithms.
+
+\b The original form of \i{Diffie-Hellman key exchange}, with a
+variety of well-known groups and hashes:
+
+\lcont{
+\b \q{Group 18}, a well-known 8192-bit group, used with the SHA-512
+hash function.
+
+\b \q{Group 17}, a well-known 6144-bit group, used with the SHA-512
+hash function.
 
-\b \q{Group 14}: Diffie-Hellman key exchange with a well-known
-2048-bit group.
+\b \q{Group 16}, a well-known 4096-bit group, used with the SHA-512
+hash function.
 
-\b \q{Group 1}: Diffie-Hellman key exchange with a well-known
-1024-bit group. We no longer recommend using this method, and it's
-not used by default in new installations; however, it may be the
-only method supported by very old server software.
+\b \q{Group 15}, a well-known 3072-bit group, used with the SHA-512
+hash function.
 
-\b \q{\ii{Group exchange}}: with this method, instead of using a fixed
-group, PuTTY requests that the server suggest a group to use for key
-exchange; the server can avoid groups known to be weak, and possibly
-invent new ones over time, without any changes required to PuTTY's
-configuration. We recommend use of this method instead of the
-well-known groups, if possible.
+\b \q{Group 14}: a well-known 2048-bit group, used with the SHA-256
+hash function or, if the server doesn't support that, SHA-1.
+
+\b \q{Group 1}: a well-known 1024-bit group, used with the SHA-1
+hash function. Neither we nor current SSH standards recommend using
+this method any longer, and it's not used by default in new
+installations; however, it may be the only method supported by very
+old server software.
+}
 
-\b \q{\i{RSA key exchange}}: this requires much less computational
+\b \q{Diffie-Hellman \i{group exchange}}: with this method, instead
+of using a fixed group, PuTTY requests that the server suggest a group
+to use for a subsequent Diffie-Hellman key exchange; the server can
+avoid groups known to be weak, and possibly invent new ones over time,
+without any changes required to PuTTY's configuration. This key
+exchange method uses the SHA-256 hash or, if the server doesn't
+support that, SHA-1.
+
+\b \q{\i{RSA-based key exchange}}: this requires much less computational
 effort on the part of the client, and somewhat less on the part of
 the server, than Diffie-Hellman key exchange.
 
@@ -2366,6 +2413,10 @@ when using Kerberos V5, and not other GSSAPI mechanisms. If the user
 running PuTTY has current Kerberos V5 credentials, then PuTTY will
 select the GSSAPI key exchange methods in preference to any of the
 ordinary SSH key exchange methods configured in the preference list.
+There's a GSSAPI-based equivalent to most of the ordinary methods
+listed in \k{config-ssh-kex-order}; server support determines which
+one will be used. (PuTTY's preference order for GSSAPI-authenticated
+key exchange methods is fixed, not controlled by the preference list.)
 
 The advantage of doing GSSAPI authentication as part of the SSH key
 exchange is apparent when you are using credential delegation (see
@@ -2380,7 +2431,8 @@ support GSSAPI in the SSH user authentication phase. This will still
 let you log in using your Kerberos credentials, but will only allow
 you to delegate the credentials that are active at the beginning of
 the session; they can't be refreshed automatically later, in a
-long-running session.
+long-running session. See \k{config-ssh-auth-gssapi} for how to
+control GSSAPI user authentication in PuTTY.
 
 Another effect of GSSAPI key exchange is that it replaces the usual
 SSH mechanism of permanent host keys described in \k{gs-hostkey}.
@@ -2494,7 +2546,7 @@ larger elliptic curve with a 448-bit instead of 255-bit modulus (so it
 has a higher security level than Ed25519).
 
 \b \q{ECDSA}: \i{elliptic curve} \i{DSA} using one of the
-NIST-standardised elliptic curves.
+\i{NIST}-standardised elliptic curves.
 
 \b \q{DSA}: straightforward \i{DSA} using modular exponentiation.
 
@@ -2590,6 +2642,146 @@ neither read \e{nor written}, unless you explicitly do so.
 If the box is empty (as it usually is), then PuTTY's automated host
 key management will work as normal.
 
+\S{config-ssh-kex-cert} Configuring PuTTY to accept host \i{certificates}
+
+In some environments, the SSH host keys for a lot of servers will all
+be signed in turn by a central \q{certification authority} (\q{CA} for
+short). This simplifies host key configuration for users, because if
+they configure their SSH client to accept host keys certified by that
+CA, then they don't need to individually confirm each host key the
+first time they connect to that server.
+
+In order to do this, press the \q{Configure host CAs} button in the
+\q{Host keys} configuration panel. This will launch a secondary
+configuration dialog box where you can configure what CAs PuTTY will
+accept signatures from.
+
+\s{Note that this configuration is common to all saved sessions}.
+Everything in the main PuTTY configuration is specific to one saved
+session, and you can prepare a separate session with all the
+configuration different. But there's only one copy of the host CA
+configuration, and it applies to all sessions PuTTY runs, whether
+saved or not.
+
+(Otherwise, it would be useless \dash configuring a CA by hand for
+each new host wouldn't be any more convenient than pressing the
+\q{confirm} button for each new host's host key.)
+
+To set up a new CA using this config box:
+
+First, load the CA's public key from a file, or paste it directly into
+the \q{Public key of certification authority} edit box. If your
+organisation signs its host keys in this way, they will publish the
+public key of their CA so that SSH users can include it in their
+configuration.
+
+Next, in the \q{Valid hosts this key is trusted to certify} box,
+configure at least one hostname wildcard to say what servers PuTTY
+should trust this CA to speak for. For example, suppose you work for
+Example Corporation (\cw{example.com}), and the Example Corporation IT
+department has advertised a CA that signs all the Example internal
+machines' host keys. Then probably you want to trust that CA to sign
+host keys for machines in the domain \cw{example.com}, but not for
+anything else. So you might enter \cq{*.example.com} into the \q{Valid
+hosts} box.
+
+\s{It's important to limit what the CA key is allowed to sign}. Don't
+just enter \cq{*} in that box! If you do that, you're saying that
+Example Corporation IT department is authorised to sign a host key for
+\e{anything at all} you might decide to connect to \dash even if
+you're connecting out of the company network to a machine somewhere
+else, such as your own personal server. So that configuration would
+enable the Example IT department to act as a \q{man-in-the-middle}
+between your PuTTY process and your server, and listen in to your
+communications \dash exactly the thing SSH is supposed to avoid.
+
+So, if the CA was provided to you by the sysadmins responsible for
+\cw{example.com} (or whatever), make sure PuTTY will \e{only} trust it
+for machines in the \cw{example.com} domain.
+
+For the full syntax of the \q{Valid hosts} expression, see
+\k{config-ssh-cert-valid-expr}.
+
+Finally, choose an identifying name for this CA; enter that name in
+the \q{Name for this CA} edit box at the top of the window, and press
+\q{Save} to record the CA in your configuration. The name you chose
+will appear in the list of saved CAs to the left of the \q{Save}
+button.
+
+The identifying name can be anything you like. It's there so that if
+you store multiple certificates you can tell which is which later when
+you want to edit or delete them. It also appears in the PuTTY Event
+Log when a server presents a certificate signed by that CA.
+
+To reload an existing CA configuration, select it in the list box and
+press \q{Load}. Then you can make changes, and save it again.
+
+To remove a CA from your configuration completely, select it in the
+list and press \q{Delete}.
+
+\S2{config-ssh-cert-valid-expr} Expressions you can enter in \q{Valid
+hosts}
+
+The simplest thing you can enter in the \q{Valid hosts this key is
+trusted to certify} edit box is just a hostname wildcard such as
+\cq{*.example.com}. This matches any host in any subdomain, so
+both \cq{ssh.example.com} and \cq{login.dept.example.com} would
+match, but \cq{prod.example.net} would not.
+
+But you can also enter multiple host name wildcards, and port number
+ranges, and make complicated Boolean expressions out of them using the
+operators \cq{&&} for \q{and}, \cq{||} for \q{or}, \cq{!} for \q{not},
+and parentheses.
+
+For example, here are some other things you could enter.
+
+\b \cq{*.foo.example.com || *.bar.example.com}. This means the CA is
+trusted to sign the host key for a connection if the host name matches
+\q{*.foo.example.com} \e{or} it matches \q{*.bar.example.com}. In
+other words, the CA has authority over those two particular subdomains
+of \cw{example.com}, but not for anything else, like
+\cw{www.example.com}.
+
+\b \cq{*.example.com && ! *.extrasecure.example.com}. This means the
+CA is trusted to sign the host key for a connection if the host name
+matches \q{*.example.com} \e{but does not} match
+\q{*.extrasecure.example.com}. (Imagine if there was one top-secret
+set of servers in your company that the main IT department didn't have
+security clearance to administer.)
+
+\b \cq{*.example.com && port:22}. This means the CA is trusted to sign
+the host key for a connection if the host name matches
+\q{*.example.com} \e{and} the port number is 22. SSH servers running
+on other ports would not be covered.
+
+\b \cq{(*.foo.example.com || *.bar.example.com) && port:0-1023}. This
+matches two subdomains of \cw{example.com}, as before, but \e{also}
+restricts the port number to the range 0-1023.
+
+A certificate configuration expression consists of one or more
+individual requirements which can each be a hostname wildcard, a
+single port number, or a port number range, combined together with
+these Boolean operators.
+
+Unlike other languages such as C, there is no implied priority between
+\cq{&&} and \cq{||}. If you write \cq{A && B || C} (where \cw{A},
+\cw{B} and \cw{C} are some particular requirements), then PuTTY will
+report a syntax error, because you haven't said which of the \cq{&&}
+and \cq{||} takes priority tightly. You will have to write either
+\cq{(A && B) || C}, meaning \q{both of \cw{A} and \cw{B}, or
+alternatively just \cw{C}}, or \cq{A && (B || C)} (\q{\cw{A}, and also
+at least one of \cw{B} and \cw{C}}), to make it clear.
+
+\S2{config-ssh-cert-rsa-hash} RSA signature types in certificates
+
+RSA keys can be used to generate signatures with a choice of secure
+hash function. Typically, any version of OpenSSH new enough to support
+certificates at all will also be new enough to avoid using SHA-1, so
+the default settings of accepting the more modern SHA-256 and SHA-512
+should be suitable for nearly all cases. For completeness, however,
+you can configure which types of RSA signature PuTTY will accept in a
+certificate from a CA using an RSA key.
+
 \H{config-ssh-encryption} The Cipher panel
 
 PuTTY supports a variety of different \i{encryption algorithm}s, and
@@ -2604,7 +2796,8 @@ PuTTY currently supports the following algorithms:
 
 \b \i{ChaCha20-Poly1305}, a combined cipher and \i{MAC} (SSH-2 only)
 
-\b \i{AES} (Rijndael) - 256, 192, or 128-bit SDCTR or CBC (SSH-2 only)
+\b \i{AES} (Rijndael) - 256, 192, or 128-bit SDCTR or CBC, or
+256 or 128-bit GCM (SSH-2 only)
 
 \b \i{Arcfour} (RC4) - 256 or 128-bit stream cipher (SSH-2 only)
 
@@ -2808,6 +3001,12 @@ username more than once, in case the server complains. If you know
 your server can cope with it, you can enable the \q{Allow attempted
 changes of username} option to modify PuTTY's behaviour.
 
+\H{config-ssh-auth-creds} The Credentials panel
+
+This subpane of the Auth panel contains configuration options that
+specify actual \e{credentials} to present to the server: key files and
+certificates.
+
 \S{config-ssh-privkey} \q{\ii{Private key} file for authentication}
 
 This box is where you enter the name of your private key file if you
@@ -2829,6 +3028,54 @@ in this case (in RFC 4716 or OpenSSH format), as that's sufficient to
 identify the key to Pageant, but of course if Pageant isn't present
 PuTTY can't fall back to using this file itself.
 
+\S{config-ssh-cert} \q{\ii{Certificate} to use with the private key}
+
+In some environments, user authentication keys can be signed in turn
+by a \q{certifying authority} (\q{CA} for short), and user accounts on
+an SSH server can be configured to automatically trust any key that's
+certified by the right signature.
+
+This can be a convenient setup if you have a very large number of
+servers. When you change your key pair, you might otherwise have to
+edit the \cw{authorized_keys} file on every server individually, to
+make them all accept the new key. But if instead you configure all
+those servers \e{once} to accept keys signed as yours by a CA, then
+when you change your public key, all you have to do is to get the new
+key certified by the same CA as before, and then all your servers will
+automatically accept it without needing individual reconfiguration.
+
+One way to use a certificate is to incorporate it into your private
+key file. \K{puttygen-cert} explains how to do that using PuTTYgen.
+But another approach is to tell PuTTY itself where to find the public
+certificate file, and then it will automatically present that
+certificate when authenticating with the corresponding private key.
+
+To do this, enter the pathname of the certificate file into the
+\q{Certificate to use with the private key} file selector.
+
+When this setting is configured, PuTTY will honour it no matter
+whether the private key is found in a file, or loaded into Pageant.
+
+\S{config-ssh-authplugin} \q{\ii{Plugin} to provide authentication responses}
+
+An SSH server can use the \q{keyboard-interactive} protocol to present
+a series of arbitrary questions and answers. Sometimes this is used
+for ordinary passwords, but sometimes the server will use the same
+mechanism for something more complicated, such as a one-time password
+system.
+
+Some of these systems can be automated. For this purpose, PuTTY allows
+you to provide a separate program to act as a \q{plugin} which will
+take over the authentication and send answers to the questions on your
+behalf.
+
+If you have been provided with a plugin of this type, you can
+configure it here, by entering a full command line in the \q{Plugin
+command to run} box.
+
+(If you want to \e{write} a plugin of this type, see \k{authplugin}
+for the full specification of how the plugin is expected to behave.)
+
 \H{config-ssh-auth-gssapi} The \i{GSSAPI} panel
 
 The \q{GSSAPI} subpanel of the \q{Auth} panel controls the use of
@@ -3388,6 +3635,32 @@ auto-detection relies on the version string in the server's greeting,
 and PuTTY has to decide whether to expect this bug \e{before} it sees
 the server's greeting. So this is a manual workaround only.
 
+\S{config-ssh-bug-filter-kexinit} \q{Chokes on PuTTY's full \cw{KEXINIT}}
+
+At the start of an SSH connection, the client and server exchange long
+messages of type \cw{SSH_MSG_KEXINIT}, containing lists of all the
+cryptographic algorithms they're prepared to use. This is used to
+negotiate a set of algorithms that both ends can speak.
+
+Occasionally, a badly written server might have a length limit on the
+list it's prepared to receive, and refuse to make a connection simply
+because PuTTY is giving it too many choices.
+
+A workaround is to enable this flag, which will make PuTTY wait to
+send \cw{KEXINIT} until after it receives the one from the server, and
+then filter its own \cw{KEXINIT} to leave out any algorithm the server
+doesn't also announce support for. This will generally make PuTTY's
+\cw{KEXINIT} at most the size of the server's, and will otherwise make
+no difference to the algorithm negotiation.
+
+This flag is a minor violation of the SSH protocol, because both sides
+are supposed to send \cw{KEXINIT} proactively. It still works provided
+\e{one} side sends its \cw{KEXINIT} without waiting, but if both
+client and server waited for the other one to speak first, the
+connection would deadlock. We don't know of any servers that do this,
+but if there is one, then this flag will make PuTTY unable to speak to
+them at all.
+
 \S{config-ssh-bug-sig} \q{Requires padding on SSH-2 \i{RSA} \i{signatures}}
 
 Versions below 3.3 of \i{OpenSSH} require SSH-2 RSA signatures to be

+ 38 - 0
source/putty/doc/errors.but

@@ -39,6 +39,9 @@ the one PuTTY has cached for this server}, means that PuTTY has
 connected to the SSH server before, knows what its host key
 \e{should} be, but has found a different one.
 
+(If the message instead talks about a \q{certified host key}, see
+instead \k{errors-cert-mismatch}.)
+
 This may mean that a malicious attacker has replaced your server
 with a different one, or has redirected your network connection to
 their own machine. On the other hand, it may simply mean that the
@@ -52,6 +55,41 @@ in the same way as you would if it was new.
 
 See \k{gs-hostkey} for more information on host keys.
 
+\H{errors-cert-mismatch} \q{This server presented a certified host key
+which was signed by a different certification authority ...}
+
+If you've configured PuTTY to trust at least one
+\I{certificate}certification authority for signing host keys (see
+\k{config-ssh-kex-cert}), then it will ask the SSH server to send it
+any available certified host keys. If the server sends back a
+certified key signed by a \e{different} certification authority, PuTTY
+will present this variant of the host key prompt, preceded by
+\q{WARNING - POTENTIAL SECURITY BREACH!}
+
+One reason why this can happen is a deliberate attack. Just like an
+ordinary man-in-the-middle attack which substitutes a wrong host key,
+a particularly ambitious attacker might substitute an entire wrong
+certification authority, and hope that you connect anyway.
+
+But it's also possible in some situations that this error might arise
+legitimately. For example, if your organisation's IT department has
+just rolled out a new CA key which you haven't yet entered in PuTTY's
+configuration, or if your CA configuration involves two overlapping
+domains, or something similar.
+
+So, unfortunately, you'll have to work out what to do about it
+yourself: make an exception for this specific case, or abandon this
+connection and install a new CA key before trying again (if you're
+really sure you trust the CA), or edit your configuration in some
+other way, or just stop trying to use this server.
+
+If you're convinced that this particular server is legitimate even
+though the CA is not one you trust, PuTTY will let you cache the
+certified host key, treating it in the same way as an uncertified one.
+Then that particular certificate will be accepted for future
+connections to this specific server, even though other certificates
+signed by the same CA will still be rejected.
+
 \H{errors-ssh-protocol} \q{SSH protocol version 2 required by our
 configuration but remote only provides (old, insecure) SSH-1}
 

+ 31 - 16
source/putty/doc/faq.but

@@ -150,7 +150,7 @@ data at the server end; it's your guarantee that it hasn't been
 removed and replaced somewhere on the way. Host key checking makes
 the attacker's job \e{astronomically} hard, compared to packet
 sniffing, and even compared to subverting a router. Instead of
-applying a little intelligence and keeping an eye on Bugtraq, the
+applying a little intelligence and keeping an eye on oss-security, the
 attacker must now perform a brute-force attack against at least one
 military-strength cipher. That insignificant host key prompt really
 does make \e{that} much difference.
@@ -220,7 +220,7 @@ Currently, release versions of PuTTY tools only run on Windows
 systems and Unix.
 
 As of 0.68, the supplied PuTTY executables run on versions of Windows
-from XP onwards, up to and including Windows 10; and we know of no
+from XP onwards, up to and including Windows 11; and we know of no
 reason why PuTTY should not continue to work on future versions of
 Windows. We provide 32-bit and 64-bit Windows executables for the
 common x86 processor family; see \k{faq-32bit-64bit} for discussion
@@ -250,8 +250,7 @@ There are Unix ports of most of the traditional PuTTY tools, and also
 one entirely new application.
 
 If you look at the source release, you should find a \c{unix}
-subdirectory. There are a couple of ways of building it,
-including the usual \c{configure}/\c{make}; see the file \c{README}
+subdirectory. You need \c{cmake} to build it; see the file \c{README}
 in the source distribution. This should build you:
 
 \b Unix ports of PuTTY, Plink, PSCP, and PSFTP, which work pretty much
@@ -585,7 +584,7 @@ You can also paste by pressing Shift-Ins.
 keys, proxying, cipher selection, etc.) in PSCP, PSFTP and Plink?
 
 Most major features (e.g., public keys, port forwarding) are available
-through command line options. See the documentation.
+through command line options. See \k{using-general-opts}.
 
 Not all features are accessible from the command line yet, although
 we'd like to fix this. In the meantime, you can use most of
@@ -607,9 +606,16 @@ To use PSCP properly, run it from a Command Prompt window. See
 \S{faq-pscp-spaces}{Question} \I{spaces in filenames}How do I use
 PSCP to copy a file whose name has spaces in?
 
-If PSCP is using the traditional SCP protocol, this is confusing. If
-you're specifying a file at the local end, you just use one set of
-quotes as you would normally do:
+If PSCP is using the newer SFTP protocol (which is usual with most
+modern servers), this is straightforward; all filenames with spaces
+in are specified using a single pair of quotes in the obvious way:
+
+\c pscp "local file" user@host:
+\c pscp user@host:"remote file" .
+
+However, if PSCP is using the older SCP protocol for some reason,
+things are more confusing. If you're specifying a file at the local
+end, you just use one set of quotes as you would normally do:
 
 \c pscp "local filename with spaces" user@host:
 \c pscp user@host:myfile "local filename with spaces"
@@ -633,13 +639,6 @@ Instead, you need to specify the local file name in full:
 
 \c c:\>pscp user@host:"\"oo er\"" "oo er"
 
-If PSCP is using the newer SFTP protocol, none of this is a problem,
-and all filenames with spaces in are specified using a single pair
-of quotes in the obvious way:
-
-\c pscp "local file" user@host:
-\c pscp user@host:"remote file" .
-
 \S{faq-32bit-64bit}{Question} Should I run the 32-bit or the
 64-bit version?
 
@@ -1153,6 +1152,22 @@ running, but it doesn't stop the process's memory as a whole from
 being swapped completely out to disk when the process is long-term
 inactive. And Pageant spends most of its time inactive.
 
+\S{faq-windowsstore}{Question} Is the version of PuTTY in the
+\i{Microsoft Store} legit?
+
+The free-of-charge \q{PuTTY} application at
+\W{https://apps.microsoft.com/store/detail/putty/XPFNZKSKLBP7RJ}{this link}
+is published and maintained by us. The copy there is the latest
+release, usually updated within a few days of us publishing it on our
+own website.
+
+There have been other copies of PuTTY on the store, some looking quite
+similar, and some charging money. Those were uploaded by other people,
+and we can't guarantee anything about them.
+
+The first version we published to the Microsoft Store was 0.76 (some
+time after its initial release on our website).
+
 \H{faq-admin} Administrative questions
 
 \S{faq-putty-org}{Question} Is \cw{putty.org} your website?
@@ -1287,7 +1302,7 @@ Small donations (tens of dollars or tens of euros) will probably be
 spent on beer or curry, which helps motivate our volunteer team to
 continue doing this for the world. Larger donations will be spent on
 something that actually helps development, if we can find anything
-(perhaps new hardware, or a copy of Windows XP), but if we can't
+(perhaps new hardware, or a new version of Windows), but if we can't
 find anything then we'll just distribute the money among the
 developers. If you want to be sure your donation is going towards
 something worthwhile, ask us first. If you don't like these terms,

+ 4 - 4
source/putty/doc/gs.but

@@ -80,10 +80,10 @@ PuTTY \I{host key cache}records the host key for each server you
 connect to, in the Windows \i{Registry}. Every time you connect to a
 server, it checks that the host key presented by the server is the
 same host key as it was the last time you connected. If it is not,
-you will see a warning, and you will have the chance to abandon your
-connection before you type any private information (such as a
-password) into it. (See \k{errors-hostkey-wrong} for what that looks
-like.)
+you will see a stronger warning, and you will have the chance to
+abandon your connection before you type any private information (such
+as a password) into it. (See \k{errors-hostkey-wrong} for what that
+looks like.)
 
 However, when you connect to a server you have not connected to
 before, PuTTY has no way of telling whether the host key is the

+ 24 - 0
source/putty/doc/index.but

@@ -688,6 +688,16 @@ saved sessions from
 \IM{group exchange} Diffie-Hellman group exchange
 \IM{group exchange} group exchange, Diffie-Hellman
 
+\IM{ECDH} \q{ECDH} (elliptic-curve Diffie-Hellman)
+\IM{ECDH} elliptic-curve Diffie-Hellman key exchange
+\IM{ECDH} key exchange, elliptic-curve Diffie-Hellman
+\IM{ECDH} Diffie-Hellman key exchange, with elliptic curves
+
+\IM{Streamlined NTRU Prime} Streamlined NTRU Prime
+\IM{Streamlined NTRU Prime} NTRU Prime
+
+\IM{quantum attacks} quantum attacks, resistance to
+
 \IM{repeat key exchange} repeat key exchange
 \IM{repeat key exchange} key exchange, repeat
 
@@ -819,6 +829,12 @@ saved sessions from
 \IM{DSA} DSA
 \IM{DSA} Digital Signature Standard
 
+\IM{ECDSA} ECDSA
+\IM{ECDSA} elliptic-curve DSA
+
+\IM{NIST} NIST-standardised elliptic curves
+\IM{NIST} elliptic curves, NIST-standardised
+
 \IM{EdDSA} EdDSA
 \IM{EdDSA} Edwards-curve DSA
 
@@ -942,3 +958,11 @@ saved sessions from
 
 \IM{shifted arrow keys} arrow keys, shifted
 \IM{shifted arrow keys} shifted arrow keys
+
+\IM{certificate}{certificates} certificates, SSH
+\IM{certificate}{certificates} SSH certificates
+\IM{certificate}{certificates} OpenSSH certificates
+\IM{certificate}{certificates} CA (certification authority)
+
+\IM{Microsoft Store} Microsoft Store
+\IM{Microsoft Store} Windows Store

+ 6 - 0
source/putty/doc/man-pageant.but

@@ -256,6 +256,12 @@ be matched
 
 \dd to indicate that it is a fingerprint of a specific format
 
+\dt \cq{sha256-cert:} or \cq{md5-cert:}
+
+\dd to indicate that it is a fingerprint of a specific format, and
+specifically matches the fingerprint of the public key \e{including} a
+certificate if any
+
 }
 
 \dt \cw{--public-openssh} \e{key-identifiers}, \cw{-L} \e{key-identifiers}

+ 51 - 11
source/putty/doc/man-puttygen.but

@@ -12,10 +12,12 @@
 \e bbbbbbbb   iiiiiii   bb iiiiiii   bb iiii     bbbbbbbb iiiiii     bb
 \c          [ -C new-comment ] [ -P ] [ --reencrypt ]
 \e            bb iiiiiiiiiii     bb     bbbbbbbbbbb
-\c          [ -O output-type | -l | -L | -p | --dump ] [ -E fptype ]
-\e            bb iiiiiiiiiii   bb   bb   bb   bbbbbb     bb iiiiii
-\c             [ --ppk-param key=value,... ]
-\e               bbbbbbbbbbb iiibiiiiib
+\c          [ --certificate cert-file | --remove-certificate ]
+\e            bbbbbbbbbbbbb iiiiiiiii   bbbbbbbbbbbbbbbbbbbb
+\c          [ -O output-type | -l | -L | -p | --dump | --cert-info ]
+\e            bb iiiiiiiiiii   bb   bb   bb   bbbbbb   bbbbbbbbbbb  
+\c             [ --ppk-param key=value,... | -E fptype ]
+\e               bbbbbbbbbbb iiibiiiiib      bb iiiiii
 \c          [ -o output-file ]
 \e            bb iiiiiiiiiii
 
@@ -58,8 +60,9 @@ ssh.com's implementation.
 
 You can also specify a file containing only a \e{public} key here.
 The operations you can do are limited to outputting another public
-key format or a fingerprint. Public keys can be in RFC 4716 or
-OpenSSH format, or the standard SSH-1 format.
+key format (possibly removing an attached certificate first), or a
+fingerprint. Public keys can be in RFC 4716 or OpenSSH format, or
+the standard SSH-1 format.
 
 }
 
@@ -143,6 +146,19 @@ to type).
 automatic when you are generating a new key, but not when you are
 modifying an existing key.
 
+\dt \cw{\-\-certificate} \e{certificate-file}
+
+\dd Adds an OpenSSH-style certificate to the public half of the key,
+so that the output file contains a certified public key with the same
+private key. If the input file already contained a certificate, it
+will be replaced with the new one. (Use \cq{-} to read a certificate
+from standard input.)
+
+\dt \cw{\-\-remove\-certificate}
+
+\dd Removes any certificate that was part of the key, to recover the
+uncertified version of the underlying key.
+
 \dt \cw{\-\-reencrypt}
 
 \dd For an existing private key saved with a passphrase, refresh the
@@ -260,6 +276,13 @@ newer format even for RSA, DSA, and ECDSA keys.
 \dd Save an SSH-2 private key in ssh.com's format. This option is not
 permitted for SSH-1 keys.
 
+\dt \cw{cert-info}
+
+\dd Save a textual dump of information about the certificate on the
+key, if any: whether it's a host or a user certificate, what host(s)
+or user(s) it's certified to be, its validity period, ID and serial
+number, and the fingerprint of the signing CA.
+
 \dt \cw{text}
 
 \dd Save a textual dump of the numeric components comprising the key
@@ -269,8 +292,9 @@ SSH.
 
 \lcont{
 The output consists of a series of \cw{name=value} lines, where each
-\c{value} is either a C-like string literal in double quotes, or a
-hexadecimal number starting with \cw{0x...}
+\c{value} is either a C-like string literal in double quotes, a
+hexadecimal number starting with \cw{0x...}, or a binary blob
+encoded with base64, denoted by \cw{b64("...")}.
 }
 
 If no output type is specified, the default is \c{private}.
@@ -283,8 +307,9 @@ If no output type is specified, the default is \c{private}.
 this option is not specified, \c{puttygen} will assume you want to
 overwrite the original file if the input and output file types are
 the same (changing a comment or passphrase), and will assume you
-want to output to stdout if you are asking for a public key or
-fingerprint. Otherwise, the \c{\-o} option is required.
+want to output to stdout if you are asking for a public key,
+fingerprint, or one of the textual dump types. Otherwise, the
+\c{\-o} option is required.
 
 \dt \cw{\-l}
 
@@ -298,6 +323,10 @@ fingerprint. Otherwise, the \c{\-o} option is required.
 
 \dd Synonym for \q{\cw{-O public}}.
 
+\dt \cw{\-\-cert\-info}
+
+\dd Synonym for \q{\cw{-O cert-info}}.
+
 \dt \cw{\-\-dump}
 
 \dd Synonym for \q{\cw{-O text}}.
@@ -305,7 +334,18 @@ fingerprint. Otherwise, the \c{\-o} option is required.
 \dt \cw{-E} \e{fptype}
 
 \dd Specify the algorithm to use if generating a fingerprint. The
-options are \cw{sha256} (the default) and \cw{md5}.
+available algorithms are are \cw{sha256} (the default) and \cw{md5}.
+
+\lcont{
+
+By default, when showing the fingerprint of a public key that includes
+a certificate, \c{puttygen} will not include the certificate, so that
+the fingerprint shown will be the same as the underlying public key.
+If you want the fingerprint including the certificate (for example, so
+as to tell two certified keys apart), you can specify \cw{sha256-cert}
+or \cw{md5-cert} as the fingerprint type.
+
+}
 
 \dt \cw{\-\-new\-passphrase} \e{file}
 

+ 65 - 13
source/putty/doc/pageant.but

@@ -64,21 +64,24 @@ The large list box in the Pageant main window lists the private keys
 that are currently loaded into Pageant. The list might look
 something like this:
 
-\c ssh-ed25519  SHA256:TddlQk20DVs4LRcAsIfDN9pInKpY06D+h4kSHwWAj4w
-\c ssh-rsa 2048 SHA256:8DFtyHm3kQihgy52nzX96qMcEVOq7/yJmmwQQhBWYFg
+\c Ed25519    SHA256:TddlQk20DVs4LRcAsIfDN9pInKpY06D+h4kSHwWAj4w
+\c RSA  2048  SHA256:8DFtyHm3kQihgy52nzX96qMcEVOq7/yJmmwQQhBWYFg
 
 For each key, the list box will tell you:
 
 \b The type of the key. Currently, this can be
-\c{ssh-rsa} (an RSA key for use with the SSH-2 protocol),
-\c{ssh-dss} (a DSA key for use with the SSH-2 protocol),
-\c{ecdsa-sha2-*} (an ECDSA key for use with the SSH-2 protocol),
-\c{ssh-ed25519} (an Ed25519 key for use with the SSH-2 protocol),
-\c{ssh-ed448} (an Ed448 key for use with the SSH-2 protocol),
-or \c{ssh1} (an RSA key for use with the old SSH-1 protocol).
+\q{RSA} (an RSA key for use with the SSH-2 protocol),
+\q{DSA} (a DSA key for use with the SSH-2 protocol),
+\q{\i{NIST}} (an ECDSA key for use with the SSH-2 protocol),
+\q{Ed25519} (an Ed25519 key for use with the SSH-2 protocol),
+\q{Ed448} (an Ed448 key for use with the SSH-2 protocol),
+or \q{SSH-1} (an RSA key for use with the old SSH-1 protocol).
+(If the key has an associated certificate, this is shown here with a
+\q{cert} suffix.)
 
 \b The size (in bits) of the key, for key types that come in different
-sizes.
+sizes. (For ECDSA \q{NIST} keys, this is indicated as \q{p256} or
+\q{p384} or \q{p521}.)
 
 \b The \I{key fingerprint}fingerprint for the public key. This should be
 the same fingerprint given by PuTTYgen, and (hopefully) also the same
@@ -86,10 +89,20 @@ fingerprint shown by remote utilities such as \i\c{ssh-keygen} when
 applied to your \c{authorized_keys} file.
 
 \lcont{
-By default this is shown in the \q{SHA256} format. You can change to the
-older \q{MD5} format (which looks like \c{aa:bb:cc:...}) with the
-\q{Fingerprint type} drop-down, but bear in mind that this format is
-less secure and should be avoided for comparison purposes where possible.
+For SSH-2 keys, by default this is shown in the \q{SHA256} format. You
+can change to the older \q{MD5} format (which looks like \c{aa:bb:cc:...})
+with the \q{Fingerprint type} drop-down, but bear in mind that this
+format is less secure and should be avoided for comparison purposes
+where possible.
+
+If some of the keys loaded into Pageant have certificates attached,
+then Pageant will default to showing the fingerprint of the underlying
+key. This way, a certified and uncertified version of the same key
+will have the same fingerprint, so you can see that they match. You
+can instead use the \q{Fingerprint type} drop-down to ask for a
+different fingerprint to be shown for certified keys, which includes
+the certificate as part of the fingerprinted data. That way you can
+tell two certificates apart.
 }
 
 \b The comment attached to the key.
@@ -217,6 +230,45 @@ point at a different program. You could point it at
 \cw{c:\\Windows\\System32\\OpenSSH\\ssh.exe} once you've done this
 setup \dash but it's just as easy to point it at Plink!
 
+\S{pageant-cmdline-unix} Unix-domain sockets: integrating with WSL 1
+
+Pageant can listen on the WinSock implementation of \q{Unix-domain
+sockets}. These interoperate with the Unix-domain sockets found in the
+original Windows Subsystem for Linux (now known as WSL 1). So if you
+ask Pageant to listen on one of these, then your WSL 1 processes can
+talk directly to Pageant.
+
+To configure this, run Pageant with the option \c{--unix}, followed
+with a pathname. Then, in WSL 1, set the environment variable
+\cw{SSH_AUTH_SOCK} to point at the WSL translation of that pathname.
+
+For example, you might run
+
+\c pageant --unix C:\Users\Simon\.ssh\agent.sock
+
+and in WSL 1, set the environment variable
+
+\c SSH_AUTH_SOCK=/mnt/c/Users/Simon/.ssh/agent.sock
+
+Alternatively, you can add a line to your \cw{.ssh/config} file inside
+WSL that says
+
+\c IdentityAgent /mnt/c/Users/Simon/.ssh/agent.sock
+
+although doing it like that may mean that \cw{ssh-add} commands won't
+find the agent, even though \cw{ssh} itself will.
+
+\s{Security note}: Unix-domain sockets are protected against access by
+other users by the file protections on their containing directory. So
+if your Windows machine is multiuser, make sure you create the socket
+inside a directory that other users can't access at all. (In fact,
+that's a good idea on general principles.)
+
+\s{Compatibility note}: WSL 2 processes cannot talk to Pageant by this
+mechanism, because WSL 2's Unix-domain sockets are managed by a
+separate Linux kernel, and not by the same kernel that WinSock talks
+to.
+
 \S{pageant-cmdline-keylist} Starting with the key list visible
 
 Start Pageant with the \i\c{--keylist} option to show the main window

+ 1 - 1
source/putty/doc/plink.but

@@ -41,7 +41,7 @@ use Plink:
 
 \c C:\>plink
 \c Plink: command-line connection utility
-\c Release 0.77
+\c Release 0.78
 \c Usage: plink [options] [user@]host [command]
 \c        ("host" can also be a PuTTY saved session name)
 \c Options:

+ 1 - 1
source/putty/doc/pscp.but

@@ -39,7 +39,7 @@ use PSCP:
 
 \c C:\>pscp
 \c PuTTY Secure Copy client
-\c Release 0.77
+\c Release 0.78
 \c Usage: pscp [options] [user@]host:source target
 \c        pscp [options] source [source...] [user@]host:target
 \c        pscp [options] -ls [user@]host:filespec

+ 59 - 13
source/putty/doc/pubkey.but

@@ -62,9 +62,9 @@ The key types supported by PuTTY are described in \k{puttygen-keytype}.
 \H{pubkey-puttygen} Using \i{PuTTYgen}, the PuTTY key generator
 
 PuTTYgen is a key generator. It \I{generating keys}generates pairs of
-public and private keys to be used with PuTTY, PSCP, and Plink, as well
-as the PuTTY authentication agent, Pageant (see \k{pageant}).  PuTTYgen
-generates RSA, DSA, ECDSA, and EdDSA keys.
+public and private keys to be used with PuTTY, PSCP, PSFTP, and Plink,
+as well as the PuTTY authentication agent, Pageant (see \k{pageant}).
+PuTTYgen generates RSA, DSA, ECDSA, and EdDSA keys.
 
 When you run PuTTYgen you will see a window where you have two main
 choices: \q{Generate}, to generate a new public/private key pair, or
@@ -132,10 +132,13 @@ The \q{Number of bits} input box allows you to choose the strength
 of the key PuTTYgen will generate.
 
 \b For RSA and DSA, 2048 bits should currently be sufficient for most
-purposes.
+purposes. (Smaller keys of these types are no longer considered
+secure, and PuTTYgen will warn if you try to generate them.)
 
-\b For ECDSA, only 256, 384, and 521 bits are supported. (ECDSA offers
-equivalent security to RSA with smaller key sizes.)
+\b For ECDSA, only 256, 384, and 521 bits are supported, corresponding
+to \i{NIST}-standardised elliptic curves. (Elliptic-curve keys do not
+need as many bits as RSA keys for equivalent security, so these numbers
+are smaller than the RSA recommendations.)
 
 \b For EdDSA, the only valid sizes are 255 bits (these keys are also
 known as \q{\i{Ed25519}} and are commonly used) and 448 bits
@@ -145,6 +148,9 @@ the same as 255.)
 
 \S{puttygen-primes} Selecting the \i{prime generation method}
 
+(This is entirely optional. Unless you know better, it's entirely
+sensible to skip this and use the default settings.)
+
 On the \q{Key} menu, you can also optionally change the method for
 generating the prime numbers used in the generated key. This is used
 for RSA and DSA keys only. (The other key types don't require
@@ -154,9 +160,6 @@ The prime-generation method does not affect compatibility: a key
 generated with any of these methods will still work with all the same
 SSH servers.
 
-If you don't care about this, it's entirely sensible to leave it on the
-default setting.
-
 The available methods are:
 
 \b Use \i{probable primes} (fast)
@@ -239,9 +242,9 @@ a particular fingerprint. So some utilities, such as the Pageant key
 list box (see \k{pageant-mainwin-keylist}) and the Unix \c{ssh-add}
 utility, will list key fingerprints rather than the whole public key.
 
-By default, PuTTYgen will display fingerprints in the \q{SHA256}
-format. If you need to see the fingerprint in the older \q{MD5} format
-(which looks like \c{aa:bb:cc:...}), you can choose
+By default, PuTTYgen will display SSH-2 key fingerprints in the
+\q{SHA256} format. If you need to see the fingerprint in the older
+\q{MD5} format (which looks like \c{aa:bb:cc:...}), you can choose
 \q{Show fingerprint as MD5} from the \q{Key} menu, but bear in mind
 that this is less cryptographically secure; it may be feasible for
 an attacker to create a key with the same fingerprint as yours.
@@ -307,6 +310,48 @@ a result.
 
 \e{Do not forget your passphrase}. There is no way to recover it.
 
+\S{puttygen-cert} Adding a \i{certificate} to your key
+
+In some environments, user authentication keys can be signed in turn
+by a \q{certifying authority} (\q{CA} for short), and user accounts on
+an SSH server can be configured to automatically trust any key that's
+certified by the right signature.
+
+This can be a convenient setup if you have a very large number of
+servers. When you change your key pair, you might otherwise have to
+edit the \cw{authorized_keys} file on every server individually, to
+make them all accept the new key. But if instead you configure all
+those servers \e{once} to accept keys signed as yours by a CA, then
+when you change your public key, all you have to do is to get the new
+key certified by the same CA as before, and then all your servers will
+automatically accept it without needing individual reconfiguration.
+
+To get your key signed by a CA, you'll probably send the CA the new
+\e{public} key (not the private half), and get back a modified version
+of the public key with the certificate included.
+
+If you want to incorporate the certificate into your PPK file for
+convenience, you can use the \q{Add certificate to key} menu option in
+PuTTYgen's \q{Key} menu. This will give you a single file containing
+your private key and the certificate, which is everything you need to
+authenticate to a server prepared to accept that certificate. 
+
+To remove the certificate again and restore the uncertified PPK file,
+there's also a \q{Remove certificate from key} option.
+
+(However, you don't \e{have} to incorporate the certificate into your
+PPK file. You can equally well use it separately, via the
+\q{Certificate to use with the private key} option in PuTTY itself.
+See \k{config-ssh-cert}. It's up to you which you find more
+convenient.)
+
+When the currently loaded key in PuTTYgen contains a certificate, the
+large \q{Public key for pasting} edit box (see \k{puttygen-pastekey})
+is replaced by a button that brings up an information box telling you
+about the certificate, such as who it certifies your key as belonging
+to, when it expires (if ever), and the fingerprint of the CA key that
+signed it in turn.
+
 \S{puttygen-savepriv} Saving your private key to a disk file
 
 Once you have generated a key, set a comment field and set a
@@ -493,7 +538,8 @@ The options supported on the command line are:
 \dt \cw{\-t} \e{keytype}
 
 \dd Type of key to generate. You can select \c{rsa}, \c{dsa},
-\c{ecdsa}, \c{eddsa} or \c{rsa1}. See \k{puttygen-keytype}.
+\c{ecdsa}, \c{eddsa}, \c{ed25519}, \c{ed448}, or \c{rsa1}.
+See \k{puttygen-keytype}.
 
 \dt \cw{\-b} \e{bits}
 

+ 3 - 3
source/putty/doc/pubkeyfmt.but

@@ -7,7 +7,7 @@ In this appendix, binary data structures are described using data type
 representations such as \cq{uint32}, \cq{string} and \cq{mpint} as
 used in the SSH protocol standards themselves. These are defined
 authoritatively by
-\W{https://tools.ietf.org/html/rfc4251#section-5}{RFC 4251 section 5},
+\W{https://www.rfc-editor.org/rfc/rfc4251#section-5}{RFC 4251 section 5},
 \q{Data Type Representations Used in the SSH Protocols}.
 
 \H{ppk-overview} Overview
@@ -86,7 +86,7 @@ can contain any byte values other than 13 and 10 (CR and LF).
 
 The next part of the file gives the public key. This is stored
 unencrypted but base64-encoded
-(\W{https://tools.ietf.org/html/rfc4648}{RFC 4648}), and is preceded
+(\W{https://www.rfc-editor.org/rfc/rfc4648}{RFC 4648}), and is preceded
 by a header line saying how many lines of base64 data are shown,
 looking like this:
 
@@ -241,7 +241,7 @@ of \e{y} in the group generated by \e{g} mod \e{p}.
 
 \S{ppk-privkey-ecdsa} NIST elliptic-curve keys
 
-NIST elliptic-curve keys are stored using one of the following
+\i{NIST} elliptic-curve keys are stored using one of the following
 \s{algorithm-name} values, each corresponding to a different elliptic
 curve and key size:
 

File diff suppressed because it is too large
+ 396 - 100
source/putty/doc/puttydoc.txt


+ 1 - 1
source/putty/doc/sshnames.but

@@ -70,7 +70,7 @@ They have been superseded by \cw{arcfour128} and \cw{arcfour256}.
 
 The SSH agent protocol, which is only specified in an Internet-Draft
 at the time of writing
-(\W{https://tools.ietf.org/html/draft-miller-ssh-agent}\cw{draft-miller-ssh-agent}),
+(\W{https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent}\cw{draft-miller-ssh-agent}),
 defines an extension mechanism. These names can be sent in an
 \cw{SSH_AGENTC_EXTENSION} message.
 

+ 22 - 0
source/putty/doc/using.but

@@ -1024,6 +1024,19 @@ This option is equivalent to the \q{Private key file for
 authentication} box in the Auth panel of the PuTTY configuration box
 (see \k{config-ssh-privkey}).
 
+\S2{using-cmdline-cert} \i\c{-cert}: specify an SSH \i{certificate}
+
+The \c{-cert} option allows you to specify the name of a certificate
+file containing a signed version of your public key. If you specify
+this option, PuTTY will present that certificate in place of the plain
+public key, whenever it tries to authenticate with a key that matches.
+(This applies whether the key is stored in Pageant or loaded directly
+from a file by PuTTY.)
+
+This option is equivalent to the \q{Certificate to use with the
+private key} box in the Auth panel of the PuTTY configuration box (see
+\k{config-ssh-cert}).
+
 \S2{using-cmdline-no-trivial-auth} \i\c{-no-trivial-auth}: disconnect
 if SSH authentication succeeds trivially
 
@@ -1162,3 +1175,12 @@ the extra protection), so it's reasonable to want to run Pageant but
 not PuTTY with the ACL restrictions. You can force Pageant to start
 subsidiary PuTTY processes with a restricted ACL if you also pass the
 \i\c{-restrict-putty-acl} option.
+
+\S2{using-cmdline-host-ca} \i{\c{-host-ca}}: launch the
+\I{certificate}host CA configuration
+
+If you start PuTTY with the \c{-host-ca} option, it will not launch a
+session at all. Instead, it will just display the configuration dialog
+box for host certification authorities, as described in
+\k{config-ssh-kex-cert}. When you dismiss that dialog box, PuTTY will
+terminate.

+ 1 - 1
source/putty/doc/version.but

@@ -1 +1 @@
-ersionid PuTTY release 0.77
+\versionid PuTTY release 0.78

+ 49 - 51
source/putty/import.c

@@ -174,17 +174,6 @@ bool export_ssh2(const Filename *filename, int type,
     return false;
 }
 
-/*
- * Strip trailing CRs and LFs at the end of a line of text.
- */
-void strip_crlf(char *str)
-{
-    char *p = str + strlen(str);
-
-    while (p > str && (p[-1] == '\r' || p[-1] == '\n'))
-        *--p = '\0';
-}
-
 /* ----------------------------------------------------------------------
  * Helper routines. (The base64 ones are defined in sshpubk.c.)
  */
@@ -328,7 +317,7 @@ struct openssh_pem_key {
     strbuf *keyblob;
 };
 
-void BinarySink_put_mp_ssh2_from_string(BinarySink *bs, ptrlen str)
+static void BinarySink_put_mp_ssh2_from_string(BinarySink *bs, ptrlen str)
 {
     const unsigned char *bytes = (const unsigned char *)str.ptr;
     size_t nbytes = str.len;
@@ -498,7 +487,7 @@ static struct openssh_pem_key *load_openssh_pem_key(BinarySource *src,
     if (errmsg_p) *errmsg_p = NULL;
     return ret;
 
-    error:
+  error:
     if (line) {
         smemclr(line, strlen(line));
         sfree(line);
@@ -801,7 +790,7 @@ static ssh2_userkey *openssh_pem_read(
     errmsg = NULL;                     /* no error */
     retval = retkey;
 
-    error:
+  error:
     strbuf_free(blob);
     strbuf_free(key->keyblob);
     smemclr(key, sizeof(*key));
@@ -811,7 +800,7 @@ static ssh2_userkey *openssh_pem_read(
 }
 
 static bool openssh_pem_write(
-    const Filename *filename, ssh2_userkey *key, const char *passphrase)
+    const Filename *filename, ssh2_userkey *ukey, const char *passphrase)
 {
     strbuf *pubblob, *privblob, *outblob;
     unsigned char *spareblob;
@@ -825,13 +814,17 @@ static bool openssh_pem_write(
     FILE *fp;
     BinarySource src[1];
 
+    /* OpenSSH's private key files never contain a certificate, so
+     * revert to the underlying base key if necessary */
+    ssh_key *key = ssh_key_base_key(ukey->key);
+
     /*
      * Fetch the key blobs.
      */
     pubblob = strbuf_new();
-    ssh_key_public_blob(key->key, BinarySink_UPCAST(pubblob));
+    ssh_key_public_blob(key, BinarySink_UPCAST(pubblob));
     privblob = strbuf_new_nm();
-    ssh_key_private_blob(key->key, BinarySink_UPCAST(privblob));
+    ssh_key_private_blob(key, BinarySink_UPCAST(privblob));
     spareblob = NULL;
 
     outblob = strbuf_new_nm();
@@ -840,8 +833,8 @@ static bool openssh_pem_write(
      * Encode the OpenSSH key blob, and also decide on the header
      * line.
      */
-    if (ssh_key_alg(key->key) == &ssh_rsa ||
-        ssh_key_alg(key->key) == &ssh_dsa) {
+    if (ssh_key_alg(key) == &ssh_rsa ||
+        ssh_key_alg(key) == &ssh_dsa) {
         strbuf *seq;
 
         /*
@@ -851,7 +844,7 @@ static bool openssh_pem_write(
          * bignums per key type and then construct the actual blob in
          * common code after that.
          */
-        if (ssh_key_alg(key->key) == &ssh_rsa) {
+        if (ssh_key_alg(key) == &ssh_rsa) {
             ptrlen n, e, d, p, q, iqmp, dmp1, dmq1;
             mp_int *bd, *bp, *bq, *bdmp1, *bdmq1;
 
@@ -947,11 +940,11 @@ static bool openssh_pem_write(
         put_ber_id_len(outblob, 16, seq->len, ASN1_CONSTRUCTED);
         put_data(outblob, seq->s, seq->len);
         strbuf_free(seq);
-    } else if (ssh_key_alg(key->key) == &ssh_ecdsa_nistp256 ||
-               ssh_key_alg(key->key) == &ssh_ecdsa_nistp384 ||
-               ssh_key_alg(key->key) == &ssh_ecdsa_nistp521) {
+    } else if (ssh_key_alg(key) == &ssh_ecdsa_nistp256 ||
+               ssh_key_alg(key) == &ssh_ecdsa_nistp384 ||
+               ssh_key_alg(key) == &ssh_ecdsa_nistp521) {
         const unsigned char *oid;
-        struct ecdsa_key *ec = container_of(key->key, struct ecdsa_key, sshk);
+        struct ecdsa_key *ec = container_of(key, struct ecdsa_key, sshk);
         int oidlen;
         int pointlen;
         strbuf *seq, *sub;
@@ -966,7 +959,7 @@ static bool openssh_pem_write(
          *   [1]
          *     BIT STRING (0x00 public key point)
          */
-        oid = ec_alg_oid(ssh_key_alg(key->key), &oidlen);
+        oid = ec_alg_oid(ssh_key_alg(key), &oidlen);
         pointlen = (ec->curve->fieldBits + 7) / 8 * 2;
 
         seq = strbuf_new_nm();
@@ -998,7 +991,7 @@ static bool openssh_pem_write(
 
         /* Append the BIT STRING to the sequence */
         put_ber_id_len(seq, 1, sub->len,
-                         ASN1_CLASS_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED);
+                       ASN1_CLASS_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED);
         put_data(seq, sub->s, sub->len);
         strbuf_free(sub);
 
@@ -1075,12 +1068,12 @@ static bool openssh_pem_write(
             fprintf(fp, "%02X", iv[i]);
         fprintf(fp, "\n\n");
     }
-    base64_encode(fp, outblob->u, outblob->len, 64);
+    base64_encode_fp(fp, ptrlen_from_strbuf(outblob), 64);
     fputs(footer, fp);
     fclose(fp);
     ret = true;
 
-    error:
+  error:
     if (outblob)
         strbuf_free(outblob);
     if (spareblob) {
@@ -1245,8 +1238,8 @@ static struct openssh_new_key *load_openssh_new_key(BinarySource *filesrc,
         ret->kdfopts.bcrypt.rounds = get_uint32(opts);
 
         if (get_err(opts)) {
-          errmsg = "failed to parse bcrypt options string";
-          goto error;
+            errmsg = "failed to parse bcrypt options string";
+            goto error;
         }
         break;
       }
@@ -1294,7 +1287,7 @@ static struct openssh_new_key *load_openssh_new_key(BinarySource *filesrc,
     if (errmsg_p) *errmsg_p = NULL;
     return ret;
 
-    error:
+  error:
     if (line) {
         smemclr(line, strlen(line));
         sfree(line);
@@ -1484,7 +1477,7 @@ static ssh2_userkey *openssh_new_read(
     retval = retkey;
     retkey = NULL;                     /* prevent the free */
 
-    error:
+  error:
     if (retkey) {
         sfree(retkey->comment);
         if (retkey->key)
@@ -1499,7 +1492,7 @@ static ssh2_userkey *openssh_new_read(
 }
 
 static bool openssh_new_write(
-    const Filename *filename, ssh2_userkey *key, const char *passphrase)
+    const Filename *filename, ssh2_userkey *ukey, const char *passphrase)
 {
     strbuf *pubblob, *privblob, *cblob;
     int padvalue;
@@ -1509,13 +1502,17 @@ static bool openssh_new_write(
     const int bcrypt_rounds = 16;
     FILE *fp;
 
+    /* OpenSSH's private key files never contain a certificate, so
+     * revert to the underlying base key if necessary */
+    ssh_key *key = ssh_key_base_key(ukey->key);
+
     /*
      * Fetch the key blobs and find out the lengths of things.
      */
     pubblob = strbuf_new();
-    ssh_key_public_blob(key->key, BinarySink_UPCAST(pubblob));
+    ssh_key_public_blob(key, BinarySink_UPCAST(pubblob));
     privblob = strbuf_new_nm();
-    ssh_key_openssh_blob(key->key, BinarySink_UPCAST(privblob));
+    ssh_key_openssh_blob(key, BinarySink_UPCAST(privblob));
 
     /*
      * Construct the cleartext version of the blob.
@@ -1562,11 +1559,11 @@ static bool openssh_new_write(
 
         /* Private key. The main private blob goes inline, with no string
          * wrapper. */
-        put_stringz(cpblob, ssh_key_ssh_id(key->key));
+        put_stringz(cpblob, ssh_key_ssh_id(key));
         put_data(cpblob, privblob->s, privblob->len);
 
         /* Comment. */
-        put_stringz(cpblob, key->comment);
+        put_stringz(cpblob, ukey->comment);
 
         /* Pad out the encrypted section. */
         padvalue = 1;
@@ -1606,12 +1603,12 @@ static bool openssh_new_write(
     if (!fp)
         goto error;
     fputs("-----BEGIN OPENSSH PRIVATE KEY-----\n", fp);
-    base64_encode(fp, cblob->u, cblob->len, 64);
+    base64_encode_fp(fp, ptrlen_from_strbuf(cblob), 64);
     fputs("-----END OPENSSH PRIVATE KEY-----\n", fp);
     fclose(fp);
     ret = true;
 
-    error:
+  error:
     if (cblob)
         strbuf_free(cblob);
     if (privblob)
@@ -1633,11 +1630,12 @@ static bool openssh_auto_write(
      * assume that anything not in that fixed list is newer, and hence
      * will use the new format.
      */
-    if (ssh_key_alg(key->key) == &ssh_dsa ||
-        ssh_key_alg(key->key) == &ssh_rsa ||
-        ssh_key_alg(key->key) == &ssh_ecdsa_nistp256 ||
-        ssh_key_alg(key->key) == &ssh_ecdsa_nistp384 ||
-        ssh_key_alg(key->key) == &ssh_ecdsa_nistp521)
+    const ssh_keyalg *alg = ssh_key_alg(ssh_key_base_key(key->key));
+    if (alg == &ssh_dsa ||
+        alg == &ssh_rsa ||
+        alg == &ssh_ecdsa_nistp256 ||
+        alg == &ssh_ecdsa_nistp384 ||
+        alg == &ssh_ecdsa_nistp521)
         return openssh_pem_write(filename, key, passphrase);
     else
         return openssh_new_write(filename, key, passphrase);
@@ -1845,7 +1843,7 @@ static struct sshcom_key *load_sshcom_key(BinarySource *src,
     if (errmsg_p) *errmsg_p = NULL;
     return ret;
 
-    error:
+  error:
     if (line) {
         smemclr(line, strlen(line));
         sfree(line);
@@ -1883,7 +1881,7 @@ static bool sshcom_encrypted(BinarySource *filesrc, char **comment)
     if (!ptrlen_eq_string(str, "none"))
         answer = true;
 
-    done:
+  done:
     if (key) {
         *comment = dupstr(key->comment);
         strbuf_free(key->keyblob);
@@ -1895,7 +1893,7 @@ static bool sshcom_encrypted(BinarySource *filesrc, char **comment)
     return answer;
 }
 
-void BinarySink_put_mp_sshcom_from_string(BinarySink *bs, ptrlen str)
+static void BinarySink_put_mp_sshcom_from_string(BinarySink *bs, ptrlen str)
 {
     const unsigned char *bytes = (const unsigned char *)str.ptr;
     size_t nbytes = str.len;
@@ -1979,7 +1977,7 @@ static ssh2_userkey *sshcom_read(
         !memcmp(str.ptr, prefix_rsa, sizeof(prefix_rsa) - 1)) {
         type = RSA;
     } else if (str.len > sizeof(prefix_dsa) - 1 &&
-        !memcmp(str.ptr, prefix_dsa, sizeof(prefix_dsa) - 1)) {
+               !memcmp(str.ptr, prefix_dsa, sizeof(prefix_dsa) - 1)) {
         type = DSA;
     } else {
         errmsg = "key is of unknown type";
@@ -2134,7 +2132,7 @@ static ssh2_userkey *sshcom_read(
     errmsg = NULL; /* no error */
     ret = retkey;
 
-    error:
+  error:
     if (blob) {
         strbuf_free(blob);
     }
@@ -2308,12 +2306,12 @@ static bool sshcom_write(
         }
         fprintf(fp, "%s\"\n", c);
     }
-    base64_encode(fp, outblob->u, outblob->len, 70);
+    base64_encode_fp(fp, ptrlen_from_strbuf(outblob), 70);
     fputs("---- END SSH2 ENCRYPTED PRIVATE KEY ----\n", fp);
     fclose(fp);
     ret = true;
 
-    error:
+  error:
     if (outblob)
         strbuf_free(outblob);
     if (privblob)

+ 1 - 1
source/putty/marshal.h

@@ -316,7 +316,7 @@ static inline void BinarySource_INIT__(BinarySource *src, ptrlen data)
 
 #define get_err(src) (BinarySource_UPCAST(src)->err)
 #define get_avail(src) (BinarySource_UPCAST(src)->len - \
-                       BinarySource_UPCAST(src)->pos)
+                        BinarySource_UPCAST(src)->pos)
 #define get_ptr(src)                                                    \
     ((const void *)(                                                    \
         (const unsigned char *)(BinarySource_UPCAST(src)->data) +       \

+ 56 - 5
source/putty/misc.h

@@ -52,6 +52,10 @@ struct strbuf {
 strbuf *strbuf_new(void);
 strbuf *strbuf_new_nm(void);
 
+/* Helpers to allocate a strbuf containing an existing string */
+strbuf *strbuf_dup(ptrlen string);
+strbuf *strbuf_dup_nm(ptrlen string);
+
 void strbuf_free(strbuf *buf);
 void *strbuf_append(strbuf *buf, size_t len);
 void strbuf_shrink_to(strbuf *buf, size_t new_len);
@@ -68,9 +72,9 @@ void strbuf_finalise_agent_query(strbuf *buf);
 wchar_t *dup_mb_to_wc_c(int codepage, int flags, const char *string, int len);
 wchar_t *dup_mb_to_wc(int codepage, int flags, const char *string);
 char *dup_wc_to_mb_c(int codepage, int flags, const wchar_t *string, int len,
-                     const char *defchr, struct unicode_data *ucsdata);
+                     const char *defchr);
 char *dup_wc_to_mb(int codepage, int flags, const wchar_t *string,
-                   const char *defchr, struct unicode_data *ucsdata);
+                   const char *defchr);
 
 static inline int toint(unsigned u)
 {
@@ -104,6 +108,20 @@ bool strendswith(const char *s, const char *t);
 
 void base64_encode_atom(const unsigned char *data, int n, char *out);
 int base64_decode_atom(const char *atom, unsigned char *out);
+void base64_decode_bs(BinarySink *bs, ptrlen data);
+void base64_decode_fp(FILE *fp, ptrlen data);
+strbuf *base64_decode_sb(ptrlen data);
+void base64_encode_bs(BinarySink *bs, ptrlen data, int cpl);
+void base64_encode_fp(FILE *fp, ptrlen data, int cpl);
+strbuf *base64_encode_sb(ptrlen data, int cpl);
+bool base64_valid(ptrlen data);
+
+void percent_encode_bs(BinarySink *bs, ptrlen data, const char *badchars);
+void percent_encode_fp(FILE *fp, ptrlen data, const char *badchars);
+strbuf *percent_encode_sb(ptrlen data, const char *badchars);
+void percent_decode_bs(BinarySink *bs, ptrlen data);
+void percent_decode_fp(FILE *fp, ptrlen data);
+strbuf *percent_decode_sb(ptrlen data);
 
 struct bufchain_granule;
 struct bufchain_tag {
@@ -158,6 +176,21 @@ static inline ptrlen make_ptrlen(const void *ptr, size_t len)
     return pl;
 }
 
+static inline const void *ptrlen_end(ptrlen pl)
+{
+    return (const char *)pl.ptr + pl.len;
+}
+
+static inline ptrlen make_ptrlen_startend(const void *startv, const void *endv)
+{
+    const char *start = (const char *)startv, *end = (const char *)endv;
+    assert(end >= start);
+    ptrlen pl;
+    pl.ptr = start;
+    pl.len = end - start;
+    return pl;
+}
+
 static inline ptrlen ptrlen_from_asciz(const char *str)
 {
     return make_ptrlen(str, strlen(str));
@@ -179,6 +212,8 @@ int ptrlen_strcmp(ptrlen pl1, ptrlen pl2);
 bool ptrlen_startswith(ptrlen whole, ptrlen prefix, ptrlen *tail);
 bool ptrlen_endswith(ptrlen whole, ptrlen suffix, ptrlen *tail);
 ptrlen ptrlen_get_word(ptrlen *input, const char *separators);
+bool ptrlen_contains(ptrlen input, const char *characters);
+bool ptrlen_contains_only(ptrlen input, const char *characters);
 char *mkstr(ptrlen pl);
 int string_length_for_printf(size_t);
 /* Derive two printf arguments from a ptrlen, suitable for "%.*s" */
@@ -197,6 +232,8 @@ int string_length_for_printf(size_t);
 /* Make a ptrlen out of a constant byte array. */
 #define PTRLEN_FROM_CONST_BYTES(a) make_ptrlen(a, sizeof(a))
 
+void wordwrap(BinarySink *bs, ptrlen input, size_t maxwid);
+
 /* Wipe sensitive data out of memory that's about to be freed. Simpler
  * than memset because we don't need the fill char parameter; also
  * attempts (by fiddly use of volatile) to inhibit the compiler from
@@ -207,9 +244,9 @@ void smemclr(void *b, size_t len);
 /* Compare two fixed-length chunks of memory for equality, without
  * data-dependent control flow (so an attacker with a very accurate
  * stopwatch can't try to guess where the first mismatching byte was).
- * Returns false for mismatch or true for equality (unlike memcmp),
- * hinted at by the 'eq' in the name. */
-bool smemeq(const void *av, const void *bv, size_t len);
+ * Returns 0 for mismatch or 1 for equality (unlike memcmp), hinted at
+ * by the 'eq' in the name. */
+unsigned smemeq(const void *av, const void *bv, size_t len);
 
 /* Encode a single UTF-8 character. Assumes that illegal characters
  * (such as things in the surrogate range, or > 0x10FFFF) have already
@@ -472,4 +509,18 @@ static inline ptrlen ptrlen_from_lf(LoadedFile *lf)
  * is made to handle difficult overlap cases. */
 void memxor(uint8_t *out, const uint8_t *in1, const uint8_t *in2, size_t size);
 
+/* Boolean expressions used in OpenSSH certificate configuration */
+bool cert_expr_valid(const char *expression,
+                     char **error_msg, ptrlen *error_loc);
+bool cert_expr_match_str(const char *expression,
+                         const char *hostname, unsigned port);
+/* Build a certificate expression out of hostname wildcards. Required
+ * to handle legacy configuration from early in development, when
+ * multiple wildcards were stored separately in config, implicitly
+ * ORed together. */
+CertExprBuilder *cert_expr_builder_new(void);
+void cert_expr_builder_free(CertExprBuilder *eb);
+void cert_expr_builder_add(CertExprBuilder *eb, const char *wildcard);
+char *cert_expr_expression(CertExprBuilder *eb);
+
 #endif

+ 7 - 0
source/putty/mpint.h

@@ -42,6 +42,13 @@ mp_int *mp_new(size_t maxbits);
 void mp_free(mp_int *);
 void mp_clear(mp_int *x);
 
+/*
+ * Resize the physical size of existing mp_int, e.g. so that you have
+ * room to transform it in place to a larger value. Destroys the old
+ * mp_int in the process.
+ */
+mp_int *mp_resize(mp_int *, size_t newmaxbits);
+
 /*
  * Create mp_ints from various sources: little- and big-endian binary
  * data, an ordinary C unsigned integer type, a decimal or hex string

+ 4 - 0
source/putty/network.h

@@ -397,8 +397,10 @@ void backend_socket_log(Seat *seat, LogContext *logctx,
 typedef struct ProxyStderrBuf {
     char buf[8192];
     size_t size;
+    const char *prefix;                /* must be statically allocated */
 } ProxyStderrBuf;
 void psb_init(ProxyStderrBuf *psb);
+void psb_set_prefix(ProxyStderrBuf *psb, const char *prefix);
 void log_proxy_stderr(
     Plug *plug, ProxyStderrBuf *psb, const void *vdata, size_t len);
 
@@ -429,4 +431,6 @@ struct DeferredSocketOpenerVtable {
 static inline void deferred_socket_opener_free(DeferredSocketOpener *dso)
 { dso->vt->free(dso); }
 
+DeferredSocketOpener *null_deferred_socket_opener(void);
+
 #endif

+ 2 - 1
source/putty/proxy/cproxy.c

@@ -26,7 +26,8 @@ strbuf *chap_response(ptrlen challenge, ptrlen password)
     return sb;
 }
 
-void BinarySink_put_hex_data(BinarySink *bs, const void *vptr, size_t len)
+static void BinarySink_put_hex_data(BinarySink *bs, const void *vptr,
+                                    size_t len)
 {
     const unsigned char *p = (const unsigned char *)vptr;
     const char *hexdigits = "0123456789abcdef";

+ 11 - 9
source/putty/proxy/proxy.c

@@ -35,7 +35,7 @@ static void proxy_negotiator_cleanup(ProxySocket *ps)
  * Call this when proxy negotiation is complete, so that this
  * socket can begin working normally.
  */
-void proxy_activate(ProxySocket *ps)
+static void proxy_activate(ProxySocket *ps)
 {
     size_t output_before, output_after;
 
@@ -178,7 +178,7 @@ static void sk_proxy_set_frozen (Socket *s, bool is_frozen)
     sk_set_frozen(ps->sub_socket, is_frozen);
 }
 
-static const char * sk_proxy_socket_error (Socket *s)
+static const char *sk_proxy_socket_error (Socket *s)
 {
     ProxySocket *ps = container_of(s, ProxySocket, sock);
     if (ps->error != NULL || ps->sub_socket == NULL) {
@@ -393,8 +393,8 @@ static char *dns_log_msg(const char *host, int addressfamily,
 }
 
 SockAddr *name_lookup(const char *host, int port, char **canonicalname,
-                     Conf *conf, int addressfamily, LogContext *logctx,
-                     const char *reason)
+                      Conf *conf, int addressfamily, LogContext *logctx,
+                      const char *reason)
 {
     if (conf_get_int(conf, CONF_proxy_type) != PROXY_NONE &&
         do_proxy_dns(conf) &&
@@ -506,7 +506,9 @@ Socket *new_connection(SockAddr *addr, const char *hostname,
         char *proxy_canonical_name;
         Socket *sret;
 
-        if (type == PROXY_SSH &&
+        if ((type == PROXY_SSH_TCPIP ||
+             type == PROXY_SSH_EXEC ||
+             type == PROXY_SSH_SUBSYSTEM) &&
             (sret = sshproxy_new_connection(addr, hostname, port, privport,
                                             oobinline, nodelay, keepalive,
                                             plug, conf, itr)) != NULL)
@@ -579,10 +581,10 @@ Socket *new_connection(SockAddr *addr, const char *hostname,
 
         {
             char *logmsg = dupprintf("Will use %s proxy at %s:%d to connect"
-                                      " to %s:%d", vt->type,
-                                      conf_get_str(conf, CONF_proxy_host),
-                                      conf_get_int(conf, CONF_proxy_port),
-                                      hostname, port);
+                                     " to %s:%d", vt->type,
+                                     conf_get_str(conf, CONF_proxy_host),
+                                     conf_get_int(conf, CONF_proxy_port),
+                                     hostname, port);
             plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, logmsg, 0);
             sfree(logmsg);
         }

+ 24 - 24
source/putty/proxy/telnet.c

@@ -85,31 +85,31 @@ char *format_telnet_command(SockAddr *addr, int port, Conf *conf,
                 int i = 0;
 
                 for (;;) {
-                  eo++;
-                  if (fmt[eo] >= '0' && fmt[eo] <= '9')
-                      v += fmt[eo] - '0';
-                  else if (fmt[eo] >= 'a' && fmt[eo] <= 'f')
-                      v += fmt[eo] - 'a' + 10;
-                  else if (fmt[eo] >= 'A' && fmt[eo] <= 'F')
-                      v += fmt[eo] - 'A' + 10;
-                  else {
-                    /* non hex character, so we abort and just
-                     * send the whole thing unescaped (including \x)
-                     */
-                    put_byte(buf, '\\');
-                    eo = so + 1;
-                    break;
-                  }
-
-                  /* we only extract two hex characters */
-                  if (i == 1) {
-                    put_byte(buf, v);
                     eo++;
-                    break;
-                  }
-
-                  i++;
-                  v <<= 4;
+                    if (fmt[eo] >= '0' && fmt[eo] <= '9')
+                        v += fmt[eo] - '0';
+                    else if (fmt[eo] >= 'a' && fmt[eo] <= 'f')
+                        v += fmt[eo] - 'a' + 10;
+                    else if (fmt[eo] >= 'A' && fmt[eo] <= 'F')
+                        v += fmt[eo] - 'A' + 10;
+                    else {
+                        /* non hex character, so we abort and just
+                         * send the whole thing unescaped (including \x)
+                         */
+                        put_byte(buf, '\\');
+                        eo = so + 1;
+                        break;
+                    }
+
+                    /* we only extract two hex characters */
+                    if (i == 1) {
+                        put_byte(buf, v);
+                        eo++;
+                        break;
+                    }
+
+                    i++;
+                    v <<= 4;
                 }
                 break;
               }

+ 137 - 23
source/putty/putty.h

@@ -266,7 +266,6 @@ struct sesslist {
 };
 
 struct unicode_data {
-    char **uni_tbl;
     bool dbcs_screenfont;
     int font_codepage;
     int line_codepage;
@@ -423,9 +422,14 @@ enum {
     KEX_WARN,
     KEX_DHGROUP1,
     KEX_DHGROUP14,
+    KEX_DHGROUP15,
+    KEX_DHGROUP16,
+    KEX_DHGROUP17,
+    KEX_DHGROUP18,
     KEX_DHGEX,
     KEX_RSA,
     KEX_ECDH,
+    KEX_NTRU_HYBRID,
     KEX_MAX
 };
 
@@ -453,6 +457,7 @@ enum {
     CIPHER_DES,
     CIPHER_ARCFOUR,
     CIPHER_CHACHA20,
+    CIPHER_AESGCM,
     CIPHER_MAX                         /* no. ciphers (inc warn) */
 };
 
@@ -474,7 +479,8 @@ enum {
      * Proxy types.
      */
     PROXY_NONE, PROXY_SOCKS4, PROXY_SOCKS5,
-    PROXY_HTTP, PROXY_TELNET, PROXY_CMD, PROXY_SSH,
+    PROXY_HTTP, PROXY_TELNET, PROXY_CMD, PROXY_SSH_TCPIP,
+    PROXY_SSH_EXEC, PROXY_SSH_SUBSYSTEM,
     PROXY_FUZZ
 };
 
@@ -1084,6 +1090,24 @@ typedef enum SeatOutputType {
     SEAT_OUTPUT_STDOUT, SEAT_OUTPUT_STDERR
 } SeatOutputType;
 
+typedef enum SeatDialogTextType {
+    SDT_PARA, SDT_DISPLAY, SDT_SCARY_HEADING,
+    SDT_TITLE, SDT_PROMPT, SDT_BATCH_ABORT,
+    SDT_MORE_INFO_KEY, SDT_MORE_INFO_VALUE_SHORT, SDT_MORE_INFO_VALUE_BLOB
+} SeatDialogTextType;
+struct SeatDialogTextItem {
+    SeatDialogTextType type;
+    char *text;
+};
+struct SeatDialogText {
+    size_t nitems, itemsize;
+    SeatDialogTextItem *items;
+};
+SeatDialogText *seat_dialog_text_new(void);
+void seat_dialog_text_free(SeatDialogText *sdt);
+PRINTF_LIKE(3, 4) void seat_dialog_text_append(
+    SeatDialogText *sdt, SeatDialogTextType type, const char *fmt, ...);
+
 /*
  * Data type 'Seat', which is an API intended to contain essentially
  * everything that a back end might need to talk to its client for:
@@ -1258,9 +1282,8 @@ struct SeatVtable {
      */
     SeatPromptResult (*confirm_ssh_host_key)(
         Seat *seat, const char *host, int port, const char *keytype,
-        char *keystr, const char *keydisp, char **key_fingerprints,
-        bool mismatch, void (*callback)(void *ctx, SeatPromptResult result),
-        void *ctx);
+        char *keystr, SeatDialogText *text, HelpCtx helpctx,
+        void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 
     /*
      * Check with the seat whether it's OK to use a cryptographic
@@ -1287,6 +1310,13 @@ struct SeatVtable {
         Seat *seat, const char *algname, const char *betteralgs,
         void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 
+    /*
+     * Some snippets of text describing the UI actions in host key
+     * prompts / dialog boxes, to be used in ssh/common.c when it
+     * assembles the full text of those prompts.
+     */
+    const SeatDialogPromptDescriptions *(*prompt_descriptions)(Seat *seat);
+
     /*
      * Indicates whether the seat is expecting to interact with the
      * user in the UTF-8 character set. (Affects e.g. visual erase
@@ -1408,10 +1438,10 @@ static inline void seat_set_busy_status(Seat *seat, BusyStatus status)
 { seat->vt->set_busy_status(seat, status); }
 static inline SeatPromptResult seat_confirm_ssh_host_key(
     InteractionReadySeat iseat, const char *h, int p, const char *ktyp,
-    char *kstr, const char *kdsp, char **fps, bool mis,
+    char *kstr, SeatDialogText *text, HelpCtx helpctx,
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
 { return iseat.seat->vt->confirm_ssh_host_key(
-        iseat.seat, h, p, ktyp, kstr, kdsp, fps, mis, cb, ctx); }
+        iseat.seat, h, p, ktyp, kstr, text, helpctx, cb, ctx); }
 static inline SeatPromptResult seat_confirm_weak_crypto_primitive(
     InteractionReadySeat iseat, const char *atyp, const char *aname,
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
@@ -1422,6 +1452,9 @@ static inline SeatPromptResult seat_confirm_weak_cached_hostkey(
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
 { return iseat.seat->vt->confirm_weak_cached_hostkey(
         iseat.seat, aname, better, cb, ctx); }
+static inline const SeatDialogPromptDescriptions *seat_prompt_descriptions(
+    Seat *seat)
+{ return seat->vt->prompt_descriptions(seat); }
 static inline bool seat_is_utf8(Seat *seat)
 { return seat->vt->is_utf8(seat); }
 static inline void seat_echoedit_update(Seat *seat, bool ec, bool ed)
@@ -1467,6 +1500,12 @@ static inline size_t seat_stderr_pl(Seat *seat, ptrlen data)
 static inline size_t seat_banner_pl(InteractionReadySeat iseat, ptrlen data)
 { return iseat.seat->vt->banner(iseat.seat, data.ptr, data.len); }
 
+struct SeatDialogPromptDescriptions {
+    const char *hk_accept_action;
+    const char *hk_connect_once_action;
+    const char *hk_cancel_action, *hk_cancel_action_Participle;
+};
+
 /* In the utils subdir: print a message to the Seat which can't be
  * spoofed by server-supplied auth-time output such as SSH banners */
 void seat_antispoof_msg(InteractionReadySeat iseat, const char *msg);
@@ -1494,7 +1533,7 @@ char *nullseat_get_ttymode(Seat *seat, const char *mode);
 void nullseat_set_busy_status(Seat *seat, BusyStatus status);
 SeatPromptResult nullseat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
-    char *keystr, const char *keydisp, char **key_fingerprints, bool mismatch,
+    char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 SeatPromptResult nullseat_confirm_weak_crypto_primitive(
     Seat *seat, const char *algtype, const char *algname,
@@ -1502,6 +1541,7 @@ SeatPromptResult nullseat_confirm_weak_crypto_primitive(
 SeatPromptResult nullseat_confirm_weak_cached_hostkey(
     Seat *seat, const char *algname, const char *betteralgs,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat);
 bool nullseat_is_never_utf8(Seat *seat);
 bool nullseat_is_always_utf8(Seat *seat);
 void nullseat_echoedit_update(Seat *seat, bool echoing, bool editing);
@@ -1509,7 +1549,7 @@ const char *nullseat_get_x_display(Seat *seat);
 bool nullseat_get_windowid(Seat *seat, long *id_out);
 bool nullseat_get_window_pixel_size(Seat *seat, int *width, int *height);
 StripCtrlChars *nullseat_stripctrl_new(
-        Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
+    Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
 void nullseat_set_trust_status(Seat *seat, bool trusted);
 bool nullseat_can_set_trust_status_yes(Seat *seat);
 bool nullseat_can_set_trust_status_no(Seat *seat);
@@ -1529,7 +1569,7 @@ bool nullseat_get_cursor_position(Seat *seat, int *x, int *y);
 void console_connection_fatal(Seat *seat, const char *message);
 SeatPromptResult console_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
-    char *keystr, const char *keydisp, char **key_fingerprints, bool mismatch,
+    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,
@@ -1538,10 +1578,11 @@ SeatPromptResult console_confirm_weak_cached_hostkey(
     Seat *seat, const char *algname, const char *betteralgs,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
 StripCtrlChars *console_stripctrl_new(
-        Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
+    Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
 void console_set_trust_status(Seat *seat, bool trusted);
 bool console_can_set_trust_status(Seat *seat);
 bool console_has_mixed_input_stream(Seat *seat);
+const SeatDialogPromptDescriptions *console_prompt_descriptions(Seat *seat);
 
 /*
  * Other centralised seat functions.
@@ -1634,6 +1675,17 @@ struct TermWinVtable {
 
     void (*refresh)(TermWin *);
 
+    /* request_resize asks the front end if the terminal can please be
+     * resized to (w,h) in characters. The front end MAY call
+     * term_size() in response to tell the terminal its new size
+     * (which MAY be the requested size, or some other size if the
+     * requested one can't be achieved). The front end MAY also not
+     * call term_size() at all. But the front end MUST reply to this
+     * request by calling term_resize_request_completed(), after the
+     * responding resize event has taken place (if any).
+     *
+     * The calls to term_size and term_resize_request_completed may be
+     * synchronous callbacks from within the call to request_resize(). */
     void (*request_resize)(TermWin *, int w, int h);
 
     void (*set_title)(TermWin *, const char *title, int codepage);
@@ -1778,6 +1830,8 @@ NORETURN void cleanup_exit(int);
     X(BOOL, NONE, change_username) /* allow username switching in SSH-2 */ \
     X(INT, INT, ssh_cipherlist) \
     X(FILENAME, NONE, keyfile) \
+    X(FILENAME, NONE, detached_cert) \
+    X(STR, NONE, auth_plugin) \
     /* \
      * Which SSH protocol to use. \
      * For historical reasons, the current legal values for CONF_sshprot \
@@ -1969,6 +2023,7 @@ NORETURN void cleanup_exit(int);
     X(INT, NONE, sshbug_winadj) \
     X(INT, NONE, sshbug_chanreq) \
     X(INT, NONE, sshbug_dropstart) \
+    X(INT, NONE, sshbug_filter_kexinit) \
     /*                                                                \
      * ssh_simple means that we promise never to open any channel     \
      * other than the main one, which means it can safely use a very  \
@@ -2130,6 +2185,7 @@ FontSpec *platform_default_fontspec(const char *name);
 Terminal *term_init(Conf *, struct unicode_data *, TermWin *);
 void term_free(Terminal *);
 void term_size(Terminal *, int, int, int);
+void term_resize_request_completed(Terminal *);
 void term_paint(Terminal *, int, int, int, int, bool);
 void term_scroll(Terminal *, int, int);
 void term_scroll_to_selection(Terminal *, int);
@@ -2160,7 +2216,7 @@ char *term_get_ttymode(Terminal *term, const char *mode);
 SeatPromptResult term_get_userpass_input(Terminal *term, prompts_t *p);
 void term_set_trust_status(Terminal *term, bool trusted);
 void term_keyinput(Terminal *, int codepage, const void *buf, int len);
-void term_keyinputw(Terminal *, const wchar_t * widebuf, int len);
+void term_keyinputw(Terminal *, const wchar_t *widebuf, int len);
 void term_get_cursor_position(Terminal *term, int *x, int *y);
 void term_setup_window_titles(Terminal *term, const char *title_hostname);
 void term_notify_minimised(Terminal *term, bool minimised);
@@ -2176,7 +2232,9 @@ int format_arrow_key(char *buf, Terminal *term, int xkey,
                      bool shift, bool ctrl, bool alt, bool *consumed_alt);
 int format_function_key(char *buf, Terminal *term, int key_number,
                         bool shift, bool ctrl, bool alt, bool *consumed_alt);
-int format_small_keypad_key(char *buf, Terminal *term, SmallKeypadKey key);
+int format_small_keypad_key(char *buf, Terminal *term, SmallKeypadKey key,
+                            bool shift, bool ctrl, bool alt,
+                            bool *consumed_alt);
 int format_numeric_keypad_key(char *buf, Terminal *term, char key,
                               bool shift, bool ctrl);
 
@@ -2421,14 +2479,13 @@ bool is_dbcs_leadbyte(int codepage, char byte);
 int mb_to_wc(int codepage, int flags, const char *mbstr, int mblen,
              wchar_t *wcstr, int wclen);
 int wc_to_mb(int codepage, int flags, const wchar_t *wcstr, int wclen,
-             char *mbstr, int mblen, const char *defchr,
-             struct unicode_data *ucsdata);
+             char *mbstr, int mblen, const char *defchr);
 wchar_t xlat_uskbd2cyrllic(int ch);
 int check_compose(int first, int second);
-int decode_codepage(char *cp_name);
+int decode_codepage(const char *cp_name);
 const char *cp_enumerate (int index);
 const char *cp_name(int codepage);
-void get_unitab(int codepage, wchar_t * unitab, int ftype);
+void get_unitab(int codepage, wchar_t *unitab, int ftype);
 
 /*
  * Exports from wcwidth.c
@@ -2572,22 +2629,69 @@ void cmdline_error(const char *, ...) PRINTF_LIKE(1, 2);
  * Exports from config.c.
  */
 struct controlbox;
-union control;
-void conf_radiobutton_handler(union control *ctrl, dlgparam *dlg,
+void conf_radiobutton_handler(dlgcontrol *ctrl, dlgparam *dlg,
                               void *data, int event);
 #define CHECKBOX_INVERT (1<<30)
-void conf_checkbox_handler(union control *ctrl, dlgparam *dlg,
+void conf_checkbox_handler(dlgcontrol *ctrl, dlgparam *dlg,
                            void *data, int event);
-void conf_editbox_handler(union control *ctrl, dlgparam *dlg,
+void conf_editbox_handler(dlgcontrol *ctrl, dlgparam *dlg,
                           void *data, int event);
-void conf_filesel_handler(union control *ctrl, dlgparam *dlg,
+void conf_filesel_handler(dlgcontrol *ctrl, dlgparam *dlg,
                           void *data, int event);
-void conf_fontsel_handler(union control *ctrl, dlgparam *dlg,
+void conf_fontsel_handler(dlgcontrol *ctrl, dlgparam *dlg,
                           void *data, int event);
 
+struct conf_editbox_handler_type {
+    /* Structure passed as context2 to conf_editbox_handler */
+    enum { EDIT_STR, EDIT_INT, EDIT_FIXEDPOINT } type;
+    union {
+        /*
+         * EDIT_STR means the edit box is connected to a string
+         * field in Conf. No further parameters needed.
+         */
+
+        /*
+         * EDIT_INT means the edit box is connected to an int field in
+         * Conf, and the input string is interpreted as decimal. No
+         * further parameters needed. (But we could add one here later
+         * if for some reason we wanted int fields in hex.)
+         */
+
+        /*
+         * EDIT_FIXEDPOINT means the edit box is connected to an int
+         * field in Conf, but the input string is interpreted as
+         * _floating point_, and converted to/from the output int by
+         * means of a fixed denominator. That is,
+         *
+         *   (floating value in edit box) * denominator = value in Conf
+         */
+        struct {
+            double denominator;
+        };
+    };
+};
+
+extern const struct conf_editbox_handler_type conf_editbox_str;
+extern const struct conf_editbox_handler_type conf_editbox_int;
+#define ED_STR CP(&conf_editbox_str)
+#define ED_INT CP(&conf_editbox_int)
+
 void setup_config_box(struct controlbox *b, bool midsession,
                       int protocol, int protcfginfo);
 
+void setup_ca_config_box(struct controlbox *b);
+
+/* Platforms provide this to be called from config.c */
+void show_ca_config_box(dlgparam *dlg);
+extern const bool has_ca_config_box; /* false if, e.g., we're PuTTYtel */
+
+/* Visible outside config.c so that platforms can use it to recognise
+ * the proxy type control */
+void proxy_type_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                        void *data, int event);
+/* And then they'll set this flag in its generic.context.i */
+#define PROXY_UI_FLAG_LOCAL 1 /* has a local proxy */
+
 /*
  * Exports from bidi.c.
  */
@@ -2790,6 +2894,16 @@ typedef void (*toplevel_callback_notify_fn_t)(void *ctx);
 void request_callback_notifications(toplevel_callback_notify_fn_t notify,
                                     void *ctx);
 
+/*
+ * Facility provided by the platform to spawn a parallel subprocess
+ * and present its stdio via a Socket.
+ *
+ * 'prefix' indicates the prefix that should appear on messages passed
+ * to plug_log to provide stderr output from the process.
+ */
+Socket *platform_start_subprocess(const char *cmd, Plug *plug,
+                                  const char *prefix);
+
 /*
  * Define no-op macros for the jump list functions, on platforms that
  * don't support them. (This is a bit of a hack, and it'd be nicer to

+ 21 - 2
source/putty/settings.c

@@ -17,6 +17,7 @@
 static const struct keyvalwhere ciphernames[] = {
     { "aes",        CIPHER_AES,             -1, -1 },
     { "chacha20",   CIPHER_CHACHA20,        CIPHER_AES, +1 },
+    { "aesgcm",     CIPHER_AESGCM,          CIPHER_CHACHA20, +1 },
     { "3des",       CIPHER_3DES,            -1, -1 },
     { "WARN",       CIPHER_WARN,            -1, -1 },
     { "des",        CIPHER_DES,             -1, -1 },
@@ -28,12 +29,24 @@ static const struct keyvalwhere ciphernames[] = {
  * compatibility warts in load_open_settings(), and should be kept
  * in sync with those. */
 static const struct keyvalwhere kexnames[] = {
+    { "ntru-curve25519",    KEX_NTRU_HYBRID, -1, +1 },
     { "ecdh",               KEX_ECDH,       -1, +1 },
     /* This name is misleading: it covers both SHA-256 and SHA-1 variants */
     { "dh-gex-sha1",        KEX_DHGEX,      -1, -1 },
+    /* Again, this covers both SHA-256 and SHA-1, despite the name: */
     { "dh-group14-sha1",    KEX_DHGROUP14,  -1, -1 },
+    /* This one really is only SHA-1, though: */
     { "dh-group1-sha1",     KEX_DHGROUP1,   KEX_WARN, +1 },
     { "rsa",                KEX_RSA,        KEX_WARN, -1 },
+    /* Larger fixed DH groups: prefer the larger 15 and 16 over 14,
+     * but by default the even larger 17 and 18 go below 16.
+     * Rationale: diminishing returns of improving the DH strength are
+     * outweighed by increased CPU cost. Group 18 is painful on a slow
+     * machine. Users can override if they need to. */
+    { "dh-group15-sha512",  KEX_DHGROUP15,  KEX_DHGROUP14, -1 },
+    { "dh-group16-sha512",  KEX_DHGROUP16,  KEX_DHGROUP15, -1 },
+    { "dh-group17-sha512",  KEX_DHGROUP17,  KEX_DHGROUP16, +1 },
+    { "dh-group18-sha512",  KEX_DHGROUP18,  KEX_DHGROUP17, +1 },
     { "WARN",               KEX_WARN,       -1, -1 }
 };
 
@@ -624,6 +637,8 @@ void save_open_settings(settings_w *sesskey, Conf *conf)
     write_setting_s(sesskey, "LogHost", conf_get_str(conf, CONF_loghost));
     write_setting_b(sesskey, "SSH2DES", conf_get_bool(conf, CONF_ssh2_des_cbc));
     write_setting_filename(sesskey, "PublicKeyFile", conf_get_filename(conf, CONF_keyfile));
+    write_setting_filename(sesskey, "DetachedCertificate", conf_get_filename(conf, CONF_detached_cert));
+    write_setting_s(sesskey, "AuthPlugin", conf_get_str(conf, CONF_auth_plugin));
     write_setting_s(sesskey, "RemoteCommand", conf_get_str(conf, CONF_remote_cmd));
     write_setting_b(sesskey, "RFCEnviron", conf_get_bool(conf, CONF_rfc_environ));
     write_setting_b(sesskey, "PassiveTelnet", conf_get_bool(conf, CONF_passive_telnet));
@@ -771,6 +786,7 @@ void save_open_settings(settings_w *sesskey, Conf *conf)
     write_setting_i(sesskey, "BugWinadj", 2-conf_get_int(conf, CONF_sshbug_winadj));
     write_setting_i(sesskey, "BugChanReq", 2-conf_get_int(conf, CONF_sshbug_chanreq));
     write_setting_i(sesskey, "BugDropStart", 2-conf_get_int(conf, CONF_sshbug_dropstart));
+    write_setting_i(sesskey, "BugFilterKexinit", 2-conf_get_int(conf, CONF_sshbug_filter_kexinit));
     write_setting_b(sesskey, "StampUtmp", conf_get_bool(conf, CONF_stamp_utmp));
     write_setting_b(sesskey, "LoginShell", conf_get_bool(conf, CONF_login_shell));
     write_setting_b(sesskey, "ScrollbarOnLeft", conf_get_bool(conf, CONF_scrollbar_on_left));
@@ -968,9 +984,9 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
          * a server which offered it then choked, but we never got
          * a server version string or any other reports. */
         const char *default_kexes,
-                   *normal_default = "ecdh,dh-gex-sha1,dh-group14-sha1,rsa,"
+                   *normal_default = "ecdh,dh-gex-sha1,dh-group18-sha512,dh-group17-sha512,dh-group16-sha512,dh-group15-sha512,dh-group14-sha1,rsa,"
                        "WARN,dh-group1-sha1",
-                   *bugdhgex2_default = "ecdh,dh-group14-sha1,rsa,"
+                   *bugdhgex2_default = "ecdh,dh-group18-sha512,dh-group17-sha512,dh-group16-sha512,dh-group15-sha512,dh-group14-sha1,rsa,"
                        "WARN,dh-group1-sha1,dh-gex-sha1";
         char *raw;
         i = 2 - gppi_raw(sesskey, "BugDHGEx2", 0);
@@ -1041,6 +1057,8 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
 #endif
     gppb(sesskey, "SshNoShell", false, conf, CONF_ssh_no_shell);
     gppfile(sesskey, "PublicKeyFile", conf, CONF_keyfile);
+    gppfile(sesskey, "DetachedCertificate", conf, CONF_detached_cert);
+    gpps(sesskey, "AuthPlugin", "", conf, CONF_auth_plugin);
     gpps(sesskey, "RemoteCommand", "", conf, CONF_remote_cmd);
     gppb(sesskey, "RFCEnviron", false, conf, CONF_rfc_environ);
     gppb(sesskey, "PassiveTelnet", false, conf, CONF_passive_telnet);
@@ -1249,6 +1267,7 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
     i = gppi_raw(sesskey, "BugWinadj", 0); conf_set_int(conf, CONF_sshbug_winadj, 2-i);
     i = gppi_raw(sesskey, "BugChanReq", 0); conf_set_int(conf, CONF_sshbug_chanreq, 2-i);
     i = gppi_raw(sesskey, "BugDropStart", 1); conf_set_int(conf, CONF_sshbug_dropstart, 2-i);
+    i = gppi_raw(sesskey, "BugFilterKexinit", 1); conf_set_int(conf, CONF_sshbug_filter_kexinit, 2-i);
     conf_set_bool(conf, CONF_ssh_simple, false);
     gppb(sesskey, "StampUtmp", true, conf, CONF_stamp_utmp);
     gppb(sesskey, "LoginShell", true, conf, CONF_login_shell);

+ 254 - 31
source/putty/ssh.h

@@ -515,7 +515,7 @@ struct ec_curve {
 };
 
 const ssh_keyalg *ec_alg_by_oid(int len, const void *oid,
-                                        const struct ec_curve **curve);
+                                const struct ec_curve **curve);
 const unsigned char *ec_alg_oid(const ssh_keyalg *alg, int *oidlen);
 extern const int ec_nist_curve_lengths[], n_ec_nist_curve_lengths;
 extern const int ec_ed_curve_lengths[], n_ec_ed_curve_lengths;
@@ -542,22 +542,34 @@ struct eddsa_key {
 WeierstrassPoint *ecdsa_public(mp_int *private_key, const ssh_keyalg *alg);
 EdwardsPoint *eddsa_public(mp_int *private_key, const ssh_keyalg *alg);
 
+typedef enum KeyComponentType {
+    KCT_TEXT, KCT_BINARY, KCT_MPINT
+} KeyComponentType;
+typedef struct key_component {
+    char *name;
+    KeyComponentType type;
+    union {
+        strbuf *str;                   /* used for KCT_TEXT and KCT_BINARY */
+        mp_int *mp;                    /* used for KCT_MPINT */
+    };
+} key_component;
 typedef struct key_components {
     size_t ncomponents, componentsize;
-    struct {
-        char *name;
-        bool is_mp_int;
-        union {
-            char *text;
-            mp_int *mp;
-        };
-    } *components;
+    key_component *components;
 } key_components;
 key_components *key_components_new(void);
 void key_components_add_text(key_components *kc,
                              const char *name, const char *value);
+void key_components_add_text_pl(key_components *kc,
+                                const char *name, ptrlen value);
+void key_components_add_binary(key_components *kc,
+                               const char *name, ptrlen value);
 void key_components_add_mp(key_components *kc,
                            const char *name, mp_int *value);
+void key_components_add_uint(key_components *kc,
+                             const char *name, uintmax_t value);
+void key_components_add_copy(key_components *kc,
+                             const char *name, const key_component *value);
 void key_components_free(key_components *kc);
 
 /*
@@ -585,6 +597,7 @@ bool rsa_verify(RSAKey *key);
 void rsa_ssh1_public_blob(BinarySink *bs, RSAKey *key, RsaSsh1Order order);
 int rsa_ssh1_public_blob_len(ptrlen data);
 void rsa_ssh1_private_blob_agent(BinarySink *bs, RSAKey *key);
+void duprsakey(RSAKey *dst, const RSAKey *src);
 void freersapriv(RSAKey *key);
 void freersakey(RSAKey *key);
 key_components *rsa_components(RSAKey *key);
@@ -615,21 +628,12 @@ strbuf *ssh_rsakex_encrypt(
 mp_int *ssh_rsakex_decrypt(
     RSAKey *key, const ssh_hashalg *h, ptrlen ciphertext);
 
-/*
- * SSH2 ECDH key exchange functions
- */
-const char *ssh_ecdhkex_curve_textname(const ssh_kex *kex);
-ecdh_key *ssh_ecdhkex_newkey(const ssh_kex *kex);
-void ssh_ecdhkex_freekey(ecdh_key *key);
-void ssh_ecdhkex_getpublic(ecdh_key *key, BinarySink *bs);
-mp_int *ssh_ecdhkex_getkey(ecdh_key *key, ptrlen remoteKey);
-
 /*
  * Helper function for k generation in DSA, reused in ECDSA
  */
 mp_int *dsa_gen_k(const char *id_string,
-                     mp_int *modulus, mp_int *private_key,
-                     unsigned char *digest, int digest_len);
+                  mp_int *modulus, mp_int *private_key,
+                  unsigned char *digest, int digest_len);
 
 struct ssh_cipher {
     const ssh_cipheralg *vt;
@@ -647,6 +651,9 @@ struct ssh_cipheralg {
                            unsigned long seq);
     void (*decrypt_length)(ssh_cipher *, void *blk, int len,
                            unsigned long seq);
+    /* For ciphers that update their state per logical message
+     * (typically, per unit independently MACed) */
+    void (*next_message)(ssh_cipher *);
     const char *ssh2_id;
     int blksize;
     /* real_keybits is the number of bits of entropy genuinely used by
@@ -691,9 +698,13 @@ static inline void ssh_cipher_encrypt_length(
 static inline void ssh_cipher_decrypt_length(
     ssh_cipher *c, void *blk, int len, unsigned long seq)
 { c->vt->decrypt_length(c, blk, len, seq); }
+static inline void ssh_cipher_next_message(ssh_cipher *c)
+{ c->vt->next_message(c); }
 static inline const struct ssh_cipheralg *ssh_cipher_alg(ssh_cipher *c)
 { return c->vt; }
 
+void nullcipher_next_message(ssh_cipher *);
+
 struct ssh2_ciphers {
     int nciphers;
     const ssh_cipheralg *const *list;
@@ -711,6 +722,7 @@ struct ssh2_macalg {
     void (*setkey)(ssh2_mac *, ptrlen key);
     void (*start)(ssh2_mac *);
     void (*genresult)(ssh2_mac *, unsigned char *);
+    void (*next_message)(ssh2_mac *);
     const char *(*text_name)(ssh2_mac *);
     const char *name, *etm_name;
     int len, keylen;
@@ -730,6 +742,8 @@ static inline void ssh2_mac_start(ssh2_mac *m)
 { m->vt->start(m); }
 static inline void ssh2_mac_genresult(ssh2_mac *m, unsigned char *out)
 { m->vt->genresult(m, out); }
+static inline void ssh2_mac_next_message(ssh2_mac *m)
+{ m->vt->next_message(m); }
 static inline const char *ssh2_mac_text_name(ssh2_mac *m)
 { return m->vt->text_name(m); }
 static inline const ssh2_macalg *ssh2_mac_alg(ssh2_mac *m)
@@ -742,6 +756,8 @@ bool ssh2_mac_verresult(ssh2_mac *, const void *);
 void ssh2_mac_generate(ssh2_mac *, void *, int, unsigned long seq);
 bool ssh2_mac_verify(ssh2_mac *, const void *, int, unsigned long seq);
 
+void nullmac_next_message(ssh2_mac *m);
+
 /* Use a MAC in its raw form, outside SSH-2 context, to MAC a given
  * string with a given key in the most obvious way. */
 void mac_simple(const ssh2_macalg *alg, ptrlen key, ptrlen data, void *output);
@@ -804,11 +820,20 @@ void hash_simple(const ssh_hashalg *alg, ptrlen data, void *output);
 
 struct ssh_kex {
     const char *name, *groupname;
-    enum { KEXTYPE_DH, KEXTYPE_RSA, KEXTYPE_ECDH, KEXTYPE_GSS } main_type;
+    enum { KEXTYPE_DH, KEXTYPE_RSA, KEXTYPE_ECDH,
+           KEXTYPE_GSS, KEXTYPE_GSS_ECDH } main_type;
     const ssh_hashalg *hash;
+    union {                  /* publicly visible data for each type */
+        const ecdh_keyalg *ecdh_vt;    /* for KEXTYPE_ECDH, KEXTYPE_GSS_ECDH */
+    };
     const void *extra;                 /* private to the kex methods */
 };
 
+static inline bool kex_is_gss(const struct ssh_kex *kex)
+{
+    return kex->main_type == KEXTYPE_GSS || kex->main_type == KEXTYPE_GSS_ECDH;
+}
+
 struct ssh_kexes {
     int nkexes;
     const ssh_kex *const *list;
@@ -835,17 +860,34 @@ struct ssh_keyalg {
     void (*public_blob)(ssh_key *key, BinarySink *);
     void (*private_blob)(ssh_key *key, BinarySink *);
     void (*openssh_blob) (ssh_key *key, BinarySink *);
+    bool (*has_private) (ssh_key *key);
     char *(*cache_str) (ssh_key *key);
     key_components *(*components) (ssh_key *key);
+    ssh_key *(*base_key) (ssh_key *key); /* does not confer ownership */
+    /* The following methods can be NULL if !is_certificate */
+    void (*ca_public_blob)(ssh_key *key, BinarySink *);
+    bool (*check_cert)(ssh_key *key, bool host, ptrlen principal,
+                       uint64_t time, const ca_options *opts,
+                       BinarySink *error);
+    void (*cert_id_string)(ssh_key *key, BinarySink *);
+    SeatDialogText *(*cert_info)(ssh_key *key);
 
     /* 'Class methods' that don't deal with an ssh_key at all */
     int (*pubkey_bits) (const ssh_keyalg *self, ptrlen blob);
+    unsigned (*supported_flags) (const ssh_keyalg *self);
+    const char *(*alternate_ssh_id) (const ssh_keyalg *self, unsigned flags);
+    char *(*alg_desc)(const ssh_keyalg *self);
+    bool (*variable_size)(const ssh_keyalg *self);
+    /* The following methods can be NULL if !is_certificate */
+    const ssh_keyalg *(*related_alg)(const ssh_keyalg *self,
+                                     const ssh_keyalg *base);
 
     /* Constant data fields giving information about the key type */
     const char *ssh_id;    /* string identifier in the SSH protocol */
     const char *cache_id;  /* identifier used in PuTTY's host key cache */
     const void *extra;     /* private to the public key methods */
-    const unsigned supported_flags;    /* signature-type flags we understand */
+    bool is_certificate;   /* is this a certified key type? */
+    const ssh_keyalg *base_alg; /* if so, for what underlying key alg? */
 };
 
 static inline ssh_key *ssh_key_new_pub(const ssh_keyalg *self, ptrlen pub)
@@ -871,10 +913,24 @@ static inline void ssh_key_private_blob(ssh_key *key, BinarySink *bs)
 { key->vt->private_blob(key, bs); }
 static inline void ssh_key_openssh_blob(ssh_key *key, BinarySink *bs)
 { key->vt->openssh_blob(key, bs); }
+static inline bool ssh_key_has_private(ssh_key *key)
+{ return key->vt->has_private(key); }
 static inline char *ssh_key_cache_str(ssh_key *key)
 { return key->vt->cache_str(key); }
 static inline key_components *ssh_key_components(ssh_key *key)
 { return key->vt->components(key); }
+static inline ssh_key *ssh_key_base_key(ssh_key *key)
+{ return key->vt->base_key(key); }
+static inline void ssh_key_ca_public_blob(ssh_key *key, BinarySink *bs)
+{ key->vt->ca_public_blob(key, bs); }
+static inline void ssh_key_cert_id_string(ssh_key *key, BinarySink *bs)
+{ key->vt->cert_id_string(key, bs); }
+static inline SeatDialogText *ssh_key_cert_info(ssh_key *key)
+{ return key->vt->cert_info(key); }
+static inline bool ssh_key_check_cert(
+    ssh_key *key, bool host, ptrlen principal, uint64_t time,
+    const ca_options *opts, BinarySink *error)
+{ return key->vt->check_cert(key, host, principal, time, opts, error); }
 static inline int ssh_key_public_bits(const ssh_keyalg *self, ptrlen blob)
 { return self->pubkey_bits(self, blob); }
 static inline const ssh_keyalg *ssh_key_alg(ssh_key *key)
@@ -883,6 +939,73 @@ static inline const char *ssh_key_ssh_id(ssh_key *key)
 { return key->vt->ssh_id; }
 static inline const char *ssh_key_cache_id(ssh_key *key)
 { return key->vt->cache_id; }
+static inline unsigned ssh_key_supported_flags(ssh_key *key)
+{ return key->vt->supported_flags(key->vt); }
+static inline unsigned ssh_keyalg_supported_flags(const ssh_keyalg *self)
+{ return self->supported_flags(self); }
+static inline const char *ssh_keyalg_alternate_ssh_id(
+    const ssh_keyalg *self, unsigned flags)
+{ return self->alternate_ssh_id(self, flags); }
+static inline char *ssh_keyalg_desc(const ssh_keyalg *self)
+{ return self->alg_desc(self); }
+static inline bool ssh_keyalg_variable_size(const ssh_keyalg *self)
+{ return self->variable_size(self); }
+static inline const ssh_keyalg *ssh_keyalg_related_alg(
+    const ssh_keyalg *self, const ssh_keyalg *base)
+{ return self->related_alg(self, base); }
+
+/* Stub functions shared between multiple key types */
+unsigned nullkey_supported_flags(const ssh_keyalg *self);
+const char *nullkey_alternate_ssh_id(const ssh_keyalg *self, unsigned flags);
+ssh_key *nullkey_base_key(ssh_key *key);
+bool nullkey_variable_size_no(const ssh_keyalg *self);
+bool nullkey_variable_size_yes(const ssh_keyalg *self);
+
+/* Utility functions implemented centrally */
+ssh_key *ssh_key_clone(ssh_key *key);
+
+/*
+ * SSH2 ECDH key exchange vtable
+ */
+struct ecdh_key {
+    const ecdh_keyalg *vt;
+};
+struct ecdh_keyalg {
+    /* Unusually, the 'new' method here doesn't directly take a vt
+     * pointer, because it will also need the containing ssh_kex
+     * structure for top-level parameters, and since that contains a
+     * vt pointer anyway, we might as well _only_ pass that. */
+    ecdh_key *(*new)(const ssh_kex *kex, bool is_server);
+    void (*free)(ecdh_key *key);
+    void (*getpublic)(ecdh_key *key, BinarySink *bs);
+    bool (*getkey)(ecdh_key *key, ptrlen remoteKey, BinarySink *bs);
+    char *(*description)(const ssh_kex *kex);
+};
+static inline ecdh_key *ecdh_key_new(const ssh_kex *kex, bool is_server)
+{ return kex->ecdh_vt->new(kex, is_server); }
+static inline void ecdh_key_free(ecdh_key *key)
+{ key->vt->free(key); }
+static inline void ecdh_key_getpublic(ecdh_key *key, BinarySink *bs)
+{ key->vt->getpublic(key, bs); }
+static inline bool ecdh_key_getkey(ecdh_key *key, ptrlen remoteKey,
+                                   BinarySink *bs)
+{ return key->vt->getkey(key, remoteKey, bs); }
+static inline char *ecdh_keyalg_description(const ssh_kex *kex)
+{ return kex->ecdh_vt->description(kex); }
+
+/*
+ * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5
+ * as the mechanism.
+ *
+ * This suffix is the base64-encoded MD5 hash of the byte sequence
+ * 06 09 2A 86 48 86 F7 12 01 02 02, which in turn is the ASN.1 DER
+ * encoding of the object ID 1.2.840.113554.1.2.2 which designates
+ * Kerberos v5.
+ *
+ * (The same encoded OID, minus the two-byte DER header, is defined in
+ * ssh/pgssapi.c as GSS_MECH_KRB5.)
+ */
+#define GSS_KRB5_OID_HASH "toWM5Slw5Ew8Mqkay+al2g=="
 
 /*
  * Enumeration of signature flags from draft-miller-ssh-agent-02
@@ -969,6 +1092,10 @@ extern const ssh_cipheralg ssh_aes256_sdctr;
 extern const ssh_cipheralg ssh_aes256_sdctr_ni;
 extern const ssh_cipheralg ssh_aes256_sdctr_neon;
 extern const ssh_cipheralg ssh_aes256_sdctr_sw;
+extern const ssh_cipheralg ssh_aes256_gcm;
+extern const ssh_cipheralg ssh_aes256_gcm_ni;
+extern const ssh_cipheralg ssh_aes256_gcm_neon;
+extern const ssh_cipheralg ssh_aes256_gcm_sw;
 extern const ssh_cipheralg ssh_aes256_cbc;
 extern const ssh_cipheralg ssh_aes256_cbc_ni;
 extern const ssh_cipheralg ssh_aes256_cbc_neon;
@@ -977,6 +1104,10 @@ extern const ssh_cipheralg ssh_aes192_sdctr;
 extern const ssh_cipheralg ssh_aes192_sdctr_ni;
 extern const ssh_cipheralg ssh_aes192_sdctr_neon;
 extern const ssh_cipheralg ssh_aes192_sdctr_sw;
+extern const ssh_cipheralg ssh_aes192_gcm;
+extern const ssh_cipheralg ssh_aes192_gcm_ni;
+extern const ssh_cipheralg ssh_aes192_gcm_neon;
+extern const ssh_cipheralg ssh_aes192_gcm_sw;
 extern const ssh_cipheralg ssh_aes192_cbc;
 extern const ssh_cipheralg ssh_aes192_cbc_ni;
 extern const ssh_cipheralg ssh_aes192_cbc_neon;
@@ -985,6 +1116,10 @@ extern const ssh_cipheralg ssh_aes128_sdctr;
 extern const ssh_cipheralg ssh_aes128_sdctr_ni;
 extern const ssh_cipheralg ssh_aes128_sdctr_neon;
 extern const ssh_cipheralg ssh_aes128_sdctr_sw;
+extern const ssh_cipheralg ssh_aes128_gcm;
+extern const ssh_cipheralg ssh_aes128_gcm_ni;
+extern const ssh_cipheralg ssh_aes128_gcm_neon;
+extern const ssh_cipheralg ssh_aes128_gcm_sw;
 extern const ssh_cipheralg ssh_aes128_cbc;
 extern const ssh_cipheralg ssh_aes128_cbc_ni;
 extern const ssh_cipheralg ssh_aes128_cbc_neon;
@@ -1000,6 +1135,7 @@ extern const ssh2_ciphers ssh2_aes;
 extern const ssh2_ciphers ssh2_blowfish;
 extern const ssh2_ciphers ssh2_arcfour;
 extern const ssh2_ciphers ssh2_ccp;
+extern const ssh2_ciphers ssh2_aesgcm;
 extern const ssh_hashalg ssh_md5;
 extern const ssh_hashalg ssh_sha1;
 extern const ssh_hashalg ssh_sha1_ni;
@@ -1023,11 +1159,21 @@ extern const ssh_hashalg ssh_shake256_114bytes;
 extern const ssh_hashalg ssh_blake2b;
 extern const ssh_kexes ssh_diffiehellman_group1;
 extern const ssh_kexes ssh_diffiehellman_group14;
+extern const ssh_kexes ssh_diffiehellman_group15;
+extern const ssh_kexes ssh_diffiehellman_group16;
+extern const ssh_kexes ssh_diffiehellman_group17;
+extern const ssh_kexes ssh_diffiehellman_group18;
 extern const ssh_kexes ssh_diffiehellman_gex;
 extern const ssh_kex ssh_diffiehellman_group1_sha1;
 extern const ssh_kex ssh_diffiehellman_group14_sha256;
 extern const ssh_kex ssh_diffiehellman_group14_sha1;
+extern const ssh_kex ssh_diffiehellman_group15_sha512;
+extern const ssh_kex ssh_diffiehellman_group16_sha512;
+extern const ssh_kex ssh_diffiehellman_group17_sha512;
+extern const ssh_kex ssh_diffiehellman_group18_sha512;
 extern const ssh_kexes ssh_gssk5_sha1_kex;
+extern const ssh_kexes ssh_gssk5_sha2_kex;
+extern const ssh_kexes ssh_gssk5_ecdh_kex;
 extern const ssh_kexes ssh_rsa_kex;
 extern const ssh_kex ssh_ec_kex_curve25519;
 extern const ssh_kex ssh_ec_kex_curve448;
@@ -1035,6 +1181,7 @@ extern const ssh_kex ssh_ec_kex_nistp256;
 extern const ssh_kex ssh_ec_kex_nistp384;
 extern const ssh_kex ssh_ec_kex_nistp521;
 extern const ssh_kexes ssh_ecdh_kex;
+extern const ssh_kexes ssh_ntru_hybrid_kex;
 extern const ssh_keyalg ssh_dsa;
 extern const ssh_keyalg ssh_rsa;
 extern const ssh_keyalg ssh_rsa_sha256;
@@ -1044,6 +1191,14 @@ extern const ssh_keyalg ssh_ecdsa_ed448;
 extern const ssh_keyalg ssh_ecdsa_nistp256;
 extern const ssh_keyalg ssh_ecdsa_nistp384;
 extern const ssh_keyalg ssh_ecdsa_nistp521;
+extern const ssh_keyalg opensshcert_ssh_dsa;
+extern const ssh_keyalg opensshcert_ssh_rsa;
+extern const ssh_keyalg opensshcert_ssh_rsa_sha256;
+extern const ssh_keyalg opensshcert_ssh_rsa_sha512;
+extern const ssh_keyalg opensshcert_ssh_ecdsa_ed25519;
+extern const ssh_keyalg opensshcert_ssh_ecdsa_nistp256;
+extern const ssh_keyalg opensshcert_ssh_ecdsa_nistp384;
+extern const ssh_keyalg opensshcert_ssh_ecdsa_nistp521;
 extern const ssh2_macalg ssh_hmac_md5;
 extern const ssh2_macalg ssh_hmac_sha1;
 extern const ssh2_macalg ssh_hmac_sha1_buggy;
@@ -1051,12 +1206,20 @@ extern const ssh2_macalg ssh_hmac_sha1_96;
 extern const ssh2_macalg ssh_hmac_sha1_96_buggy;
 extern const ssh2_macalg ssh_hmac_sha256;
 extern const ssh2_macalg ssh2_poly1305;
+extern const ssh2_macalg ssh2_aesgcm_mac;
+extern const ssh2_macalg ssh2_aesgcm_mac_sw;
+extern const ssh2_macalg ssh2_aesgcm_mac_ref_poly;
+extern const ssh2_macalg ssh2_aesgcm_mac_clmul;
+extern const ssh2_macalg ssh2_aesgcm_mac_neon;
 extern const ssh_compression_alg ssh_zlib;
 
 /* Special constructor: BLAKE2b can be instantiated with any hash
  * length up to 128 bytes */
 ssh_hash *blake2b_new_general(unsigned hashlen);
 
+/* Special test function for AES-GCM */
+void aesgcm_set_prefix_lengths(ssh2_mac *mac, size_t skip, size_t aad);
+
 /*
  * On some systems, you have to detect hardware crypto acceleration by
  * asking the local OS API rather than OS-agnostically asking the CPU
@@ -1064,6 +1227,7 @@ ssh_hash *blake2b_new_general(unsigned hashlen);
  * platform subdirectory.
  */
 bool platform_aes_neon_available(void);
+bool platform_pmull_neon_available(void);
 bool platform_sha256_neon_available(void);
 bool platform_sha1_neon_available(void);
 bool platform_sha512_neon_available(void);
@@ -1236,11 +1400,7 @@ static inline bool is_base64_char(char c)
             c == '+' || c == '/' || c == '=');
 }
 
-extern int base64_decode_atom(const char *atom, unsigned char *out);
 extern int base64_lines(int datalen);
-extern void base64_encode_atom(const unsigned char *data, int n, char *out);
-extern void base64_encode(FILE *fp, const unsigned char *data, int datalen,
-                          int cpl);
 
 /* ppk_load_* can return this as an error */
 extern ssh2_userkey ssh2_wrong_passphrase;
@@ -1307,6 +1467,9 @@ extern const size_t n_keyalgs;
 const ssh_keyalg *find_pubkey_alg(const char *name);
 const ssh_keyalg *find_pubkey_alg_len(ptrlen name);
 
+ptrlen pubkey_blob_to_alg_name(ptrlen blob);
+const ssh_keyalg *pubkey_blob_to_alg(ptrlen blob);
+
 /* Convenient wrappers on the LoadedFile mechanism suitable for key files */
 LoadedFile *lf_load_keyfile(const Filename *filename, const char **errptr);
 LoadedFile *lf_load_keyfile_fp(FILE *fp, const char **errptr);
@@ -1356,12 +1519,36 @@ enum {
 };
 
 typedef enum {
+    /* Default fingerprint types strip off a certificate to show you
+     * the fingerprint of the underlying public key */
     SSH_FPTYPE_MD5,
     SSH_FPTYPE_SHA256,
+    /* Non-default version of each fingerprint type which is 'raw',
+     * giving you the true hash of the public key blob even if it
+     * includes a certificate */
+    SSH_FPTYPE_MD5_CERT,
+    SSH_FPTYPE_SHA256_CERT,
 } FingerprintType;
 
+static inline bool ssh_fptype_is_cert(FingerprintType fptype)
+{
+    return fptype >= SSH_FPTYPE_MD5_CERT;
+}
+static inline FingerprintType ssh_fptype_from_cert(FingerprintType fptype)
+{
+    if (ssh_fptype_is_cert(fptype))
+        fptype -= (SSH_FPTYPE_MD5_CERT - SSH_FPTYPE_MD5);
+    return fptype;
+}
+static inline FingerprintType ssh_fptype_to_cert(FingerprintType fptype)
+{
+    if (!ssh_fptype_is_cert(fptype))
+        fptype += (SSH_FPTYPE_MD5_CERT - SSH_FPTYPE_MD5);
+    return fptype;
+}
+
+#define SSH_N_FPTYPES (SSH_FPTYPE_SHA256_CERT + 1)
 #define SSH_FPTYPE_DEFAULT SSH_FPTYPE_SHA256
-#define SSH_N_FPTYPES (SSH_FPTYPE_SHA256 + 1)
 
 FingerprintType ssh2_pick_fingerprint(char **fingerprints,
                                       FingerprintType preferred_type);
@@ -1375,6 +1562,8 @@ void ssh2_write_pubkey(FILE *fp, const char *comment,
                        int keytype);
 char *ssh2_fingerprint_blob(ptrlen, FingerprintType);
 char *ssh2_fingerprint(ssh_key *key, FingerprintType);
+char *ssh2_double_fingerprint_blob(ptrlen, FingerprintType);
+char *ssh2_double_fingerprint(ssh_key *key, FingerprintType);
 char **ssh2_all_fingerprints_for_blob(ptrlen);
 char **ssh2_all_fingerprints(ssh_key *key);
 void ssh2_free_all_fingerprints(char **);
@@ -1386,7 +1575,7 @@ bool import_possible(int type);
 int import_target_type(int type);
 bool import_encrypted(const Filename *filename, int type, char **comment);
 bool import_encrypted_s(const Filename *filename, BinarySource *src,
-                      int type, char **comment);
+                        int type, char **comment);
 int import_ssh1(const Filename *filename, int type,
                 RSAKey *key, char *passphrase, const char **errmsg_p);
 int import_ssh1_s(BinarySource *src, int type,
@@ -1394,7 +1583,7 @@ int import_ssh1_s(BinarySource *src, int type,
 ssh2_userkey *import_ssh2(const Filename *filename, int type,
                           char *passphrase, const char **errmsg_p);
 ssh2_userkey *import_ssh2_s(BinarySource *src, int type,
-                          char *passphrase, const char **errmsg_p);
+                            char *passphrase, const char **errmsg_p);
 bool export_ssh1(const Filename *filename, int type,
                  RSAKey *key, char *passphrase);
 bool export_ssh2(const Filename *filename, int type,
@@ -1691,6 +1880,7 @@ void old_keyfile_warning(void);
     X(BUG_CHOKES_ON_WINADJ)                     \
     X(BUG_SENDS_LATE_REQUEST_REPLY)             \
     X(BUG_SSH2_OLDGEX)                          \
+    X(BUG_REQUIRES_FILTERED_KEXINIT)            \
     /* end of list */
 #define TMP_DECLARE_LOG2_ENUM(thing) log2_##thing,
 enum { SSH_IMPL_BUG_LIST(TMP_DECLARE_LOG2_ENUM) };
@@ -1708,13 +1898,14 @@ unsigned alloc_channel_id_general(tree234 *channels, size_t localid_offset);
               alloc_channel_id_general(tree, offsetof(type, localid)))
 
 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);
 
 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, void (*callback)(void *ctx, SeatPromptResult result),
-    void *ctx);
+    char **fingerprints, int ca_count,
+    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);
@@ -1726,3 +1917,35 @@ bool ssh_transient_hostkey_cache_verify(
 bool ssh_transient_hostkey_cache_has(
     ssh_transient_hostkey_cache *thc, const ssh_keyalg *alg);
 bool ssh_transient_hostkey_cache_non_empty(ssh_transient_hostkey_cache *thc);
+
+/*
+ * Protocol definitions for authentication helper plugins
+ */
+
+#define AUTHPLUGIN_MSG_NAMES(X)                 \
+    X(PLUGIN_INIT, 1)                           \
+    X(PLUGIN_INIT_RESPONSE, 2)                  \
+    X(PLUGIN_PROTOCOL, 3)                       \
+    X(PLUGIN_PROTOCOL_ACCEPT, 4)                \
+    X(PLUGIN_PROTOCOL_REJECT, 5)                \
+    X(PLUGIN_AUTH_SUCCESS, 6)                   \
+    X(PLUGIN_AUTH_FAILURE, 7)                   \
+    X(PLUGIN_INIT_FAILURE, 8)                   \
+    X(PLUGIN_KI_SERVER_REQUEST, 20)             \
+    X(PLUGIN_KI_SERVER_RESPONSE, 21)            \
+    X(PLUGIN_KI_USER_REQUEST, 22)               \
+    X(PLUGIN_KI_USER_RESPONSE, 23)              \
+    /* end of list */
+
+#define PLUGIN_PROTOCOL_MAX_VERSION 2  /* the highest version we speak */
+
+enum {
+    #define ENUMDECL(name, value) name = value,
+    AUTHPLUGIN_MSG_NAMES(ENUMDECL)
+    #undef ENUMDECL
+
+    /* Error codes internal to this implementation, indicating failure
+     * to receive a meaningful packet at all */
+    PLUGIN_NOTYPE = 256, /* packet too short to have a type */
+    PLUGIN_EOF = 257 /* EOF from auth plugin */
+};

+ 15 - 9
source/putty/ssh/bpp2.c

@@ -73,15 +73,6 @@ BinaryPacketProtocol *ssh2_bpp_new(
 
 static void ssh2_bpp_free_outgoing_crypto(struct ssh2_bpp_state *s)
 {
-    /*
-     * We must free the MAC before the cipher, because sometimes the
-     * MAC is not actually separately allocated but just a different
-     * facet of the same object as the cipher, in which case
-     * ssh2_mac_free does nothing and ssh_cipher_free does the actual
-     * freeing. So if we freed the cipher first and then tried to
-     * dereference the MAC's vtable pointer to find out how to free
-     * that too, we'd be accessing freed memory.
-     */
     if (s->out.mac)
         ssh2_mac_free(s->out.mac);
     if (s->out.cipher)
@@ -141,6 +132,12 @@ void ssh2_bpp_new_outgoing_crypto(
     s->out.etm_mode = etm_mode;
     if (mac) {
         s->out.mac = ssh2_mac_new(mac, s->out.cipher);
+        /*
+         * Important that mac_setkey comes after cipher_setkey,
+         * because in the case where the MAC makes use of the cipher
+         * (e.g. AES-GCM), it will need the cipher to be keyed
+         * already.
+         */
         ssh2_mac_setkey(s->out.mac, make_ptrlen(mac_key, mac->keylen));
 
         bpp_logevent("Initialised %s outbound MAC algorithm%s%s",
@@ -198,6 +195,7 @@ void ssh2_bpp_new_incoming_crypto(
     s->in.etm_mode = etm_mode;
     if (mac) {
         s->in.mac = ssh2_mac_new(mac, s->in.cipher);
+        /* MAC setkey has to follow cipher, just as in outgoing_crypto above */
         ssh2_mac_setkey(s->in.mac, make_ptrlen(mac_key, mac->keylen));
 
         bpp_logevent("Initialised %s inbound MAC algorithm%s%s",
@@ -522,6 +520,10 @@ static void ssh2_bpp_handle_input(BinaryPacketProtocol *bpp)
         dts_consume(&s->stats->in, s->packetlen);
 
         s->pktin->sequence = s->in.sequence++;
+        if (s->in.cipher)
+            ssh_cipher_next_message(s->in.cipher);
+        if (s->in.mac)
+            ssh2_mac_next_message(s->in.mac);
 
         s->length = s->packetlen - s->pad;
         assert(s->length >= 0);
@@ -828,6 +830,10 @@ static void ssh2_bpp_format_packet_inner(struct ssh2_bpp_state *s, PktOut *pkt)
     }
 
     s->out.sequence++;       /* whether or not we MACed */
+    if (s->out.cipher)
+        ssh_cipher_next_message(s->out.cipher);
+    if (s->out.mac)
+        ssh2_mac_next_message(s->out.mac);
 
     dts_consume(&s->stats->out, origlen + padding);
 }

+ 2 - 2
source/putty/ssh/channel.h

@@ -84,10 +84,10 @@ static inline bool chan_want_close(Channel *ch, bool leof, bool reof)
 static inline bool chan_rcvd_exit_status(Channel *ch, int status)
 { return ch->vt->rcvd_exit_status(ch, status); }
 static inline bool chan_rcvd_exit_signal(
-        Channel *ch, ptrlen sig, bool core, ptrlen msg)
+    Channel *ch, ptrlen sig, bool core, ptrlen msg)
 { return ch->vt->rcvd_exit_signal(ch, sig, core, msg); }
 static inline bool chan_rcvd_exit_signal_numeric(
-        Channel *ch, int sig, bool core, ptrlen msg)
+    Channel *ch, int sig, bool core, ptrlen msg)
 { return ch->vt->rcvd_exit_signal_numeric(ch, sig, core, msg); }
 static inline bool chan_run_shell(Channel *ch)
 { return ch->vt->run_shell(ch); }

+ 159 - 9
source/putty/ssh/common.c

@@ -607,11 +607,16 @@ unsigned alloc_channel_id_general(tree234 *channels, size_t localid_offset)
  * lists of protocol identifiers in SSH-2.
  */
 
-void add_to_commasep(strbuf *buf, const char *data)
+void add_to_commasep_pl(strbuf *buf, ptrlen data)
 {
     if (buf->len > 0)
         put_byte(buf, ',');
-    put_data(buf, data, strlen(data));
+    put_datapl(buf, data);
+}
+
+void add_to_commasep(strbuf *buf, const char *data)
+{
+    add_to_commasep_pl(buf, ptrlen_from_asciz(data));
 }
 
 bool get_commasep_word(ptrlen *list, ptrlen *word)
@@ -835,7 +840,7 @@ bool ssh2_bpp_check_unimplemented(BinaryPacketProtocol *bpp, PktIn *pktin)
 
 #undef BITMAP_UNIVERSAL
 #undef BITMAP_CONDITIONAL
-#undef SSH1_BITMAP_WORD
+#undef SSH2_BITMAP_WORD
 
 /* ----------------------------------------------------------------------
  * Centralised component of SSH host key verification.
@@ -850,8 +855,8 @@ bool ssh2_bpp_check_unimplemented(BinaryPacketProtocol *bpp, PktIn *pktin)
 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, void (*callback)(void *ctx, SeatPromptResult result),
-    void *ctx)
+    char **fingerprints, int ca_count,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 {
     /*
      * First, check if the Conf includes a manual specification of the
@@ -919,10 +924,155 @@ SeatPromptResult verify_ssh_host_key(
      * The key is either missing from the cache, or does not match.
      * Either way, fall back to an interactive prompt from the Seat.
      */
-    bool mismatch = (storage_status != 1);
-    return seat_confirm_ssh_host_key(
-        iseat, host, port, keytype, keystr, keydisp, fingerprints, mismatch,
-        callback, ctx);
+    SeatDialogText *text = seat_dialog_text_new();
+    const SeatDialogPromptDescriptions *pds =
+        seat_prompt_descriptions(iseat.seat);
+
+    FingerprintType fptype_default =
+        ssh2_pick_default_fingerprint(fingerprints);
+
+    seat_dialog_text_append(
+        text, SDT_TITLE, "%s Security Alert", appname);
+
+    HelpCtx helpctx;
+
+    if (key && ssh_key_alg(key)->is_certificate) {
+        seat_dialog_text_append(
+            text, SDT_SCARY_HEADING, "WARNING - POTENTIAL SECURITY BREACH!");
+        seat_dialog_text_append(
+            text, SDT_PARA, "This server presented a certified host key:");
+        seat_dialog_text_append(
+            text, SDT_DISPLAY, "%s (port %d)", host, port);
+        if (ca_count) {
+            seat_dialog_text_append(
+                text, SDT_PARA, "which was signed by a different "
+                "certification authority from the %s %s is configured to "
+                "trust for this server.", ca_count > 1 ? "ones" : "one",
+                appname);
+            if (storage_status == 2) {
+                seat_dialog_text_append(
+                    text, SDT_PARA, "ALSO, that key does not match the key "
+                    "%s had previously cached for this server.", appname);
+                seat_dialog_text_append(
+                    text, SDT_PARA, "This means that either another "
+                    "certification authority is operating in this realm AND "
+                    "the server administrator has changed the host key, or "
+                    "you have actually connected to another computer "
+                    "pretending to be the server.");
+            } else {
+                seat_dialog_text_append(
+                    text, SDT_PARA, "This means that either another "
+                    "certification authority is operating in this realm, or "
+                    "you have actually connected to another computer "
+                    "pretending to be the server.");
+            }
+        } else {
+            assert(storage_status == 2);
+            seat_dialog_text_append(
+                text, SDT_PARA, "which does not match the certified key %s "
+                "had previously cached for this server.", appname);
+            seat_dialog_text_append(
+                text, SDT_PARA, "This means that either the server "
+                "administrator has changed the host key, or you have actually "
+                "connected to another computer pretending to be the server.");
+        }
+        seat_dialog_text_append(
+            text, SDT_PARA, "The new %s key fingerprint is:", keytype);
+        seat_dialog_text_append(
+            text, SDT_DISPLAY, "%s", fingerprints[fptype_default]);
+        helpctx = HELPCTX(errors_cert_mismatch);
+    } else if (storage_status == 1) {
+        seat_dialog_text_append(
+            text, SDT_PARA, "The host key is not cached for this server:");
+        seat_dialog_text_append(
+            text, SDT_DISPLAY, "%s (port %d)", host, port);
+        seat_dialog_text_append(
+            text, SDT_PARA, "You have no guarantee that the server is the "
+            "computer you think it is.");
+        seat_dialog_text_append(
+            text, SDT_PARA, "The server's %s key fingerprint is:", keytype);
+        seat_dialog_text_append(
+            text, SDT_DISPLAY, "%s", fingerprints[fptype_default]);
+        helpctx = HELPCTX(errors_hostkey_absent);
+    } else {
+        seat_dialog_text_append(
+            text, SDT_SCARY_HEADING, "WARNING - POTENTIAL SECURITY BREACH!");
+        seat_dialog_text_append(
+            text, SDT_PARA, "The host key does not match the one %s has "
+            "cached for this server:", appname);
+        seat_dialog_text_append(
+            text, SDT_DISPLAY, "%s (port %d)", host, port);
+        seat_dialog_text_append(
+            text, SDT_PARA, "This means that either the server administrator "
+            "has changed the host key, or you have actually connected to "
+            "another computer pretending to be the server.");
+        seat_dialog_text_append(
+            text, SDT_PARA, "The new %s key fingerprint is:", keytype);
+        seat_dialog_text_append(
+            text, SDT_DISPLAY, "%s", fingerprints[fptype_default]);
+        helpctx = HELPCTX(errors_hostkey_changed);
+    }
+
+    /* The above text is printed even in batch mode. Here's where we stop if
+     * we can't present interactive prompts. */
+    seat_dialog_text_append(
+        text, SDT_BATCH_ABORT, "Connection abandoned.");
+
+    if (storage_status == 1) {
+        seat_dialog_text_append(
+            text, SDT_PARA, "If you trust this host, %s to add the key to "
+            "%s's cache and carry on connecting.",
+            pds->hk_accept_action, appname);
+        seat_dialog_text_append(
+            text, SDT_PARA, "If you want to carry on connecting just once, "
+            "without adding the key to the cache, %s.",
+            pds->hk_connect_once_action);
+        seat_dialog_text_append(
+            text, SDT_PARA, "If you do not trust this host, %s to abandon the "
+            "connection.", pds->hk_cancel_action);
+        seat_dialog_text_append(
+            text, SDT_PROMPT, "Store key in cache?");
+    } else {
+        seat_dialog_text_append(
+            text, SDT_PARA, "If you were expecting this change and trust the "
+            "new key, %s to update %s's cache and carry on connecting.",
+            pds->hk_accept_action, appname);
+        if (key && ssh_key_alg(key)->is_certificate) {
+            seat_dialog_text_append(
+                text, SDT_PARA, "(Storing this certified key in the cache "
+                "will NOT cause its certification authority to be trusted "
+                "for any other key or host.)");
+        }
+        seat_dialog_text_append(
+            text, SDT_PARA, "If you want to carry on connecting but without "
+            "updating the cache, %s.", pds->hk_connect_once_action);
+        seat_dialog_text_append(
+            text, SDT_PARA, "If you want to abandon the connection "
+            "completely, %s to cancel. %s is the ONLY guaranteed safe choice.",
+            pds->hk_cancel_action, pds->hk_cancel_action_Participle);
+        seat_dialog_text_append(
+            text, SDT_PROMPT, "Update cached key?");
+    }
+
+    seat_dialog_text_append(text, SDT_MORE_INFO_KEY,
+                            "Full text of host's public key");
+    seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_BLOB, "%s", keydisp);
+
+    if (fingerprints[SSH_FPTYPE_SHA256]) {
+        seat_dialog_text_append(text, SDT_MORE_INFO_KEY, "SHA256 fingerprint");
+        seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s",
+                                fingerprints[SSH_FPTYPE_SHA256]);
+    }
+    if (fingerprints[SSH_FPTYPE_MD5]) {
+        seat_dialog_text_append(text, SDT_MORE_INFO_KEY, "MD5 fingerprint");
+        seat_dialog_text_append(text, SDT_MORE_INFO_VALUE_SHORT, "%s",
+                                fingerprints[SSH_FPTYPE_MD5]);
+    }
+
+    SeatPromptResult toret = seat_confirm_ssh_host_key(
+        iseat, host, port, keytype, keystr, text, helpctx, callback, ctx);
+    seat_dialog_text_free(text);
+    return toret;
 }
 
 /* ----------------------------------------------------------------------

+ 3 - 3
source/putty/ssh/connection2.c

@@ -989,7 +989,7 @@ static void ssh2_connection_process_queue(PacketProtocolLayer *ppl)
      */
     if (ssh2_connection_need_antispoof_prompt(s)) {
         s->antispoof_prompt = ssh_ppl_new_prompts(&s->ppl);
-        s->antispoof_prompt->to_server = true;
+        s->antispoof_prompt->to_server = false;
         s->antispoof_prompt->from_server = false;
         s->antispoof_prompt->name = dupstr("Authentication successful");
         add_prompt(
@@ -1565,8 +1565,8 @@ static void ssh2_delete_sharing_channel(ConnectionLayer *cl, unsigned localid)
 }
 
 static void ssh2_send_packet_from_downstream(
-        ConnectionLayer *cl, unsigned id, int type,
-        const void *data, int datalen, const char *additional_log_text)
+    ConnectionLayer *cl, unsigned id, int type,
+    const void *data, int datalen, const char *additional_log_text)
 {
     struct ssh2_connection_state *s =
         container_of(cl, struct ssh2_connection_state, cl);

+ 1 - 1
source/putty/ssh/gssc.c

@@ -75,7 +75,7 @@ static Ssh_gss_stat ssh_gssapi_acquire_cred(struct ssh_gss_library *lib,
         gssctx->maj_stat =
             gss->inquire_cred_by_mech(&gssctx->min_stat, cred,
                                       (gss_OID) GSS_MECH_KRB5,
-                                      GSS_C_NO_NAME,
+                                      NULL,
                                       &time_rec,
                                       NULL,
                                       NULL);

+ 214 - 75
source/putty/ssh/kex2-client.c

@@ -156,7 +156,9 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                 return;
             }
         }
-        s->K = dh_find_K(s->dh_ctx, s->f);
+        mp_int *K = dh_find_K(s->dh_ctx, s->f);
+        put_mp_ssh2(s->kex_shared_secret, K);
+        mp_free(K);
 
         /* We assume everything from now on will be quick, and it might
          * involve user interaction. */
@@ -183,23 +185,19 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
             mp_free(s->p); s->p = NULL;
         }
     } else if (s->kex_alg->main_type == KEXTYPE_ECDH) {
-
-        ppl_logevent("Doing ECDH key exchange with curve %s and hash %s",
-                     ssh_ecdhkex_curve_textname(s->kex_alg),
+        char *desc = ecdh_keyalg_description(s->kex_alg);
+        ppl_logevent("Doing %s, using hash %s", desc,
                      ssh_hash_alg(s->exhash)->text_name);
+        sfree(desc);
+
         s->ppl.bpp->pls->kctx = SSH2_PKTCTX_ECDHKEX;
 
-        s->ecdh_key = ssh_ecdhkex_newkey(s->kex_alg);
-        if (!s->ecdh_key) {
-            ssh_sw_abort(s->ppl.ssh, "Unable to generate key for ECDH");
-            *aborted = true;
-            return;
-        }
+        s->ecdh_key = ecdh_key_new(s->kex_alg, false);
 
         pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH2_MSG_KEX_ECDH_INIT);
         {
             strbuf *pubpoint = strbuf_new();
-            ssh_ecdhkex_getpublic(s->ecdh_key, BinarySink_UPCAST(pubpoint));
+            ecdh_key_getpublic(s->ecdh_key, BinarySink_UPCAST(pubpoint));
             put_stringsb(pktout, pubpoint);
         }
 
@@ -222,7 +220,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
 
         {
             strbuf *pubpoint = strbuf_new();
-            ssh_ecdhkex_getpublic(s->ecdh_key, BinarySink_UPCAST(pubpoint));
+            ecdh_key_getpublic(s->ecdh_key, BinarySink_UPCAST(pubpoint));
             put_string(s->exhash, pubpoint->u, pubpoint->len);
             strbuf_free(pubpoint);
         }
@@ -230,8 +228,9 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
         {
             ptrlen keydata = get_string(pktin);
             put_stringpl(s->exhash, keydata);
-            s->K = ssh_ecdhkex_getkey(s->ecdh_key, keydata);
-            if (!get_err(pktin) && !s->K) {
+            bool ok = ecdh_key_getkey(s->ecdh_key, keydata,
+                                      BinarySink_UPCAST(s->kex_shared_secret));
+            if (!get_err(pktin) && !ok) {
                 ssh_proto_error(s->ppl.ssh, "Received invalid elliptic curve "
                                 "point in ECDH reply");
                 *aborted = true;
@@ -246,10 +245,10 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
             return;
         }
 
-        ssh_ecdhkex_freekey(s->ecdh_key);
+        ecdh_key_free(s->ecdh_key);
         s->ecdh_key = NULL;
 #ifndef NO_GSSAPI
-    } else if (s->kex_alg->main_type == KEXTYPE_GSS) {
+    } else if (kex_is_gss(s->kex_alg)) {
         ptrlen data;
 
         s->ppl.bpp->pls->kctx = SSH2_PKTCTX_GSSKEX;
@@ -277,14 +276,25 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
         if (s->nbits > s->kex_alg->hash->hlen * 8)
             s->nbits = s->kex_alg->hash->hlen * 8;
 
-        if (dh_is_gex(s->kex_alg)) {
+        assert(!s->ecdh_key);
+        assert(!s->dh_ctx);
+
+        if (s->kex_alg->main_type == KEXTYPE_GSS_ECDH) {
+            s->ecdh_key = ecdh_key_new(s->kex_alg, false);
+
+            char *desc = ecdh_keyalg_description(s->kex_alg);
+            ppl_logevent("Doing GSSAPI (with Kerberos V5) %s with hash %s",
+                         desc, ssh_hash_alg(s->exhash)->text_name);
+            sfree(desc);
+        } else if (dh_is_gex(s->kex_alg)) {
             /*
              * Work out how big a DH group we will need to allow that
              * much data.
              */
             s->pbits = 512 << ((s->nbits - 1) / 64);
             ppl_logevent("Doing GSSAPI (with Kerberos V5) Diffie-Hellman "
-                         "group exchange, with minimum %d bits", s->pbits);
+                         "group exchange, with minimum %d bits, and hash %s",
+                         s->pbits, ssh_hash_alg(s->exhash)->text_name);
             pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH2_MSG_KEXGSS_GROUPREQ);
             put_uint32(pktout, s->pbits); /* min */
             put_uint32(pktout, s->pbits); /* preferred */
@@ -315,14 +325,19 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
         } else {
             s->dh_ctx = dh_setup_group(s->kex_alg);
             ppl_logevent("Using GSSAPI (with Kerberos V5) Diffie-Hellman with"
-                         " standard group \"%s\"", s->kex_alg->groupname);
+                         " standard group \"%s\" and hash %s",
+                         s->kex_alg->groupname,
+                         ssh_hash_alg(s->exhash)->text_name);
         }
 
-        ppl_logevent("Doing GSSAPI (with Kerberos V5) Diffie-Hellman key "
-                     "exchange with hash %s", ssh_hash_alg(s->exhash)->text_name);
         /* Now generate e for Diffie-Hellman. */
         seat_set_busy_status(s->ppl.seat, BUSY_CPU);
-        s->e = dh_create_e(s->dh_ctx);
+        if (s->ecdh_key) {
+            s->ebuf = strbuf_new_nm();
+            ecdh_key_getpublic(s->ecdh_key, BinarySink_UPCAST(s->ebuf));
+        } else {
+            s->e = dh_create_e(s->dh_ctx);
+        }
 
         if (s->shgss->lib->gsslogmsg)
             ppl_logevent("%s", s->shgss->lib->gsslogmsg);
@@ -386,7 +401,11 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                 }
                 put_string(pktout,
                            s->gss_sndtok.value, s->gss_sndtok.length);
-                put_mp_ssh2(pktout, s->e);
+                if (s->ecdh_key) {
+                    put_stringpl(pktout, ptrlen_from_strbuf(s->ebuf));
+                } else {
+                    put_mp_ssh2(pktout, s->e);
+                }
                 pq_push(s->ppl.out_pq, pktout);
                 s->shgss->lib->free_tok(s->shgss->lib, &s->gss_sndtok);
                 ppl_logevent("GSSAPI key exchange initialised");
@@ -413,7 +432,11 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                 continue;
               case SSH2_MSG_KEXGSS_COMPLETE:
                 s->complete_rcvd = true;
-                s->f = get_mp_ssh2(pktin);
+                if (s->ecdh_key) {
+                    s->fbuf = strbuf_dup_nm(get_string(pktin));
+                } else {
+                    s->f = get_mp_ssh2(pktin);
+                }
                 data = get_string(pktin);
                 s->mic.value = (char *)data.ptr;
                 s->mic.length = data.len;
@@ -476,7 +499,16 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                  s->gss_stat == SSH_GSS_S_CONTINUE_NEEDED ||
                  !s->complete_rcvd);
 
-        {
+        if (s->ecdh_key) {
+            bool ok = ecdh_key_getkey(s->ecdh_key, ptrlen_from_strbuf(s->fbuf),
+                                      BinarySink_UPCAST(s->kex_shared_secret));
+            if (!ok) {
+                ssh_proto_error(s->ppl.ssh, "Received invalid elliptic curve "
+                                "point in GSSAPI ECDH reply");
+                *aborted = true;
+                return;
+            }
+        } else {
             const char *err = dh_validate_f(s->dh_ctx, s->f);
             if (err) {
                 ssh_proto_error(s->ppl.ssh, "GSSAPI reply failed "
@@ -484,8 +516,10 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                 *aborted = true;
                 return;
             }
+            mp_int *K = dh_find_K(s->dh_ctx, s->f);
+            put_mp_ssh2(s->kex_shared_secret, K);
+            mp_free(K);
         }
-        s->K = dh_find_K(s->dh_ctx, s->f);
 
         /* We assume everything from now on will be quick, and it might
          * involve user interaction. */
@@ -493,29 +527,42 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
 
         if (!s->hkey)
             put_stringz(s->exhash, "");
-        if (dh_is_gex(s->kex_alg)) {
-            /* min,  preferred, max */
-            put_uint32(s->exhash, s->pbits);
-            put_uint32(s->exhash, s->pbits);
-            put_uint32(s->exhash, s->pbits * 2);
 
-            put_mp_ssh2(s->exhash, s->p);
-            put_mp_ssh2(s->exhash, s->g);
+        if (s->ecdh_key) {
+            put_stringpl(s->exhash, ptrlen_from_strbuf(s->ebuf));
+            put_stringpl(s->exhash, ptrlen_from_strbuf(s->fbuf));
+        } else {
+            if (dh_is_gex(s->kex_alg)) {
+                /* min, preferred, max */
+                put_uint32(s->exhash, s->pbits);
+                put_uint32(s->exhash, s->pbits);
+                put_uint32(s->exhash, s->pbits * 2);
+
+                put_mp_ssh2(s->exhash, s->p);
+                put_mp_ssh2(s->exhash, s->g);
+            }
+            put_mp_ssh2(s->exhash, s->e);
+            put_mp_ssh2(s->exhash, s->f);
         }
-        put_mp_ssh2(s->exhash, s->e);
-        put_mp_ssh2(s->exhash, s->f);
 
         /*
          * MIC verification is done below, after we compute the hash
          * used as the MIC input.
          */
 
-        dh_cleanup(s->dh_ctx);
-        s->dh_ctx = NULL;
-        mp_free(s->f); s->f = NULL;
-        if (dh_is_gex(s->kex_alg)) {
-            mp_free(s->g); s->g = NULL;
-            mp_free(s->p); s->p = NULL;
+        if (s->ecdh_key) {
+            ecdh_key_free(s->ecdh_key);
+            s->ecdh_key = NULL;
+            strbuf_free(s->ebuf); s->ebuf = NULL;
+            strbuf_free(s->fbuf); s->fbuf = NULL;
+        } else {
+            dh_cleanup(s->dh_ctx);
+            s->dh_ctx = NULL;
+            mp_free(s->f); s->f = NULL;
+            if (dh_is_gex(s->kex_alg)) {
+                mp_free(s->g); s->g = NULL;
+                mp_free(s->p); s->p = NULL;
+            }
         }
 #endif
     } else {
@@ -584,15 +631,21 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
             strbuf *buf, *outstr;
 
             mp_int *tmp = mp_random_bits(nbits - 1);
-            s->K = mp_power_2(nbits - 1);
-            mp_add_into(s->K, s->K, tmp);
+            mp_int *K = mp_power_2(nbits - 1);
+            mp_add_into(K, K, tmp);
             mp_free(tmp);
 
             /*
              * Encode this as an mpint.
              */
             buf = strbuf_new_nm();
-            put_mp_ssh2(buf, s->K);
+            put_mp_ssh2(buf, K);
+
+            /*
+             * Store a copy as the output shared secret from the kex.
+             */
+            put_mp_ssh2(s->kex_shared_secret, K);
+            mp_free(K);
 
             /*
              * Encrypt it with the given RSA key.
@@ -639,7 +692,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
     ssh2transport_finalise_exhash(s);
 
 #ifndef NO_GSSAPI
-    if (s->kex_alg->main_type == KEXTYPE_GSS) {
+    if (kex_is_gss(s->kex_alg)) {
         Ssh_gss_buf gss_buf;
         SSH_GSS_CLEAR_BUF(&s->gss_buf);
 
@@ -668,7 +721,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
          * If this the first KEX, save the GSS context for "gssapi-keyex"
          * authentication.
          *
-         * http://tools.ietf.org/html/rfc4462#section-4
+         * https://www.rfc-editor.org/rfc/rfc4462#section-4
          *
          * This method may be used only if the initial key exchange was
          * performed using a GSS-API-based key exchange method defined in
@@ -687,7 +740,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
     s->dh_ctx = NULL;
 
     /* In GSS keyex there's no hostkey signature to verify */
-    if (s->kex_alg->main_type != KEXTYPE_GSS) {
+    if (!kex_is_gss(s->kex_alg)) {
         if (!s->hkey) {
             ssh_proto_error(s->ppl.ssh, "Server's host key is invalid");
             *aborted = true;
@@ -706,21 +759,21 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
         }
     }
 
-    s->keystr = (s->hkey ? ssh_key_cache_str(s->hkey) : NULL);
+    s->keystr = s->hkey ? ssh_key_cache_str(s->hkey) : NULL;
 #ifndef NO_GSSAPI
     if (s->gss_kex_used) {
         /*
          * In a GSS-based session, check the host key (if any) against
          * the transient host key cache.
          */
-        if (s->kex_alg->main_type == KEXTYPE_GSS) {
+        if (kex_is_gss(s->kex_alg)) {
 
             /*
              * We've just done a GSS key exchange. If it gave us a
              * host key, store it.
              */
             if (s->hkey) {
-                char *fingerprint = ssh2_fingerprint(
+                char *fingerprint = ssh2_double_fingerprint(
                     s->hkey, SSH_FPTYPE_DEFAULT);
                 ppl_logevent("GSS kex provided fallback host key:");
                 ppl_logevent("%s", fingerprint);
@@ -778,8 +831,9 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
              * An exception is if this was the non-GSS key exchange we
              * triggered on purpose to populate the transient cache.
              */
-            assert(s->hkey);  /* only KEXTYPE_GSS lets this be null */
-            char *fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT);
+            assert(s->hkey);  /* only KEXTYPE_GSS* lets this be null */
+            char *fingerprint = ssh2_double_fingerprint(
+                s->hkey, SSH_FPTYPE_DEFAULT);
 
             if (s->need_gss_transient_hostkey) {
                 ppl_logevent("Post-GSS rekey provided fallback host key:");
@@ -839,49 +893,130 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                 }
             }
 
+            ssh2_userkey uk = { .key = s->hkey, .comment = NULL };
+            char **fingerprints = ssh2_all_fingerprints(s->hkey);
+
+            FingerprintType fptype_default =
+                ssh2_pick_default_fingerprint(fingerprints);
+            ppl_logevent("Host key fingerprint is:");
+            ppl_logevent("%s", fingerprints[fptype_default]);
+
             /*
-             * Authenticate remote host: verify host key. (We've already
-             * checked the signature of the exchange hash.)
+             * Authenticate remote host: verify host key, either by
+             * certification or by the local host key cache.
+             *
+             * (We've already checked the signature of the exchange
+             * hash.)
              */
+            if (ssh_key_alg(s->hkey)->is_certificate) {
+                char *base_fp = ssh2_fingerprint(
+                    s->hkey, ssh_fptype_to_cert(fptype_default));
+                ppl_logevent("Host key is a certificate. "
+                             "Hash including certificate:");
+                ppl_logevent("%s", base_fp);
+                sfree(base_fp);
+
+                strbuf *id_string = strbuf_new();
+                StripCtrlChars *id_string_scc = stripctrl_new(
+                    BinarySink_UPCAST(id_string), false, L'\0');
+                ssh_key_cert_id_string(
+                    s->hkey, BinarySink_UPCAST(id_string_scc));
+                stripctrl_free(id_string_scc);
+                ppl_logevent("Certificate ID string is \"%s\"", id_string->s);
+                strbuf_free(id_string);
+
+                strbuf *ca_pub = strbuf_new();
+                ssh_key_ca_public_blob(s->hkey, BinarySink_UPCAST(ca_pub));
+                host_ca hca_search = { .ca_public_key = ca_pub };
+                host_ca *hca_found = find234(s->host_cas, &hca_search, NULL);
+
+                char *ca_fp = ssh2_fingerprint_blob(ptrlen_from_strbuf(ca_pub),
+                                                    fptype_default);
+                ppl_logevent("Fingerprint of certification authority:");
+                ppl_logevent("%s", ca_fp);
+                sfree(ca_fp);
+
+                strbuf_free(ca_pub);
+
+                strbuf *error = strbuf_new();
+                bool cert_ok = false;
+
+                if (!hca_found) {
+                    put_fmt(error, "Certification authority is not trusted");
+                } else {
+                    ppl_logevent("Certification authority matches '%s'",
+                                 hca_found->name);
+                    cert_ok = ssh_key_check_cert(
+                        s->hkey,
+                        true, /* host certificate */
+                        ptrlen_from_asciz(s->savedhost),
+                        time(NULL),
+                        &hca_found->opts,
+                        BinarySink_UPCAST(error));
+                }
+                if (cert_ok) {
+                    strbuf_free(error);
+                    ssh2_free_all_fingerprints(fingerprints);
+                    ppl_logevent("Accepted certificate");
+                    goto host_key_ok;
+                } else {
+                    ppl_logevent("Rejected host key certificate: %s",
+                                 error->s);
+                    strbuf_free(error);
+                    /* now fall through into normal host key checking */
+                }
+            }
+
             {
-                ssh2_userkey uk = { .key = s->hkey, .comment = NULL };
                 char *keydisp = ssh2_pubkey_openssh_str(&uk);
-                char **fingerprints = ssh2_all_fingerprints(s->hkey);
 
-                FingerprintType fptype_default =
-                    ssh2_pick_default_fingerprint(fingerprints);
-                ppl_logevent("Host key fingerprint is:");
-                ppl_logevent("%s", fingerprints[fptype_default]);
+                int ca_count = ssh_key_alg(s->hkey)->is_certificate ?
+                    count234(s->host_cas) : 0;
 
                 s->spr = verify_ssh_host_key(
                     ppl_get_iseat(&s->ppl), s->conf, s->savedhost, s->savedport,
                     s->hkey, ssh_key_cache_id(s->hkey), s->keystr, keydisp,
-                    fingerprints, ssh2_transport_dialog_callback, s);
+                    fingerprints, ca_count, ssh2_transport_dialog_callback, s);
 
                 ssh2_free_all_fingerprints(fingerprints);
                 sfree(keydisp);
-            }
 #ifdef FUZZING
-            s->spr = SPR_OK;
+                s->spr = SPR_OK;
 #endif
-            crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
-            if (spr_is_abort(s->spr)) {
-                *aborted = true;
-                ssh_spr_close(s->ppl.ssh, s->spr, "host key verification");
-                return;
+                crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
+                if (spr_is_abort(s->spr)) {
+                    *aborted = true;
+                    ssh_spr_close(s->ppl.ssh, s->spr, "host key verification");
+                    return;
+                }
+
+                if (ssh_key_alg(s->hkey)->is_certificate) {
+                    /*
+                     * Explain what's going on in the Event Log: if we
+                     * got here by way of a certified key whose
+                     * certificate we didn't like, then we should
+                     * explain why we chose to continue with the
+                     * connection anyway!
+                     */
+                    ppl_logevent("Accepting certified host key anyway based "
+                                 "on cache");
+                }
             }
 
+          host_key_ok:
+
             /*
              * Save this host key, to check against the one presented in
              * subsequent rekeys.
              */
-            s->hostkey_str = s->keystr;
-            s->keystr = NULL;
+            strbuf_clear(s->hostkeyblob);
+            ssh_key_public_blob(s->hkey, BinarySink_UPCAST(s->hostkeyblob));
         } else if (s->cross_certifying) {
             assert(s->hkey);
             assert(ssh_key_alg(s->hkey) == s->cross_certifying);
 
-            char *fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT);
+            char *fingerprint = ssh2_double_fingerprint(
+                s->hkey, SSH_FPTYPE_DEFAULT);
             ppl_logevent("Storing additional host key for this host:");
             ppl_logevent("%s", fingerprint);
             sfree(fingerprint);
@@ -892,8 +1027,8 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
              * Don't forget to store the new key as the one we'll be
              * re-checking in future normal rekeys.
              */
-            s->hostkey_str = s->keystr;
-            s->keystr = NULL;
+            strbuf_clear(s->hostkeyblob);
+            ssh_key_public_blob(s->hkey, BinarySink_UPCAST(s->hostkeyblob));
         } else {
             /*
              * In a rekey, we never present an interactive host key
@@ -901,8 +1036,12 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
              * enforce that the key we're seeing this time is identical to
              * the one we saw before.
              */
-            assert(s->keystr);         /* filled in by prior key exchange */
-            if (strcmp(s->hostkey_str, s->keystr)) {
+            strbuf *thisblob = strbuf_new();
+            ssh_key_public_blob(s->hkey, BinarySink_UPCAST(thisblob));
+            bool match = ptrlen_eq_ptrlen(ptrlen_from_strbuf(thisblob),
+                                          ptrlen_from_strbuf(s->hostkeyblob));
+            strbuf_free(thisblob);
+            if (!match) {
 #ifndef FUZZING
                 ssh_sw_abort(s->ppl.ssh,
                              "Host key was different in repeat key exchange");

+ 1 - 1
source/putty/ssh/mainchan.c

@@ -344,7 +344,7 @@ static void mainchan_open_failure(Channel *chan, const char *errtext)
 }
 
 static size_t mainchan_send(Channel *chan, bool is_stderr,
-                         const void *data, size_t length)
+                            const void *data, size_t length)
 {
     assert(chan->vt == &mainchan_channelvt);
     mainchan *mc = container_of(chan, mainchan, chan);

+ 69 - 39
source/putty/ssh/pgssapi.c

@@ -9,38 +9,63 @@
 
 #ifndef NO_LIBDL
 
-/* Reserved static storage for GSS_oids.  Comments are quotes from RFC 2744. */
-static const gss_OID_desc oids[] = {
+/* Reserved static storage for GSS_oids.
+ * Constants of the form GSS_C_NT_* are specified by rfc 2744.
+ * Comments are quotes from RFC 2744 itself.
+ *
+ * These may be #defined to complex expressions by the local header
+ * file, if we're including one in static-GSSAPI mode. (For example,
+ * Heimdal defines them to things like
+ * (&__gss_c_nt_user_name_oid_desc).) So we only define them if
+ * needed. */
+
+#ifndef GSS_C_NT_USER_NAME
+static gss_OID_desc oid_GSS_C_NT_USER_NAME = {
     /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-    {10, (void *)"\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01"},
+    10, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01",
     /* corresponding to an object-identifier value of
      * {iso(1) member-body(2) United States(840) mit(113554)
      * infosys(1) gssapi(2) generic(1) user_name(1)}.  The constant
      * GSS_C_NT_USER_NAME should be initialized to point
-     * to that gss_OID_desc.
+     * to that gss_OID_desc. */
+};
+const_gss_OID GSS_C_NT_USER_NAME = &oid_GSS_C_NT_USER_NAME;
+#endif
 
-     * The implementation must reserve static storage for a
+#ifndef GSS_C_NT_MACHINE_UID_NAME
+static gss_OID_desc oid_GSS_C_NT_MACHINE_UID_NAME = {
+    /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-    {10, (void *)"\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x02"},
+    10, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x02",
     /* corresponding to an object-identifier value of
      * {iso(1) member-body(2) United States(840) mit(113554)
      * infosys(1) gssapi(2) generic(1) machine_uid_name(2)}.
      * The constant GSS_C_NT_MACHINE_UID_NAME should be
-     * initialized to point to that gss_OID_desc.
+     * initialized to point to that gss_OID_desc. */
+};
+const_gss_OID GSS_C_NT_MACHINE_UID_NAME = &oid_GSS_C_NT_MACHINE_UID_NAME;
+#endif
 
-     * The implementation must reserve static storage for a
+#ifndef GSS_C_NT_STRING_UID_NAME
+static gss_OID_desc oid_GSS_C_NT_STRING_UID_NAME = {
+    /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-    {10, (void *)"\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x03"},
+    10, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x03",
     /* corresponding to an object-identifier value of
      * {iso(1) member-body(2) United States(840) mit(113554)
      * infosys(1) gssapi(2) generic(1) string_uid_name(3)}.
      * The constant GSS_C_NT_STRING_UID_NAME should be
-     * initialized to point to that gss_OID_desc.
-     *
-     * The implementation must reserve static storage for a
+     * initialized to point to that gss_OID_desc. */
+};
+const_gss_OID GSS_C_NT_STRING_UID_NAME = &oid_GSS_C_NT_STRING_UID_NAME;
+#endif
+
+#ifndef GSS_C_NT_HOSTBASED_SERVICE_X
+static gss_OID_desc oid_GSS_C_NT_HOSTBASED_SERVICE_X = {
+    /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-    {6, (void *)"\x2b\x06\x01\x05\x06\x02"},
+    6, "\x2b\x06\x01\x05\x06\x02",
     /* corresponding to an object-identifier value of
      * {iso(1) org(3) dod(6) internet(1) security(5)
      * nametypes(6) gss-host-based-services(2))}.  The constant
@@ -52,53 +77,58 @@ static const gss_OID_desc oids[] = {
      * GSS_C_NT_HOSTBASED_SERVICE_X should be accepted a synonym
      * for GSS_C_NT_HOSTBASED_SERVICE when presented as an input
      * parameter, but should not be emitted by GSS-API
-     * implementations
-     *
-     * The implementation must reserve static storage for a
+     * implementations */
+};
+const_gss_OID GSS_C_NT_HOSTBASED_SERVICE_X = &oid_GSS_C_NT_HOSTBASED_SERVICE_X;
+#endif
+
+#ifndef GSS_C_NT_HOSTBASED_SERVICE
+static gss_OID_desc oid_GSS_C_NT_HOSTBASED_SERVICE = {
+    /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-     {10, (void *)"\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x04"},
+    10, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x04",
     /* corresponding to an object-identifier value of {iso(1)
      * member-body(2) Unites States(840) mit(113554) infosys(1)
      * gssapi(2) generic(1) service_name(4)}.  The constant
      * GSS_C_NT_HOSTBASED_SERVICE should be initialized
-     * to point to that gss_OID_desc.
-     *
-     * The implementation must reserve static storage for a
+     * to point to that gss_OID_desc. */
+};
+const_gss_OID GSS_C_NT_HOSTBASED_SERVICE = &oid_GSS_C_NT_HOSTBASED_SERVICE;
+#endif
+
+#ifndef GSS_C_NT_ANONYMOUS
+static gss_OID_desc oid_GSS_C_NT_ANONYMOUS = {
+    /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-    {6, (void *)"\x2b\x06\01\x05\x06\x03"},
+    6, "\x2b\x06\01\x05\x06\x03",
     /* corresponding to an object identifier value of
      * {1(iso), 3(org), 6(dod), 1(internet), 5(security),
      * 6(nametypes), 3(gss-anonymous-name)}.  The constant
      * and GSS_C_NT_ANONYMOUS should be initialized to point
-     * to that gss_OID_desc.
-     *
-     * The implementation must reserve static storage for a
+     * to that gss_OID_desc. */
+};
+const_gss_OID GSS_C_NT_ANONYMOUS = &oid_GSS_C_NT_ANONYMOUS;
+#endif
+
+#ifndef GSS_C_NT_EXPORT_NAME
+static gss_OID_desc oid_GSS_C_NT_EXPORT_NAME = {
+    /* The implementation must reserve static storage for a
      * gss_OID_desc object containing the value */
-    {6, (void *)"\x2b\x06\x01\x05\x06\x04"},
-     /* corresponding to an object-identifier value of
+    6, "\x2b\x06\x01\x05\x06\x04",
+    /* corresponding to an object-identifier value of
      * {1(iso), 3(org), 6(dod), 1(internet), 5(security),
      * 6(nametypes), 4(gss-api-exported-name)}.  The constant
      * GSS_C_NT_EXPORT_NAME should be initialized to point
      * to that gss_OID_desc.
      */
 };
-
-/* Here are the constants which point to the static structure above.
- *
- * Constants of the form GSS_C_NT_* are specified by rfc 2744.
- */
-const_gss_OID GSS_C_NT_USER_NAME           = oids+0;
-const_gss_OID GSS_C_NT_MACHINE_UID_NAME    = oids+1;
-const_gss_OID GSS_C_NT_STRING_UID_NAME     = oids+2;
-const_gss_OID GSS_C_NT_HOSTBASED_SERVICE_X = oids+3;
-const_gss_OID GSS_C_NT_HOSTBASED_SERVICE   = oids+4;
-const_gss_OID GSS_C_NT_ANONYMOUS           = oids+5;
-const_gss_OID GSS_C_NT_EXPORT_NAME         = oids+6;
+const_gss_OID GSS_C_NT_EXPORT_NAME = &oid_GSS_C_NT_EXPORT_NAME;
+#endif
 
 #endif /* NO_LIBDL */
 
 static gss_OID_desc gss_mech_krb5_desc =
-{ 9, (void *)"\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" };
+{ 9, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" };
 /* iso(1) member-body(2) United States(840) mit(113554) infosys(1) gssapi(2) krb5(2)*/
 const gss_OID GSS_MECH_KRB5 = &gss_mech_krb5_desc;
 

+ 3 - 3
source/putty/ssh/pgssapi.h

@@ -53,9 +53,9 @@ typedef struct gss_channel_bindings_struct {
     gss_buffer_desc application_data;
 } *gss_channel_bindings_t;
 
-typedef void * gss_ctx_id_t;
-typedef void * gss_name_t;
-typedef void * gss_cred_id_t;
+typedef void *gss_ctx_id_t;
+typedef void *gss_name_t;
+typedef void *gss_cred_id_t;
 
 typedef OM_uint32 gss_qop_t;
 typedef int gss_cred_usage_t;

+ 1 - 1
source/putty/ssh/portfwd.c

@@ -514,7 +514,7 @@ void portfwd_raw_setup(Channel *pfchan, Socket *s, SshChannel *sc)
 }
 
 /*
- called when someone connects to the local port
+ * called when someone connects to the local port
  */
 
 static int pfl_accepting(Plug *p, accept_fn_t constructor, accept_ctx_t ctx)

+ 6 - 5
source/putty/ssh/ppl.h

@@ -109,12 +109,13 @@ PacketProtocolLayer *ssh2_transport_new(
     const SshServerConfig *ssc);
 PacketProtocolLayer *ssh2_userauth_new(
     PacketProtocolLayer *successor_layer,
-    const char *hostname, const char *fullhostname,
-    Filename *keyfile, bool show_banner, bool tryagent, bool notrivialauth,
+    const char *hostname, int port, const char *fullhostname,
+    Filename *keyfile, Filename *detached_cert,
+    bool show_banner, bool tryagent, bool notrivialauth,
     const char *default_username, bool change_username,
-    bool try_ki_auth,
-    bool try_gssapi_auth, bool try_gssapi_kex_auth,
-    bool gssapi_fwd, struct ssh_connection_shared_gss_state *shgss);
+    bool try_ki_auth, bool try_gssapi_auth, bool try_gssapi_kex_auth,
+    bool gssapi_fwd, struct ssh_connection_shared_gss_state *shgss,
+    const char *auth_plugin);
 PacketProtocolLayer *ssh2_connection_new(
     Ssh *ssh, ssh_sharing_state *connshare, bool is_simple,
     Conf *conf, const char *peer_verstring, bufchain *user_input,

+ 40 - 43
source/putty/ssh/sharing.c

@@ -530,8 +530,8 @@ void sharestate_free(ssh_sharing_state *sharestate)
     sfree(sharestate);
 }
 
-static struct share_halfchannel *share_add_halfchannel
-    (struct ssh_sharing_connstate *cs, unsigned server_id)
+static struct share_halfchannel *share_add_halfchannel(
+    struct ssh_sharing_connstate *cs, unsigned server_id)
 {
     struct share_halfchannel *hc = snew(struct share_halfchannel);
     hc->server_id = server_id;
@@ -544,8 +544,8 @@ static struct share_halfchannel *share_add_halfchannel
     }
 }
 
-static struct share_halfchannel *share_find_halfchannel
-    (struct ssh_sharing_connstate *cs, unsigned server_id)
+static struct share_halfchannel *share_find_halfchannel(
+    struct ssh_sharing_connstate *cs, unsigned server_id)
 {
     struct share_halfchannel dummyhc;
     dummyhc.server_id = server_id;
@@ -559,9 +559,9 @@ static void share_remove_halfchannel(struct ssh_sharing_connstate *cs,
     sfree(hc);
 }
 
-static struct share_channel *share_add_channel
-    (struct ssh_sharing_connstate *cs, unsigned downstream_id,
-     unsigned upstream_id, unsigned server_id, int state, int maxpkt)
+static struct share_channel *share_add_channel(
+    struct ssh_sharing_connstate *cs, unsigned downstream_id,
+    unsigned upstream_id, unsigned server_id, int state, int maxpkt)
 {
     struct share_channel *chan = snew(struct share_channel);
     chan->downstream_id = downstream_id;
@@ -598,16 +598,16 @@ static void share_channel_set_server_id(struct ssh_sharing_connstate *cs,
     add234(cs->channels_by_server, chan);
 }
 
-static struct share_channel *share_find_channel_by_upstream
-    (struct ssh_sharing_connstate *cs, unsigned upstream_id)
+static struct share_channel *share_find_channel_by_upstream(
+    struct ssh_sharing_connstate *cs, unsigned upstream_id)
 {
     struct share_channel dummychan;
     dummychan.upstream_id = upstream_id;
     return find234(cs->channels_by_us, &dummychan, NULL);
 }
 
-static struct share_channel *share_find_channel_by_server
-    (struct ssh_sharing_connstate *cs, unsigned server_id)
+static struct share_channel *share_find_channel_by_server(
+    struct ssh_sharing_connstate *cs, unsigned server_id)
 {
     struct share_channel dummychan;
     dummychan.server_id = server_id;
@@ -626,9 +626,8 @@ static void share_remove_channel(struct ssh_sharing_connstate *cs,
     sfree(chan);
 }
 
-static struct share_xchannel *share_add_xchannel
-    (struct ssh_sharing_connstate *cs,
-     unsigned upstream_id, unsigned server_id)
+static struct share_xchannel *share_add_xchannel(
+    struct ssh_sharing_connstate *cs, unsigned upstream_id, unsigned server_id)
 {
     struct share_xchannel *xc = snew(struct share_xchannel);
     xc->upstream_id = upstream_id;
@@ -647,16 +646,16 @@ static struct share_xchannel *share_add_xchannel
     return xc;
 }
 
-static struct share_xchannel *share_find_xchannel_by_upstream
-    (struct ssh_sharing_connstate *cs, unsigned upstream_id)
+static struct share_xchannel *share_find_xchannel_by_upstream(
+    struct ssh_sharing_connstate *cs, unsigned upstream_id)
 {
     struct share_xchannel dummyxc;
     dummyxc.upstream_id = upstream_id;
     return find234(cs->xchannels_by_us, &dummyxc, NULL);
 }
 
-static struct share_xchannel *share_find_xchannel_by_server
-    (struct ssh_sharing_connstate *cs, unsigned server_id)
+static struct share_xchannel *share_find_xchannel_by_server(
+    struct ssh_sharing_connstate *cs, unsigned server_id)
 {
     struct share_xchannel dummyxc;
     dummyxc.server_id = server_id;
@@ -664,16 +663,15 @@ static struct share_xchannel *share_find_xchannel_by_server
 }
 
 static void share_remove_xchannel(struct ssh_sharing_connstate *cs,
-                                 struct share_xchannel *xc)
+                                  struct share_xchannel *xc)
 {
     del234(cs->xchannels_by_us, xc);
     del234(cs->xchannels_by_server, xc);
     share_xchannel_free(xc);
 }
 
-static struct share_forwarding *share_add_forwarding
-    (struct ssh_sharing_connstate *cs,
-     const char *host, int port)
+static struct share_forwarding *share_add_forwarding(
+    struct ssh_sharing_connstate *cs, const char *host, int port)
 {
     struct share_forwarding *fwd = snew(struct share_forwarding);
     fwd->host = dupstr(host);
@@ -687,8 +685,8 @@ static struct share_forwarding *share_add_forwarding
     return fwd;
 }
 
-static struct share_forwarding *share_find_forwarding
-    (struct ssh_sharing_connstate *cs, const char *host, int port)
+static struct share_forwarding *share_find_forwarding(
+    struct ssh_sharing_connstate *cs, const char *host, int port)
 {
     struct share_forwarding dummyfwd, *ret;
     dummyfwd.host = dupstr(host);
@@ -989,8 +987,8 @@ static void share_xchannel_add_message(
     xc->msgtail = msg;
 }
 
-void share_dead_xchannel_respond(struct ssh_sharing_connstate *cs,
-                                 struct share_xchannel *xc)
+static void share_dead_xchannel_respond(struct ssh_sharing_connstate *cs,
+                                        struct share_xchannel *xc)
 {
     /*
      * Handle queued incoming messages from the server destined for an
@@ -1013,10 +1011,10 @@ void share_dead_xchannel_respond(struct ssh_sharing_connstate *cs,
             if (get_bool(src)) {
                 strbuf *packet = strbuf_new();
                 put_uint32(packet, xc->server_id);
-                ssh_send_packet_from_downstream
-                    (cs->parent->cl, cs->id, SSH2_MSG_CHANNEL_FAILURE,
-                     packet->s, packet->len,
-                     "downstream refused X channel open");
+                ssh_send_packet_from_downstream(
+                    cs->parent->cl, cs->id, SSH2_MSG_CHANNEL_FAILURE,
+                    packet->s, packet->len,
+                    "downstream refused X channel open");
                 strbuf_free(packet);
             }
         } else if (msg->type == SSH2_MSG_CHANNEL_CLOSE) {
@@ -1035,10 +1033,9 @@ void share_dead_xchannel_respond(struct ssh_sharing_connstate *cs,
     }
 }
 
-void share_xchannel_confirmation(struct ssh_sharing_connstate *cs,
-                                 struct share_xchannel *xc,
-                                 struct share_channel *chan,
-                                 unsigned downstream_window)
+static void share_xchannel_confirmation(
+    struct ssh_sharing_connstate *cs, struct share_xchannel *xc,
+    struct share_channel *chan, unsigned downstream_window)
 {
     strbuf *packet;
 
@@ -1072,8 +1069,8 @@ void share_xchannel_confirmation(struct ssh_sharing_connstate *cs,
     strbuf_free(packet);
 }
 
-void share_xchannel_failure(struct ssh_sharing_connstate *cs,
-                            struct share_xchannel *xc)
+static void share_xchannel_failure(struct ssh_sharing_connstate *cs,
+                                   struct share_xchannel *xc)
 {
     /*
      * If downstream refuses to open our X channel at all for some
@@ -1380,9 +1377,9 @@ static void share_got_pkt_from_downstream(struct ssh_sharing_connstate *cs,
                  * cleaned up if downstream goes away.
                  */
                 pkt[wantreplypos] = 1;
-                ssh_send_packet_from_downstream
-                    (cs->parent->cl, cs->id, type, pkt, pktlen,
-                     orig_wantreply ? NULL : "upstream added want_reply flag");
+                ssh_send_packet_from_downstream(
+                    cs->parent->cl, cs->id, type, pkt, pktlen,
+                    orig_wantreply ? NULL : "upstream added want_reply flag");
                 fwd = share_add_forwarding(cs, host, port);
                 ssh_sharing_queue_global_request(cs->parent->cl, cs);
 
@@ -1443,9 +1440,9 @@ static void share_got_pkt_from_downstream(struct ssh_sharing_connstate *cs,
                  * deleted even if downstream doesn't want to know.
                  */
                 pkt[wantreplypos] = 1;
-                ssh_send_packet_from_downstream
-                    (cs->parent->cl, cs->id, type, pkt, pktlen,
-                     orig_wantreply ? NULL : "upstream added want_reply flag");
+                ssh_send_packet_from_downstream(
+                    cs->parent->cl, cs->id, type, pkt, pktlen,
+                    orig_wantreply ? NULL : "upstream added want_reply flag");
                 ssh_sharing_queue_global_request(cs->parent->cl, cs);
 
                 /*
@@ -1988,7 +1985,7 @@ static int share_listen_accepting(Plug *plug,
  * configurations which return the same string from this function will
  * be treated as potentially shareable with each other.
  */
-char *ssh_share_sockname(const char *host, int port, Conf *conf)
+static char *ssh_share_sockname(const char *host, int port, Conf *conf)
 {
     char *username = NULL;
     char *sockname;

+ 6 - 4
source/putty/ssh/ssh.c

@@ -253,8 +253,10 @@ static void ssh_got_ssh_version(struct ssh_version_receiver *rcv,
                 char *username = get_remote_username(ssh->conf);
 
                 userauth_layer = ssh2_userauth_new(
-                    connection_layer, ssh->savedhost, ssh->fullhostname,
+                    connection_layer, ssh->savedhost, ssh->savedport,
+                    ssh->fullhostname,
                     conf_get_filename(ssh->conf, CONF_keyfile),
+                    conf_get_filename(ssh->conf, CONF_detached_cert),
                     conf_get_bool(ssh->conf, CONF_ssh_show_banner),
                     conf_get_bool(ssh->conf, CONF_tryagent),
                     conf_get_bool(ssh->conf, CONF_ssh_no_trivial_userauth),
@@ -265,14 +267,14 @@ static void ssh_got_ssh_version(struct ssh_version_receiver *rcv,
                     conf_get_bool(ssh->conf, CONF_try_gssapi_auth),
                     conf_get_bool(ssh->conf, CONF_try_gssapi_kex),
                     conf_get_bool(ssh->conf, CONF_gssapifwd),
-                    &ssh->gss_state
+                    &ssh->gss_state,
 #else
                     false,
                     false,
                     false,
-                    NULL
+                    NULL,
 #endif
-                    );
+                    conf_get_str(ssh->conf, CONF_auth_plugin));
                 ssh_connect_ppl(ssh, userauth_layer);
                 transport_child_layer = userauth_layer;
 

+ 338 - 92
source/putty/ssh/transport2.c

@@ -113,6 +113,7 @@ static const char *const kexlist_descr[NKEXLIST] = {
 };
 
 static int weak_algorithm_compare(void *av, void *bv);
+static int ca_blob_compare(void *av, void *bv);
 
 PacketProtocolLayer *ssh2_transport_new(
     Conf *conf, const char *host, int port, const char *fullhostname,
@@ -134,6 +135,7 @@ PacketProtocolLayer *ssh2_transport_new(
     s->server_greeting = dupstr(server_greeting);
     s->stats = stats;
     s->hostkeyblob = strbuf_new();
+    s->host_cas = newtree234(ca_blob_compare);
 
     pq_in_init(&s->pq_in_higher);
     pq_out_init(&s->pq_out_higher);
@@ -210,16 +212,25 @@ static void ssh2_transport_free(PacketProtocolLayer *ppl)
     sfree(s->client_greeting);
     sfree(s->server_greeting);
     sfree(s->keystr);
-    sfree(s->hostkey_str);
     strbuf_free(s->hostkeyblob);
+    {
+        host_ca *hca;
+        while ( (hca = delpos234(s->host_cas, 0)) )
+            host_ca_free(hca);
+        freetree234(s->host_cas);
+    }
     if (s->hkey && !s->hostkeys) {
         ssh_key_free(s->hkey);
         s->hkey = NULL;
     }
+    for (size_t i = 0; i < NKEXLIST; i++)
+        sfree(s->kexlists[i].algs);
     if (s->f) mp_free(s->f);
     if (s->p) mp_free(s->p);
     if (s->g) mp_free(s->g);
-    if (s->K) mp_free(s->K);
+    if (s->ebuf) strbuf_free(s->ebuf);
+    if (s->fbuf) strbuf_free(s->fbuf);
+    if (s->kex_shared_secret) strbuf_free(s->kex_shared_secret);
     if (s->dh_ctx)
         dh_cleanup(s->dh_ctx);
     if (s->rsa_kex_key_needs_freeing) {
@@ -227,7 +238,7 @@ static void ssh2_transport_free(PacketProtocolLayer *ppl)
         sfree(s->rsa_kex_key);
     }
     if (s->ecdh_key)
-        ssh_ecdhkex_freekey(s->ecdh_key);
+        ecdh_key_free(s->ecdh_key);
     if (s->exhash)
         ssh_hash_free(s->exhash);
     strbuf_free(s->outgoing_kexinit);
@@ -245,7 +256,7 @@ static void ssh2_transport_free(PacketProtocolLayer *ppl)
  */
 static void ssh2_mkkey(
     struct ssh2_transport_state *s, strbuf *out,
-    mp_int *K, unsigned char *H, char chr, int keylen)
+    strbuf *kex_shared_secret, unsigned char *H, char chr, int keylen)
 {
     int hlen = s->kex_alg->hash->hlen;
     int keylen_padded;
@@ -273,7 +284,7 @@ static void ssh2_mkkey(
     /* First hlen bytes. */
     h = ssh_hash_new(s->kex_alg->hash);
     if (!(s->ppl.remote_bugs & BUG_SSH2_DERIVEKEY))
-        put_mp_ssh2(h, K);
+        put_datapl(h, ptrlen_from_strbuf(kex_shared_secret));
     put_data(h, H, hlen);
     put_byte(h, chr);
     put_data(h, s->session_id, s->session_id_len);
@@ -285,7 +296,7 @@ static void ssh2_mkkey(
 
         ssh_hash_reset(h);
         if (!(s->ppl.remote_bugs & BUG_SSH2_DERIVEKEY))
-            put_mp_ssh2(h, K);
+            put_datapl(h, ptrlen_from_strbuf(kex_shared_secret));
         put_data(h, H, hlen);
 
         for (offset = hlen; offset < keylen_padded; offset += hlen) {
@@ -302,22 +313,27 @@ static void ssh2_mkkey(
  * Find a slot in a KEXINIT algorithm list to use for a new algorithm.
  * If the algorithm is already in the list, return a pointer to its
  * entry, otherwise return an entry from the end of the list.
- * This assumes that every time a particular name is passed in, it
- * comes from the same string constant.  If this isn't true, this
- * function may need to be rewritten to use strcmp() instead.
+ *
+ * 'name' is expected to be a ptrlen which it's safe to keep a copy
+ * of.
  */
-static struct kexinit_algorithm *ssh2_kexinit_addalg(struct kexinit_algorithm
-                                                     *list, const char *name)
+static struct kexinit_algorithm *ssh2_kexinit_addalg_pl(
+    struct kexinit_algorithm_list *list, ptrlen name)
 {
-    int i;
-
-    for (i = 0; i < MAXKEXLIST; i++)
-        if (list[i].name == NULL || list[i].name == name) {
-            list[i].name = name;
-            return &list[i];
-        }
+    for (size_t i = 0; i < list->nalgs; i++)
+        if (ptrlen_eq_ptrlen(list->algs[i].name, name))
+            return &list->algs[i];
+
+    sgrowarray(list->algs, list->algsize, list->nalgs);
+    struct kexinit_algorithm *entry = &list->algs[list->nalgs++];
+    entry->name = name;
+    return entry;
+}
 
-    unreachable("Should never run out of space in KEXINIT list");
+static struct kexinit_algorithm *ssh2_kexinit_addalg(
+    struct kexinit_algorithm_list *list, const char *name)
+{
+    return ssh2_kexinit_addalg_pl(list, ptrlen_from_asciz(name));
 }
 
 bool ssh2_common_filter_queue(PacketProtocolLayer *ppl)
@@ -483,10 +499,10 @@ PktIn *ssh2_transport_pop(struct ssh2_transport_state *s)
 
 static void ssh2_write_kexinit_lists(
     BinarySink *pktout,
-    struct kexinit_algorithm kexlists[NKEXLIST][MAXKEXLIST],
+    struct kexinit_algorithm_list kexlists[NKEXLIST],
     Conf *conf, const SshServerConfig *ssc, int remote_bugs,
     const char *hk_host, int hk_port, const ssh_keyalg *hk_prev,
-    ssh_transient_hostkey_cache *thc,
+    ssh_transient_hostkey_cache *thc, tree234 *host_cas,
     ssh_key *const *our_hostkeys, int our_nhostkeys,
     bool first_time, bool can_gssapi_keyex, bool transient_hostkey_mode)
 {
@@ -494,7 +510,7 @@ static void ssh2_write_kexinit_lists(
     bool warn;
 
     int n_preferred_kex;
-    const ssh_kexes *preferred_kex[KEX_MAX + 1]; /* +1 for GSSAPI */
+    const ssh_kexes *preferred_kex[KEX_MAX + 3]; /* +3 for GSSAPI */
     int n_preferred_hk;
     int preferred_hk[HK_MAX];
     int n_preferred_ciphers;
@@ -509,14 +525,33 @@ static void ssh2_write_kexinit_lists(
      * Set up the preferred key exchange. (NULL => warn below here)
      */
     n_preferred_kex = 0;
-    if (can_gssapi_keyex)
+    if (can_gssapi_keyex) {
+        preferred_kex[n_preferred_kex++] = &ssh_gssk5_ecdh_kex;
+        preferred_kex[n_preferred_kex++] = &ssh_gssk5_sha2_kex;
         preferred_kex[n_preferred_kex++] = &ssh_gssk5_sha1_kex;
+    }
     for (i = 0; i < KEX_MAX; i++) {
         switch (conf_get_int_int(conf, CONF_ssh_kexlist, i)) {
           case KEX_DHGEX:
             preferred_kex[n_preferred_kex++] =
                 &ssh_diffiehellman_gex;
             break;
+          case KEX_DHGROUP18:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_diffiehellman_group18;
+            break;
+          case KEX_DHGROUP17:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_diffiehellman_group17;
+            break;
+          case KEX_DHGROUP16:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_diffiehellman_group16;
+            break;
+          case KEX_DHGROUP15:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_diffiehellman_group15;
+            break;
           case KEX_DHGROUP14:
             preferred_kex[n_preferred_kex++] =
                 &ssh_diffiehellman_group14;
@@ -533,6 +568,10 @@ static void ssh2_write_kexinit_lists(
             preferred_kex[n_preferred_kex++] =
                 &ssh_ecdh_kex;
             break;
+          case KEX_NTRU_HYBRID:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_ntru_hybrid_kex;
+            break;
           case KEX_WARN:
             /* Flag for later. Don't bother if it's the last in
              * the list. */
@@ -582,6 +621,9 @@ static void ssh2_write_kexinit_lists(
           case CIPHER_CHACHA20:
             preferred_ciphers[n_preferred_ciphers++] = &ssh2_ccp;
             break;
+          case CIPHER_AESGCM:
+            preferred_ciphers[n_preferred_ciphers++] = &ssh2_aesgcm;
+            break;
           case CIPHER_WARN:
             /* Flag for later. Don't bother if it's the last in
              * the list. */
@@ -601,15 +643,14 @@ static void ssh2_write_kexinit_lists(
         preferred_comp = &ssh_comp_none;
 
     for (i = 0; i < NKEXLIST; i++)
-        for (j = 0; j < MAXKEXLIST; j++)
-            kexlists[i][j].name = NULL;
+        kexlists[i].nalgs = 0;
     /* List key exchange algorithms. */
     warn = false;
     for (i = 0; i < n_preferred_kex; i++) {
         const ssh_kexes *k = preferred_kex[i];
         if (!k) warn = true;
         else for (j = 0; j < k->nkexes; j++) {
-                alg = ssh2_kexinit_addalg(kexlists[KEXLIST_KEX],
+                alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_KEX],
                                           k->list[j]->name);
                 alg->u.kex.kex = k->list[j];
                 alg->u.kex.warn = warn;
@@ -624,23 +665,27 @@ static void ssh2_write_kexinit_lists(
         for (i = 0; i < our_nhostkeys; i++) {
             const ssh_keyalg *keyalg = ssh_key_alg(our_hostkeys[i]);
 
-            alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY],
+            alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY],
                                       keyalg->ssh_id);
             alg->u.hk.hostkey = keyalg;
             alg->u.hk.hkflags = 0;
             alg->u.hk.warn = false;
 
-            if (keyalg == &ssh_rsa) {
-                alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY],
-                                          "rsa-sha2-256");
-                alg->u.hk.hostkey = keyalg;
-                alg->u.hk.hkflags = SSH_AGENT_RSA_SHA2_256;
-                alg->u.hk.warn = false;
+            uint32_t supported_flags = ssh_keyalg_supported_flags(keyalg);
+            static const uint32_t try_flags[] = {
+                SSH_AGENT_RSA_SHA2_256,
+                SSH_AGENT_RSA_SHA2_512,
+            };
+            for (size_t i = 0; i < lenof(try_flags); i++) {
+                if (try_flags[i] & ~supported_flags)
+                    continue;          /* these flags not supported */
+
+                alg = ssh2_kexinit_addalg(
+                    &kexlists[KEXLIST_HOSTKEY],
+                    ssh_keyalg_alternate_ssh_id(keyalg, try_flags[i]));
 
-                alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY],
-                                          "rsa-sha2-512");
                 alg->u.hk.hostkey = keyalg;
-                alg->u.hk.hkflags = SSH_AGENT_RSA_SHA2_512;
+                alg->u.hk.hkflags = try_flags[i];
                 alg->u.hk.warn = false;
             }
         }
@@ -658,33 +703,91 @@ static void ssh2_write_kexinit_lists(
          * they surely _do_ want to be alerted that a server
          * they're actually connecting to is using it.
          */
+
+        bool accept_certs = false;
+        {
+            host_ca_enum *handle = enum_host_ca_start();
+            if (handle) {
+                strbuf *name = strbuf_new();
+                while (strbuf_clear(name), enum_host_ca_next(handle, name)) {
+                    host_ca *hca = host_ca_load(name->s);
+                    if (!hca)
+                        continue;
+
+                    if (hca->ca_public_key &&
+                        cert_expr_match_str(hca->validity_expression,
+                                            hk_host, hk_port)) {
+                        accept_certs = true;
+                        add234(host_cas, hca);
+                    } else {
+                        host_ca_free(hca);
+                    }
+                }
+                enum_host_ca_finish(handle);
+                strbuf_free(name);
+            }
+        }
+
+        if (accept_certs) {
+            /* Add all the certificate algorithms first, in preference order */
+            warn = false;
+            for (i = 0; i < n_preferred_hk; i++) {
+                if (preferred_hk[i] == HK_WARN)
+                    warn = true;
+                for (j = 0; j < lenof(ssh2_hostkey_algs); j++) {
+                    const struct ssh_signkey_with_user_pref_id *a =
+                        &ssh2_hostkey_algs[j];
+                    if (!a->alg->is_certificate)
+                        continue;
+                    if (a->id != preferred_hk[i])
+                        continue;
+                    alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY],
+                                              a->alg->ssh_id);
+                    alg->u.hk.hostkey = a->alg;
+                    alg->u.hk.warn = warn;
+                }
+            }
+        }
+
+        /* Next, add algorithms we already know a key for (unless
+         * configured not to do that) */
         warn = false;
         for (i = 0; i < n_preferred_hk; i++) {
             if (preferred_hk[i] == HK_WARN)
                 warn = true;
             for (j = 0; j < lenof(ssh2_hostkey_algs); j++) {
-                if (ssh2_hostkey_algs[j].id != preferred_hk[i])
+                const struct ssh_signkey_with_user_pref_id *a =
+                    &ssh2_hostkey_algs[j];
+                if (a->alg->is_certificate && accept_certs)
+                    continue;          /* already added this one */
+                if (a->id != preferred_hk[i])
                     continue;
                 if (conf_get_bool(conf, CONF_ssh_prefer_known_hostkeys) &&
                     have_ssh_host_key(hk_host, hk_port,
-                                      ssh2_hostkey_algs[j].alg->cache_id)) {
-                    alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY],
-                                              ssh2_hostkey_algs[j].alg->ssh_id);
-                    alg->u.hk.hostkey = ssh2_hostkey_algs[j].alg;
+                                      a->alg->cache_id)) {
+                    alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY],
+                                              a->alg->ssh_id);
+                    alg->u.hk.hostkey = a->alg;
                     alg->u.hk.warn = warn;
                 }
             }
         }
+
+        /* And finally, everything else */
         warn = false;
         for (i = 0; i < n_preferred_hk; i++) {
             if (preferred_hk[i] == HK_WARN)
                 warn = true;
             for (j = 0; j < lenof(ssh2_hostkey_algs); j++) {
-                if (ssh2_hostkey_algs[j].id != preferred_hk[i])
+                const struct ssh_signkey_with_user_pref_id *a =
+                    &ssh2_hostkey_algs[j];
+                if (a->alg->is_certificate)
+                    continue;
+                if (a->id != preferred_hk[i])
                     continue;
-                alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY],
-                                          ssh2_hostkey_algs[j].alg->ssh_id);
-                alg->u.hk.hostkey = ssh2_hostkey_algs[j].alg;
+                alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY],
+                                          a->alg->ssh_id);
+                alg->u.hk.hostkey = a->alg;
                 alg->u.hk.warn = warn;
             }
         }
@@ -711,7 +814,7 @@ static void ssh2_write_kexinit_lists(
                     continue;
                 if (ssh_transient_hostkey_cache_has(
                         thc, ssh2_hostkey_algs[j].alg)) {
-                    alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY],
+                    alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY],
                                               ssh2_hostkey_algs[j].alg->ssh_id);
                     alg->u.hk.hostkey = ssh2_hostkey_algs[j].alg;
                     alg->u.hk.warn = warn;
@@ -728,19 +831,19 @@ static void ssh2_write_kexinit_lists(
          * reverification.
          */
         assert(hk_prev);
-        alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY], hk_prev->ssh_id);
+        alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY], hk_prev->ssh_id);
         alg->u.hk.hostkey = hk_prev;
         alg->u.hk.warn = false;
     }
     if (can_gssapi_keyex) {
-        alg = ssh2_kexinit_addalg(kexlists[KEXLIST_HOSTKEY], "null");
+        alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY], "null");
         alg->u.hk.hostkey = NULL;
     }
     /* List encryption algorithms (client->server then server->client). */
     for (k = KEXLIST_CSCIPHER; k <= KEXLIST_SCCIPHER; k++) {
         warn = false;
 #ifdef FUZZING
-        alg = ssh2_kexinit_addalg(kexlists[k], "none");
+        alg = ssh2_kexinit_addalg(&kexlists[K], "none");
         alg->u.cipher.cipher = NULL;
         alg->u.cipher.warn = warn;
 #endif /* FUZZING */
@@ -748,7 +851,7 @@ static void ssh2_write_kexinit_lists(
             const ssh2_ciphers *c = preferred_ciphers[i];
             if (!c) warn = true;
             else for (j = 0; j < c->nciphers; j++) {
-                    alg = ssh2_kexinit_addalg(kexlists[k],
+                    alg = ssh2_kexinit_addalg(&kexlists[k],
                                               c->list[j]->ssh2_id);
                     alg->u.cipher.cipher = c->list[j];
                     alg->u.cipher.warn = warn;
@@ -775,7 +878,7 @@ static void ssh2_write_kexinit_lists(
         alg->u.mac.etm = false;
 #endif /* FUZZING */
         for (i = 0; i < nmacs; i++) {
-            alg = ssh2_kexinit_addalg(kexlists[j], maclist[i]->name);
+            alg = ssh2_kexinit_addalg(&kexlists[j], maclist[i]->name);
             alg->u.mac.mac = maclist[i];
             alg->u.mac.etm = false;
         }
@@ -783,7 +886,7 @@ static void ssh2_write_kexinit_lists(
             /* For each MAC, there may also be an ETM version,
              * which we list second. */
             if (maclist[i]->etm_name) {
-                alg = ssh2_kexinit_addalg(kexlists[j], maclist[i]->etm_name);
+                alg = ssh2_kexinit_addalg(&kexlists[j], maclist[i]->etm_name);
                 alg->u.mac.mac = maclist[i];
                 alg->u.mac.etm = true;
             }
@@ -796,22 +899,22 @@ static void ssh2_write_kexinit_lists(
     for (j = KEXLIST_CSCOMP; j <= KEXLIST_SCCOMP; j++) {
         assert(lenof(compressions) > 1);
         /* Prefer non-delayed versions */
-        alg = ssh2_kexinit_addalg(kexlists[j], preferred_comp->name);
+        alg = ssh2_kexinit_addalg(&kexlists[j], preferred_comp->name);
         alg->u.comp.comp = preferred_comp;
         alg->u.comp.delayed = false;
         if (preferred_comp->delayed_name) {
-            alg = ssh2_kexinit_addalg(kexlists[j],
+            alg = ssh2_kexinit_addalg(&kexlists[j],
                                       preferred_comp->delayed_name);
             alg->u.comp.comp = preferred_comp;
             alg->u.comp.delayed = true;
         }
         for (i = 0; i < lenof(compressions); i++) {
             const ssh_compression_alg *c = compressions[i];
-            alg = ssh2_kexinit_addalg(kexlists[j], c->name);
+            alg = ssh2_kexinit_addalg(&kexlists[j], c->name);
             alg->u.comp.comp = c;
             alg->u.comp.delayed = false;
             if (c->delayed_name) {
-                alg = ssh2_kexinit_addalg(kexlists[j], c->delayed_name);
+                alg = ssh2_kexinit_addalg(&kexlists[j], c->delayed_name);
                 alg->u.comp.comp = c;
                 alg->u.comp.delayed = true;
             }
@@ -827,10 +930,8 @@ static void ssh2_write_kexinit_lists(
         if (ssc && ssc->kex_override[i].ptr) {
             put_datapl(list, ssc->kex_override[i]);
         } else {
-            for (j = 0; j < MAXKEXLIST; j++) {
-                if (kexlists[i][j].name == NULL) break;
-                add_to_commasep(list, kexlists[i][j].name);
-            }
+            for (j = 0; j < kexlists[i].nalgs; j++)
+                add_to_commasep_pl(list, kexlists[i].algs[j].name);
         }
         if (i == KEXLIST_KEX && first_time) {
             if (our_hostkeys)          /* we're the server */
@@ -846,14 +947,19 @@ static void ssh2_write_kexinit_lists(
     put_stringz(pktout, "");
 }
 
+struct server_hostkeys {
+    int *indices;
+    size_t n, size;
+};
+
 static bool ssh2_scan_kexinits(
     ptrlen client_kexinit, ptrlen server_kexinit,
-    struct kexinit_algorithm kexlists[NKEXLIST][MAXKEXLIST],
+    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,
-    int *n_server_hostkeys, int server_hostkeys[MAXKEXLIST], unsigned *hkflags,
+    struct server_hostkeys *server_hostkeys, unsigned *hkflags,
     bool *can_send_ext_info)
 {
     BinarySource client[1], server[1];
@@ -918,10 +1024,9 @@ static bool ssh2_scan_kexinits(
       found_match:
 
         selected[i] = NULL;
-        for (j = 0; j < MAXKEXLIST; j++) {
-            if (kexlists[i][j].name &&
-                ptrlen_eq_string(found, kexlists[i][j].name)) {
-                selected[i] = &kexlists[i][j];
+        for (j = 0; j < kexlists[i].nalgs; j++) {
+            if (ptrlen_eq_ptrlen(found, kexlists[i].algs[j].name)) {
+                selected[i] = &kexlists[i].algs[j];
                 break;
             }
         }
@@ -1076,13 +1181,13 @@ static bool ssh2_scan_kexinits(
          * one or not. We return these as a list of indices into the
          * constant ssh2_hostkey_algs[] array.
          */
-        *n_server_hostkeys = 0;
-
         ptrlen list = slists[KEXLIST_HOSTKEY];
         for (ptrlen word; get_commasep_word(&list, &word) ;) {
             for (i = 0; i < lenof(ssh2_hostkey_algs); i++)
                 if (ptrlen_eq_string(word, ssh2_hostkey_algs[i].alg->ssh_id)) {
-                    server_hostkeys[(*n_server_hostkeys)++] = i;
+                    sgrowarray(server_hostkeys->indices, server_hostkeys->size,
+                               server_hostkeys->n);
+                    server_hostkeys->indices[server_hostkeys->n++] = i;
                     break;
                 }
         }
@@ -1091,9 +1196,95 @@ static bool ssh2_scan_kexinits(
     return true;
 }
 
+static inline bool delay_outgoing_kexinit(struct ssh2_transport_state *s)
+{
+    if (!(s->ppl.remote_bugs & BUG_REQUIRES_FILTERED_KEXINIT))
+        return false;   /* bug flag not enabled => no need to delay */
+    if (s->incoming_kexinit->len)
+        return false; /* already got a remote KEXINIT we can filter against */
+    return true;
+}
+
+static void filter_outgoing_kexinit(struct ssh2_transport_state *s)
+{
+    strbuf *pktout = strbuf_new();
+    BinarySource osrc[1], isrc[1];
+    BinarySource_BARE_INIT(
+        osrc, s->outgoing_kexinit->u, s->outgoing_kexinit->len);
+    BinarySource_BARE_INIT(
+        isrc, s->incoming_kexinit->u, s->incoming_kexinit->len);
+
+    /* Skip the packet type bytes from both packets */
+    get_byte(osrc);
+    get_byte(isrc);
+
+    /* Copy our cookie into the real output packet; skip their cookie */
+    put_datapl(pktout, get_data(osrc, 16));
+    get_data(isrc, 16);
+
+    /*
+     * Now we expect NKEXLIST+2 name-lists. We write into the outgoing
+     * packet a subset of our intended outgoing one, containing only
+     * names mentioned in the incoming out.
+     *
+     * NKEXLIST+2 because for this purpose we treat the 'languages'
+     * lists the same as the rest. In the rest of this code base we
+     * ignore those.
+     */
+    strbuf *out = strbuf_new();
+    for (size_t i = 0; i < NKEXLIST+2; i++) {
+        strbuf_clear(out);
+        ptrlen olist = get_string(osrc), ilist = get_string(isrc);
+        for (ptrlen oword; get_commasep_word(&olist, &oword) ;) {
+            ptrlen ilist_copy = ilist;
+            bool add = false;
+            for (ptrlen iword; get_commasep_word(&ilist_copy, &iword) ;) {
+                if (ptrlen_eq_ptrlen(oword, iword)) {
+                    /* Found this word in the incoming list. */
+                    add = true;
+                    break;
+                }
+            }
+
+            if (i == KEXLIST_KEX && ptrlen_eq_string(oword, "ext-info-c")) {
+                /* Special case: this will _never_ match anything from the
+                 * server, and we need it to enable SHA-2 based RSA.
+                 *
+                 * If this ever turns out to confuse any server all by
+                 * itself then I suppose we'll need an even more
+                 * draconian bug flag to exclude that too. (Obv, such
+                 * a server wouldn't be able to speak SHA-2 RSA
+                 * anyway.) */
+                add = true;
+            }
+
+            if (add)
+                add_to_commasep_pl(out, oword);
+        }
+        put_stringpl(pktout, ptrlen_from_strbuf(out));
+    }
+    strbuf_free(out);
+
+    /*
+     * Finally, copy the remaining parts of our intended KEXINIT.
+     */
+    put_bool(pktout, get_bool(osrc));  /* first-kex-packet-follows */
+    put_uint32(pktout, get_uint32(osrc)); /* reserved word */
+
+    /*
+     * Dump this data into s->outgoing_kexinit in place of what we had
+     * there before. We need to remember the KEXINIT we _really_ sent,
+     * not the one we'd have liked to send, since the host key
+     * signature will be validated against the former.
+     */
+    strbuf_shrink_to(s->outgoing_kexinit, 1); /* keep the type byte */
+    put_datapl(s->outgoing_kexinit, ptrlen_from_strbuf(pktout));
+    strbuf_free(pktout);
+}
+
 void ssh2transport_finalise_exhash(struct ssh2_transport_state *s)
 {
-    put_mp_ssh2(s->exhash, s->K);
+    put_datapl(s->exhash, ptrlen_from_strbuf(s->kex_shared_secret));
     assert(ssh_hash_alg(s->exhash)->hlen <= sizeof(s->exchange_hash));
     ssh_hash_final(s->exhash, s->exchange_hash);
     s->exhash = NULL;
@@ -1184,13 +1375,13 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
      * Construct our KEXINIT packet, in a strbuf so we can refer to it
      * later.
      */
-    strbuf_clear(s->client_kexinit);
+    strbuf_clear(s->outgoing_kexinit);
     put_byte(s->outgoing_kexinit, SSH2_MSG_KEXINIT);
     random_read(strbuf_append(s->outgoing_kexinit, 16), 16);
     ssh2_write_kexinit_lists(
         BinarySink_UPCAST(s->outgoing_kexinit), s->kexlists,
         s->conf, s->ssc, s->ppl.remote_bugs,
-        s->savedhost, s->savedport, s->hostkey_alg, s->thc,
+        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);
@@ -1199,12 +1390,34 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
     put_uint32(s->outgoing_kexinit, 0);             /* reserved */
 
     /*
-     * Send our KEXINIT.
+     * Send our KEXINIT, most of the time.
+     *
+     * An exception: in BUG_REQUIRES_FILTERED_KEXINIT mode, we have to
+     * have seen at least one KEXINIT from the server first, so that
+     * we can filter our own KEXINIT down to contain only algorithms
+     * the server mentioned.
+     *
+     * But we only need to do this on the _first_ key exchange, when
+     * we've never seen a KEXINIT from the server before. In rekeys,
+     * we still have the server's previous KEXINIT lying around, so we
+     * can filter based on that.
+     *
+     * (And a good thing too, since the way you _initiate_ a rekey is
+     * by sending your KEXINIT, so we'd have no way to prod the server
+     * into sending its first!)
      */
-    pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH2_MSG_KEXINIT);
-    put_data(pktout, s->outgoing_kexinit->u + 1,
-             s->outgoing_kexinit->len - 1); /* omit initial packet type byte */
-    pq_push(s->ppl.out_pq, pktout);
+    s->kexinit_delayed = delay_outgoing_kexinit(s);
+    if (!s->kexinit_delayed) {
+        if (s->ppl.remote_bugs & BUG_REQUIRES_FILTERED_KEXINIT) {
+            /* Filter based on the KEXINIT from the previous exchange */
+            filter_outgoing_kexinit(s);
+        }
+
+        pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH2_MSG_KEXINIT);
+        put_data(pktout, s->outgoing_kexinit->u + 1,
+                 s->outgoing_kexinit->len - 1); /* omit type byte */
+        pq_push(s->ppl.out_pq, pktout);
+    }
 
     /*
      * Flag that KEX is in progress.
@@ -1226,21 +1439,35 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
     put_byte(s->incoming_kexinit, SSH2_MSG_KEXINIT);
     put_data(s->incoming_kexinit, get_ptr(pktin), get_avail(pktin));
 
+    /*
+     * If we've delayed sending our KEXINIT so as to filter it down to
+     * only things the server won't choke on, send ours now.
+     */
+    if (s->kexinit_delayed) {
+        filter_outgoing_kexinit(s);
+        pktout = ssh_bpp_new_pktout(s->ppl.bpp, SSH2_MSG_KEXINIT);
+        put_data(pktout, s->outgoing_kexinit->u + 1,
+                 s->outgoing_kexinit->len - 1); /* omit type byte */
+        pq_push(s->ppl.out_pq, pktout);
+    }
+
     /*
      * Work through the two KEXINIT packets in parallel to find the
      * selected algorithm identifiers.
      */
     {
-        int nhk, hks[MAXKEXLIST], i, j;
+        struct server_hostkeys hks = { NULL, 0, 0 };
 
         if (!ssh2_scan_kexinits(
                 ptrlen_from_strbuf(s->client_kexinit),
                 ptrlen_from_strbuf(s->server_kexinit),
                 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, &nhk, hks,
-                &s->hkflags, &s->can_send_ext_info))
+                &s->warn_sccipher, s->ppl.ssh, NULL, &s->ignorepkt, &hks,
+                &s->hkflags, &s->can_send_ext_info)) {
+            sfree(hks.indices);
             return; /* false means a fatal error function was called */
+        }
 
         /*
          * In addition to deciding which host key we're actually going
@@ -1254,14 +1481,17 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
          */
         s->n_uncert_hostkeys = 0;
 
-        for (i = 0; i < nhk; i++) {
-            j = hks[i];
+        for (int i = 0; i < hks.n; i++) {
+            int j = hks.indices[i];
             if (ssh2_hostkey_algs[j].alg != s->hostkey_alg &&
+                ssh2_hostkey_algs[j].alg->cache_id &&
                 !have_ssh_host_key(s->savedhost, s->savedport,
                                    ssh2_hostkey_algs[j].alg->cache_id)) {
                 s->uncert_hostkeys[s->n_uncert_hostkeys++] = j;
             }
         }
+
+        sfree(hks.indices);
     }
 
     if (s->warn_kex) {
@@ -1363,6 +1593,9 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
      * Actually perform the key exchange.
      */
     s->exhash = ssh_hash_new(s->kex_alg->hash);
+    if (s->kex_shared_secret)
+        strbuf_free(s->kex_shared_secret);
+    s->kex_shared_secret = strbuf_new_nm();
     put_stringz(s->exhash, s->client_greeting);
     put_stringz(s->exhash, s->server_greeting);
     put_string(s->exhash, s->client_kexinit->u, s->client_kexinit->len);
@@ -1416,14 +1649,14 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
         strbuf *mac_key = strbuf_new_nm();
 
         if (s->out.cipher) {
-            ssh2_mkkey(s, cipher_iv, s->K, s->exchange_hash,
+            ssh2_mkkey(s, cipher_iv, s->kex_shared_secret, s->exchange_hash,
                        'A' + s->out.mkkey_adjust, s->out.cipher->blksize);
-            ssh2_mkkey(s, cipher_key, s->K, s->exchange_hash,
+            ssh2_mkkey(s, cipher_key, s->kex_shared_secret, s->exchange_hash,
                        'C' + s->out.mkkey_adjust,
                        s->out.cipher->padded_keybytes);
         }
         if (s->out.mac) {
-            ssh2_mkkey(s, mac_key, s->K, s->exchange_hash,
+            ssh2_mkkey(s, mac_key, s->kex_shared_secret, s->exchange_hash,
                        'E' + s->out.mkkey_adjust, s->out.mac->keylen);
         }
 
@@ -1508,14 +1741,14 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
         strbuf *mac_key = strbuf_new_nm();
 
         if (s->in.cipher) {
-            ssh2_mkkey(s, cipher_iv, s->K, s->exchange_hash,
+            ssh2_mkkey(s, cipher_iv, s->kex_shared_secret, s->exchange_hash,
                        'A' + s->in.mkkey_adjust, s->in.cipher->blksize);
-            ssh2_mkkey(s, cipher_key, s->K, s->exchange_hash,
+            ssh2_mkkey(s, cipher_key, s->kex_shared_secret, s->exchange_hash,
                        'C' + s->in.mkkey_adjust,
                        s->in.cipher->padded_keybytes);
         }
         if (s->in.mac) {
-            ssh2_mkkey(s, mac_key, s->K, s->exchange_hash,
+            ssh2_mkkey(s, mac_key, s->kex_shared_secret, s->exchange_hash,
                        'E' + s->in.mkkey_adjust, s->in.mac->keylen);
         }
 
@@ -1533,7 +1766,8 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
     /*
      * Free shared secret.
      */
-    mp_free(s->K); s->K = NULL;
+    strbuf_free(s->kex_shared_secret);
+    s->kex_shared_secret = NULL;
 
     /*
      * Update the specials menu to list the remaining uncertified host
@@ -2102,9 +2336,9 @@ static void ssh2_transport_reconfigure(PacketProtocolLayer *ppl, Conf *conf)
     for (i = 0; i < CIPHER_MAX; i++)
         if (conf_get_int_int(s->conf, CONF_ssh_cipherlist, i) !=
             conf_get_int_int(conf, CONF_ssh_cipherlist, i)) {
-        rekey_reason = "cipher settings changed";
-        rekey_mandatory = true;
-    }
+            rekey_reason = "cipher settings changed";
+            rekey_mandatory = true;
+        }
     if (conf_get_bool(s->conf, CONF_ssh2_des_cbc) !=
         conf_get_bool(conf, CONF_ssh2_des_cbc)) {
         rekey_reason = "cipher settings changed";
@@ -2134,6 +2368,18 @@ static int weak_algorithm_compare(void *av, void *bv)
     return a < b ? -1 : a > b ? +1 : 0;
 }
 
+static int ca_blob_compare(void *av, void *bv)
+{
+    host_ca *a = (host_ca *)av, *b = (host_ca *)bv;
+    strbuf *apk = a->ca_public_key, *bpk = b->ca_public_key;
+    /* Ordering by public key is arbitrary here, so do whatever is easiest */
+    if (apk->len < bpk->len)
+        return -1;
+    if (apk->len > bpk->len)
+        return +1;
+    return memcmp(apk->u, bpk->u, apk->len);
+}
+
 /*
  * Wrapper on seat_confirm_weak_crypto_primitive(), which uses the
  * tree234 s->weak_algorithms_consented_to to ensure we ask at most

+ 33 - 17
source/putty/ssh/transport2.h

@@ -18,9 +18,8 @@
 #define DH_MIN_SIZE 1024
 #define DH_MAX_SIZE 8192
 
-#define MAXKEXLIST 16
 struct kexinit_algorithm {
-    const char *name;
+    ptrlen name;
     union {
         struct {
             const ssh_kex *kex;
@@ -45,17 +44,30 @@ struct kexinit_algorithm {
         } comp;
     } u;
 };
+struct kexinit_algorithm_list {
+    struct kexinit_algorithm *algs;
+    size_t nalgs, algsize;
+};
 
-#define HOSTKEY_ALGORITHMS(X)                   \
-    X(HK_ED25519, ssh_ecdsa_ed25519)            \
-    X(HK_ED448, ssh_ecdsa_ed448)                \
-    X(HK_ECDSA, ssh_ecdsa_nistp256)             \
-    X(HK_ECDSA, ssh_ecdsa_nistp384)             \
-    X(HK_ECDSA, ssh_ecdsa_nistp521)             \
-    X(HK_DSA, ssh_dsa)                          \
-    X(HK_RSA, ssh_rsa_sha512)                   \
-    X(HK_RSA, ssh_rsa_sha256)                   \
-    X(HK_RSA, ssh_rsa)                          \
+#define HOSTKEY_ALGORITHMS(X)                                   \
+    X(HK_ED25519, ssh_ecdsa_ed25519)                            \
+    X(HK_ED448, ssh_ecdsa_ed448)                                \
+    X(HK_ECDSA, ssh_ecdsa_nistp256)                             \
+    X(HK_ECDSA, ssh_ecdsa_nistp384)                             \
+    X(HK_ECDSA, ssh_ecdsa_nistp521)                             \
+    X(HK_DSA, ssh_dsa)                                          \
+    X(HK_RSA, ssh_rsa_sha512)                                   \
+    X(HK_RSA, ssh_rsa_sha256)                                   \
+    X(HK_RSA, ssh_rsa)                                          \
+    X(HK_ED25519, opensshcert_ssh_ecdsa_ed25519)                \
+    /* OpenSSH defines no certified version of Ed448 */         \
+    X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp256)                 \
+    X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp384)                 \
+    X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp521)                 \
+    X(HK_DSA, opensshcert_ssh_dsa)                              \
+    X(HK_RSA, opensshcert_ssh_rsa_sha512)                       \
+    X(HK_RSA, opensshcert_ssh_rsa_sha256)                       \
+    X(HK_RSA, opensshcert_ssh_rsa)                              \
     /* end of list */
 #define COUNT_HOSTKEY_ALGORITHM(type, alg) +1
 #define N_HOSTKEY_ALGORITHMS (0 HOSTKEY_ALGORITHMS(COUNT_HOSTKEY_ALGORITHM))
@@ -128,7 +140,6 @@ struct ssh2_transport_state {
 
     const ssh_kex *kex_alg;
     const ssh_keyalg *hostkey_alg;
-    char *hostkey_str; /* string representation, for easy checking in rekeys */
     unsigned char session_id[MAX_HASH_LEN];
     int session_id_len;
     int dh_min_size, dh_max_size;
@@ -142,7 +153,7 @@ struct ssh2_transport_state {
 
     char *client_greeting, *server_greeting;
 
-    bool kex_in_progress;
+    bool kex_in_progress, kexinit_delayed;
     unsigned long next_rekey, last_rekey;
     const char *deferred_rekey_reason;
     bool higher_layer_ok;
@@ -165,15 +176,20 @@ struct ssh2_transport_state {
 
     bool gss_kex_used;
 
+    tree234 *host_cas;
+
     int nbits, pbits;
     bool warn_kex, warn_hk, warn_cscipher, warn_sccipher;
-    mp_int *p, *g, *e, *f, *K;
+    mp_int *p, *g, *e, *f;
+    strbuf *ebuf, *fbuf;
+    strbuf *kex_shared_secret;
     strbuf *outgoing_kexinit, *incoming_kexinit;
     strbuf *client_kexinit, *server_kexinit; /* aliases to the above */
     int kex_init_value, kex_reply_value;
     transport_direction in, out, *cstrans, *sctrans;
     ptrlen hostkeydata, sigdata;
-    strbuf *hostkeyblob;
+    strbuf *hostkeyblob; /* used in server to construct host key to
+                          * send to client; in client to check in rekeys */
     char *keystr;
     ssh_key *hkey;                     /* actual host key */
     unsigned hkflags;                  /* signing flags, used in server */
@@ -189,7 +205,7 @@ struct ssh2_transport_state {
     SeatPromptResult spr;
     bool guessok;
     bool ignorepkt;
-    struct kexinit_algorithm kexlists[NKEXLIST][MAXKEXLIST];
+    struct kexinit_algorithm_list kexlists[NKEXLIST];
 #ifndef NO_GSSAPI
     Ssh_gss_buf gss_buf;
     Ssh_gss_buf gss_rcvtok, gss_sndtok;

File diff suppressed because it is too large
+ 696 - 208
source/putty/ssh/userauth2-client.c


+ 9 - 3
source/putty/ssh/verstring.c

@@ -43,7 +43,7 @@ static void ssh_verstring_handle_input(BinaryPacketProtocol *bpp);
 static void ssh_verstring_handle_output(BinaryPacketProtocol *bpp);
 static PktOut *ssh_verstring_new_pktout(int type);
 static void ssh_verstring_queue_disconnect(BinaryPacketProtocol *bpp,
-                                          const char *msg, int category);
+                                           const char *msg, int category);
 
 static const BinaryPacketProtocolVtable ssh_verstring_vtable = {
     .free = ssh_verstring_free,
@@ -309,8 +309,8 @@ void ssh_verstring_handle_input(BinaryPacketProtocol *bpp)
      * a NUL terminator.
      */
     while (s->vstring->len > 0 &&
-           (s->vstring->s[s->vstring->len-1] == '\r' ||
-            s->vstring->s[s->vstring->len-1] == '\n'))
+           (s->vstring->s[s->vstring->len-1] == '\015' ||
+            s->vstring->s[s->vstring->len-1] == '\012'))
         strbuf_shrink_by(s->vstring, 1);
 
     bpp_logevent("Remote version: %s", s->vstring->s);
@@ -606,6 +606,12 @@ static void ssh_detect_bugs(struct ssh_verstring_state *s)
         bpp_logevent("We believe remote version has SSH-2 "
                      "channel request bug");
     }
+
+    if (conf_get_int(s->conf, CONF_sshbug_filter_kexinit) == FORCE_ON) {
+        s->remote_bugs |= BUG_REQUIRES_FILTERED_KEXINIT;
+        bpp_logevent("We believe remote version requires us to "
+                     "filter our KEXINIT");
+    }
 }
 
 const char *ssh_verstring_get_remote(BinaryPacketProtocol *bpp)

+ 15 - 16
source/putty/ssh/zlib.c

@@ -54,8 +54,8 @@ struct LZ77InternalContext;
 struct LZ77Context {
     struct LZ77InternalContext *ictx;
     void *userdata;
-    void (*literal) (struct LZ77Context * ctx, unsigned char c);
-    void (*match) (struct LZ77Context * ctx, int distance, int len);
+    void (*literal) (struct LZ77Context *ctx, unsigned char c);
+    void (*match) (struct LZ77Context *ctx, int distance, int len);
 };
 
 /*
@@ -573,7 +573,7 @@ struct ssh_zlib_compressor {
     ssh_compressor sc;
 };
 
-ssh_compressor *zlib_compress_init(void)
+static ssh_compressor *zlib_compress_init(void)
 {
     struct Outbuf *out;
     struct ssh_zlib_compressor *comp = snew(struct ssh_zlib_compressor);
@@ -592,7 +592,7 @@ ssh_compressor *zlib_compress_init(void)
     return &comp->sc;
 }
 
-void zlib_compress_cleanup(ssh_compressor *sc)
+static void zlib_compress_cleanup(ssh_compressor *sc)
 {
     struct ssh_zlib_compressor *comp =
         container_of(sc, struct ssh_zlib_compressor, sc);
@@ -604,10 +604,9 @@ void zlib_compress_cleanup(ssh_compressor *sc)
     sfree(comp);
 }
 
-void zlib_compress_block(ssh_compressor *sc,
-                         const unsigned char *block, int len,
-                         unsigned char **outblock, int *outlen,
-                         int minlen)
+static void zlib_compress_block(
+    ssh_compressor *sc, const unsigned char *block, int len,
+    unsigned char **outblock, int *outlen, int minlen)
 {
     struct ssh_zlib_compressor *comp =
         container_of(sc, struct ssh_zlib_compressor, sc);
@@ -904,7 +903,7 @@ struct zlib_decompress_ctx {
     ssh_decompressor dc;
 };
 
-ssh_decompressor *zlib_decompress_init(void)
+static ssh_decompressor *zlib_decompress_init(void)
 {
     struct zlib_decompress_ctx *dctx = snew(struct zlib_decompress_ctx);
     unsigned char lengths[288];
@@ -927,7 +926,7 @@ ssh_decompressor *zlib_decompress_init(void)
     return &dctx->dc;
 }
 
-void zlib_decompress_cleanup(ssh_decompressor *dc)
+static void zlib_decompress_cleanup(ssh_decompressor *dc)
 {
     struct zlib_decompress_ctx *dctx =
         container_of(dc, struct zlib_decompress_ctx, dc);
@@ -946,7 +945,7 @@ void zlib_decompress_cleanup(ssh_decompressor *dc)
 }
 
 static int zlib_huflookup(unsigned long *bitsp, int *nbitsp,
-                   struct zlib_table *tab)
+                          struct zlib_table *tab)
 {
     unsigned long bits = *bitsp;
     int nbits = *nbitsp;
@@ -986,9 +985,9 @@ static void zlib_emit_char(struct zlib_decompress_ctx *dctx, int c)
 
 #define EATBITS(n) ( dctx->nbits -= (n), dctx->bits >>= (n) )
 
-bool zlib_decompress_block(ssh_decompressor *dc,
-                           const unsigned char *block, int len,
-                           unsigned char **outblock, int *outlen)
+static bool zlib_decompress_block(
+    ssh_decompressor *dc, const unsigned char *block, int len,
+    unsigned char **outblock, int *outlen)
 {
     struct zlib_decompress_ctx *dctx =
         container_of(dc, struct zlib_decompress_ctx, dc);
@@ -1094,7 +1093,7 @@ bool zlib_decompress_block(ssh_decompressor *dc,
             if (dctx->lenptr >= dctx->hlit + dctx->hdist) {
                 dctx->currlentable = zlib_mktable(dctx->lengths, dctx->hlit);
                 dctx->currdisttable = zlib_mktable(dctx->lengths + dctx->hlit,
-                                                  dctx->hdist);
+                                                   dctx->hdist);
                 zlib_freetable(&dctx->lenlentable);
                 dctx->lenlentable = NULL;
                 dctx->state = INBLK;
@@ -1112,7 +1111,7 @@ bool zlib_decompress_block(ssh_decompressor *dc,
                 dctx->lenextrabits = (code == 16 ? 2 : code == 17 ? 3 : 7);
                 dctx->lenaddon = (code == 18 ? 11 : 3);
                 dctx->lenrep = (code == 16 && dctx->lenptr > 0 ?
-                               dctx->lengths[dctx->lenptr - 1] : 0);
+                                dctx->lengths[dctx->lenptr - 1] : 0);
                 dctx->state = TREES_LENREP;
             }
             break;

+ 77 - 84
source/putty/sshpubk.c

@@ -199,8 +199,7 @@ static int rsa1_load_s_internal(BinarySource *src, RSAKey *key, bool pub_only,
         if (enclen & 7)
             goto end;
 
-        buf = strbuf_new_nm();
-        put_datapl(buf, get_data(src, enclen));
+        buf = strbuf_dup_nm(get_data(src, enclen));
 
         unsigned char keybuf[16];
         hash_simple(&ssh_md5, ptrlen_from_asciz(passphrase), keybuf);
@@ -569,6 +568,14 @@ const ssh_keyalg *const all_keyalgs[] = {
     &ssh_ecdsa_nistp521,
     &ssh_ecdsa_ed25519,
     &ssh_ecdsa_ed448,
+    &opensshcert_ssh_dsa,
+    &opensshcert_ssh_rsa,
+    &opensshcert_ssh_rsa_sha256,
+    &opensshcert_ssh_rsa_sha512,
+    &opensshcert_ssh_ecdsa_ed25519,
+    &opensshcert_ssh_ecdsa_nistp256,
+    &opensshcert_ssh_ecdsa_nistp384,
+    &opensshcert_ssh_ecdsa_nistp521,
 };
 const size_t n_keyalgs = lenof(all_keyalgs);
 
@@ -586,6 +593,18 @@ const ssh_keyalg *find_pubkey_alg(const char *name)
     return find_pubkey_alg_len(ptrlen_from_asciz(name));
 }
 
+ptrlen pubkey_blob_to_alg_name(ptrlen blob)
+{
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, blob);
+    return get_string(src);
+}
+
+const ssh_keyalg *pubkey_blob_to_alg(ptrlen blob)
+{
+    return find_pubkey_alg_len(pubkey_blob_to_alg_name(blob));
+}
+
 struct ppk_cipher {
     const char *name;
     size_t blocklen, keylen, ivlen;
@@ -1226,7 +1245,7 @@ bool ppk_loadpub_s(BinarySource *src, char **algorithm, BinarySink *bs,
         bool ret = openssh_loadpub(src, algorithm, bs, commentptr, errorstr);
         return ret;
     } else if (type != SSH_KEYTYPE_SSH2) {
-        error = "not a PuTTY SSH-2 private key";
+        error = "not a public key or a PuTTY SSH-2 private key";
         goto error;
     }
 
@@ -1238,7 +1257,7 @@ bool ppk_loadpub_s(BinarySource *src, char **algorithm, BinarySink *bs,
         if (0 == strncmp(header, "PuTTY-User-Key-File-", 20))
             error = "PuTTY key format too new";
         else
-            error = "not a PuTTY SSH-2 private key";
+            error = "not a public key or a PuTTY SSH-2 private key";
         goto error;
     }
     error = "file format error";
@@ -1380,37 +1399,6 @@ int base64_lines(int datalen)
     return (datalen + 47) / 48;
 }
 
-static void base64_encode_s(BinarySink *bs, const unsigned char *data,
-                            int datalen, int cpl)
-{
-    int linelen = 0;
-    char out[4];
-    int n, i;
-
-    while (datalen > 0) {
-        n = (datalen < 3 ? datalen : 3);
-        base64_encode_atom(data, n, out);
-        data += n;
-        datalen -= n;
-        for (i = 0; i < 4; i++) {
-            if (linelen >= cpl) {
-                linelen = 0;
-                put_byte(bs, '\n');
-            }
-            put_byte(bs, out[i]);
-            linelen++;
-        }
-    }
-    put_byte(bs, '\n');
-}
-
-void base64_encode(FILE *fp, const unsigned char *data, int datalen, int cpl)
-{
-    stdio_sink ss;
-    stdio_sink_init(&ss, fp);
-    base64_encode_s(BinarySink_UPCAST(&ss), data, datalen, cpl);
-}
-
 const ppk_save_parameters ppk_save_default_parameters = {
     .fmt_version = 3,
 
@@ -1555,7 +1543,7 @@ strbuf *ppk_save_sb(ssh2_userkey *key, const char *passphrase,
     put_fmt(out, "Encryption: %s\n", cipherstr);
     put_fmt(out, "Comment: %s\n", key->comment);
     put_fmt(out, "Public-Lines: %d\n", base64_lines(pub_blob->len));
-    base64_encode_s(BinarySink_UPCAST(out), pub_blob->u, pub_blob->len, 64);
+    base64_encode_bs(BinarySink_UPCAST(out), ptrlen_from_strbuf(pub_blob), 64);
     if (params.fmt_version == 3 && ciphertype->keylen != 0) {
         put_fmt(out, "Key-Derivation: %s\n",
                 params.argon2_flavour == Argon2d ? "Argon2d" :
@@ -1571,8 +1559,8 @@ strbuf *ppk_save_sb(ssh2_userkey *key, const char *passphrase,
         put_fmt(out, "\n");
     }
     put_fmt(out, "Private-Lines: %d\n", base64_lines(priv_encrypted_len));
-    base64_encode_s(BinarySink_UPCAST(out),
-                    priv_blob_encrypted, priv_encrypted_len, 64);
+    base64_encode_bs(BinarySink_UPCAST(out),
+                     make_ptrlen(priv_blob_encrypted, priv_encrypted_len), 64);
     put_fmt(out, "Private-MAC: ");
     for (i = 0; i < macalg->len; i++)
         put_fmt(out, "%02x", priv_mac[i]);
@@ -1764,6 +1752,7 @@ static void ssh2_fingerprint_blob_sha256(ptrlen blob, strbuf *sb)
 char *ssh2_fingerprint_blob(ptrlen blob, FingerprintType fptype)
 {
     strbuf *sb = strbuf_new();
+    strbuf *tmp = NULL;
 
     /*
      * Identify the key algorithm, if possible.
@@ -1779,23 +1768,62 @@ char *ssh2_fingerprint_blob(ptrlen blob, FingerprintType fptype)
         if (alg) {
             int bits = ssh_key_public_bits(alg, blob);
             put_fmt(sb, "%.*s %d ", PTRLEN_PRINTF(algname), bits);
+
+            if (!ssh_fptype_is_cert(fptype) && alg->is_certificate) {
+                ssh_key *key = ssh_key_new_pub(alg, blob);
+                if (key) {
+                    tmp = strbuf_new();
+                    ssh_key_public_blob(ssh_key_base_key(key),
+                                        BinarySink_UPCAST(tmp));
+                    blob = ptrlen_from_strbuf(tmp);
+                    ssh_key_free(key);
+                }
+            }
         } else {
             put_fmt(sb, "%.*s ", PTRLEN_PRINTF(algname));
         }
     }
 
-    switch (fptype) {
+    switch (ssh_fptype_from_cert(fptype)) {
       case SSH_FPTYPE_MD5:
         ssh2_fingerprint_blob_md5(blob, sb);
         break;
       case SSH_FPTYPE_SHA256:
         ssh2_fingerprint_blob_sha256(blob, sb);
         break;
+      default:
+        unreachable("ssh_fptype_from_cert ruled out the other values");
     }
 
+    if (tmp)
+        strbuf_free(tmp);
+
     return strbuf_to_str(sb);
 }
 
+char *ssh2_double_fingerprint_blob(ptrlen blob, FingerprintType fptype)
+{
+    if (ssh_fptype_is_cert(fptype))
+        fptype = ssh_fptype_from_cert(fptype);
+
+    char *fp = ssh2_fingerprint_blob(blob, fptype);
+    char *p = strrchr(fp, ' ');
+    char *hash = p ? p + 1 : fp;
+
+    char *fpc = ssh2_fingerprint_blob(blob, ssh_fptype_to_cert(fptype));
+    char *pc = strrchr(fpc, ' ');
+    char *hashc = pc ? pc + 1 : fpc;
+
+    if (strcmp(hash, hashc)) {
+        char *tmp = dupprintf("%s (with certificate: %s)", fp, hashc);
+        sfree(fp);
+        fp = tmp;
+    }
+
+    sfree(fpc);
+    return fp;
+}
+
 char **ssh2_all_fingerprints_for_blob(ptrlen blob)
 {
     char **fps = snewn(SSH_N_FPTYPES, char *);
@@ -1813,6 +1841,15 @@ char *ssh2_fingerprint(ssh_key *data, FingerprintType fptype)
     return ret;
 }
 
+char *ssh2_double_fingerprint(ssh_key *data, FingerprintType fptype)
+{
+    strbuf *blob = strbuf_new();
+    ssh_key_public_blob(data, BinarySink_UPCAST(blob));
+    char *ret = ssh2_double_fingerprint_blob(ptrlen_from_strbuf(blob), fptype);
+    strbuf_free(blob);
+    return ret;
+}
+
 char **ssh2_all_fingerprints(ssh_key *data)
 {
     strbuf *blob = strbuf_new();
@@ -1869,7 +1906,7 @@ static int key_type_s_internal(BinarySource *src)
     if (find_pubkey_alg_len(get_nonchars(src, " \n")) > 0 &&
         get_chars(src, " ").len == 1 &&
         get_chars(src, "0123456789ABCDEFGHIJKLMNOPQRSTUV"
-                   "WXYZabcdefghijklmnopqrstuvwxyz+/=").len > 0 &&
+                  "WXYZabcdefghijklmnopqrstuvwxyz+/=").len > 0 &&
         get_nonchars(src, " \n").len == 0)
         return SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH;
 
@@ -1936,47 +1973,3 @@ const char *key_type_to_str(int type)
         unreachable("bad key type in key_type_to_str");
     }
 }
-
-key_components *key_components_new(void)
-{
-    key_components *kc = snew(key_components);
-    kc->ncomponents = 0;
-    kc->componentsize = 0;
-    kc->components = NULL;
-    return kc;
-}
-
-void key_components_add_text(key_components *kc,
-                             const char *name, const char *value)
-{
-    sgrowarray(kc->components, kc->componentsize, kc->ncomponents);
-    size_t n = kc->ncomponents++;
-    kc->components[n].name = dupstr(name);
-    kc->components[n].is_mp_int = false;
-    kc->components[n].text = dupstr(value);
-}
-
-void key_components_add_mp(key_components *kc,
-                           const char *name, mp_int *value)
-{
-    sgrowarray(kc->components, kc->componentsize, kc->ncomponents);
-    size_t n = kc->ncomponents++;
-    kc->components[n].name = dupstr(name);
-    kc->components[n].is_mp_int = true;
-    kc->components[n].mp = mp_copy(value);
-}
-
-void key_components_free(key_components *kc)
-{
-    for (size_t i = 0; i < kc->ncomponents; i++) {
-        sfree(kc->components[i].name);
-        if (kc->components[i].is_mp_int) {
-            mp_free(kc->components[i].mp);
-        } else {
-            smemclr(kc->components[i].text, strlen(kc->components[i].text));
-            sfree(kc->components[i].text);
-        }
-    }
-    sfree(kc->components);
-    sfree(kc);
-}

+ 27 - 0
source/putty/storage.h

@@ -6,6 +6,8 @@
 #ifndef PUTTY_STORAGE_H
 #define PUTTY_STORAGE_H
 
+#include "defs.h"
+
 /* ----------------------------------------------------------------------
  * Functions to save and restore PuTTY sessions. Note that this is
  * only the low-level code to do the reading and writing. The
@@ -91,6 +93,31 @@ int check_stored_host_key(const char *hostname, int port,
 void store_host_key(const char *hostname, int port,
                     const char *keytype, const char *key);
 
+/* ----------------------------------------------------------------------
+ * Functions to access PuTTY's configuration for trusted host
+ * certification authorities. This must be stored separately from the
+ * saved-session data, because the whole point is to avoid having to
+ * configure CAs separately per session.
+ */
+
+struct host_ca {
+    char *name;
+    strbuf *ca_public_key;
+    char *validity_expression;
+    ca_options opts;
+};
+
+host_ca_enum *enum_host_ca_start(void);
+bool enum_host_ca_next(host_ca_enum *handle, strbuf *out);
+void enum_host_ca_finish(host_ca_enum *handle);
+
+host_ca *host_ca_load(const char *name);
+char *host_ca_save(host_ca *);   /* NULL on success, or dynamic error msg */
+char *host_ca_delete(const char *name); /* likewise */
+
+host_ca *host_ca_new(void);  /* initialises to default settings */
+void host_ca_free(host_ca *);
+
 /* ----------------------------------------------------------------------
  * Functions to access PuTTY's random number seed file.
  */

+ 11 - 0
source/putty/stubs/null-cipher.c

@@ -0,0 +1,11 @@
+/*
+ * Implementation of shared trivial routines that ssh_cipher
+ * implementations might use.
+ */
+
+#include "ssh.h"
+
+void nullcipher_next_message(ssh_cipher *cipher)
+{
+    /* Most ciphers don't do anything at all with this */
+}

+ 22 - 0
source/putty/stubs/null-key.c

@@ -0,0 +1,22 @@
+#include "misc.h"
+#include "ssh.h"
+
+unsigned nullkey_supported_flags(const ssh_keyalg *self)
+{
+    return 0;
+}
+
+const char *nullkey_alternate_ssh_id(const ssh_keyalg *self, unsigned flags)
+{
+    /* There are no alternate ids */
+    return self->ssh_id;
+}
+
+ssh_key *nullkey_base_key(ssh_key *key)
+{
+    /* When a key is not certified, it is its own base */
+    return key;
+}
+
+bool nullkey_variable_size_no(const ssh_keyalg *self) { return false; }
+bool nullkey_variable_size_yes(const ssh_keyalg *self) { return true; }

+ 0 - 0
source/putty/utils/null_lp.c → source/putty/stubs/null-lp.c


+ 11 - 0
source/putty/stubs/null-mac.c

@@ -0,0 +1,11 @@
+/*
+ * Implementation of shared trivial routines that ssh2_mac
+ * implementations might use.
+ */
+
+#include "ssh.h"
+
+void nullmac_next_message(ssh2_mac *m)
+{
+    /* Most MACs don't do anything at all with this */
+}

+ 20 - 0
source/putty/stubs/null-opener.c

@@ -0,0 +1,20 @@
+/*
+ * Null implementation of DeferredSocketOpener. Doesn't even bother to
+ * allocate and free itself: there's just one static implementation
+ * which we hand out to any caller.
+ */
+
+#include "putty.h"
+
+static void null_opener_free(DeferredSocketOpener *opener) {}
+
+static const DeferredSocketOpenerVtable NullOpener_vt = {
+    .free = null_opener_free,
+};
+
+static DeferredSocketOpener null_opener = { .vt = &NullOpener_vt };
+
+DeferredSocketOpener *null_deferred_socket_opener(void)
+{
+    return &null_opener;
+}

+ 0 - 0
source/putty/stubs/nullplug.c → source/putty/stubs/null-plug.c


+ 12 - 1
source/putty/utils/nullseat.c → source/putty/stubs/null-seat.c

@@ -22,7 +22,7 @@ char *nullseat_get_ttymode(Seat *seat, const char *mode) { return NULL; }
 void nullseat_set_busy_status(Seat *seat, BusyStatus status) {}
 SeatPromptResult nullseat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
-    char *keystr, const char *keydisp, char **key_fingerprints, bool mismatch,
+    char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 { return SPR_SW_ABORT("this seat can't handle interactive prompts"); }
 SeatPromptResult nullseat_confirm_weak_crypto_primitive(
@@ -52,3 +52,14 @@ bool nullseat_verbose_yes(Seat *seat) { return true; }
 bool nullseat_interactive_no(Seat *seat) { return false; }
 bool nullseat_interactive_yes(Seat *seat) { return true; }
 bool nullseat_get_cursor_position(Seat *seat, int *x, int *y) { return false; }
+
+const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat)
+{
+    static const SeatDialogPromptDescriptions descs = {
+        .hk_accept_action = "",
+        .hk_connect_once_action = "",
+        .hk_cancel_action = "",
+        .hk_cancel_action_Participle = "",
+    };
+    return &descs;
+}

+ 11 - 11
source/putty/tree234.h

@@ -45,13 +45,13 @@ tree234 *newtree234(cmpfn234 cmp);
 /*
  * Free a 2-3-4 tree (not including freeing the elements).
  */
-void freetree234(tree234 * t);
+void freetree234(tree234 *t);
 
 /*
  * Add an element e to a sorted 2-3-4 tree t. Returns e on success,
  * or if an existing element compares equal, returns that.
  */
-void *add234(tree234 * t, void *e);
+void *add234(tree234 *t, void *e);
 
 /*
  * Add an element e to an unsorted 2-3-4 tree t. Returns e on
@@ -61,7 +61,7 @@ void *add234(tree234 * t, void *e);
  * Index range can be from 0 to the tree's current element count,
  * inclusive.
  */
-void *addpos234(tree234 * t, void *e, int index);
+void *addpos234(tree234 *t, void *e, int index);
 
 /*
  * Look up the element at a given numeric index in a 2-3-4 tree.
@@ -81,7 +81,7 @@ void *addpos234(tree234 * t, void *e, int index);
  *       consume(p);
  *   }
  */
-void *index234(tree234 * t, int index);
+void *index234(tree234 *t, int index);
 
 /*
  * Find an element e in a sorted 2-3-4 tree t. Returns NULL if not
@@ -126,10 +126,10 @@ void *index234(tree234 * t, int index);
 enum {
     REL234_EQ, REL234_LT, REL234_LE, REL234_GT, REL234_GE
 };
-void *find234(tree234 * t, void *e, cmpfn234 cmp);
-void *findrel234(tree234 * t, void *e, cmpfn234 cmp, int relation);
-void *findpos234(tree234 * t, void *e, cmpfn234 cmp, int *index);
-void *findrelpos234(tree234 * t, void *e, cmpfn234 cmp, int relation,
+void *find234(tree234 *t, void *e, cmpfn234 cmp);
+void *findrel234(tree234 *t, void *e, cmpfn234 cmp, int relation);
+void *findpos234(tree234 *t, void *e, cmpfn234 cmp, int *index);
+void *findrelpos234(tree234 *t, void *e, cmpfn234 cmp, int relation,
                     int *index);
 
 /*
@@ -184,12 +184,12 @@ void search234_step(search234_state *state, int direction);
  * is out of range (delpos234) or the element is already not in the
  * tree (del234) then they return NULL.
  */
-void *del234(tree234 * t, void *e);
-void *delpos234(tree234 * t, int index);
+void *del234(tree234 *t, void *e);
+void *delpos234(tree234 *t, int index);
 
 /*
  * Return the total element count of a tree234.
  */
-int count234(tree234 * t);
+int count234(tree234 *t);
 
 #endif                          /* TREE234_H */

+ 37 - 0
source/putty/utils/base64_decode.c

@@ -0,0 +1,37 @@
+#include "misc.h"
+
+void base64_decode_bs(BinarySink *bs, ptrlen input)
+{
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, input);
+
+    while (get_avail(src)) {
+        char b64atom[4];
+        unsigned char binatom[3];
+
+        for (size_t i = 0; i < 4 ;) {
+            char c = get_byte(src);
+            if (get_err(src))
+                c = '=';
+            if (c == '\n' || c == '\r')
+                continue;
+            b64atom[i++] = c;
+        }
+
+        put_data(bs, binatom, base64_decode_atom(b64atom, binatom));
+    }
+}
+
+void base64_decode_fp(FILE *fp, ptrlen input)
+{
+    stdio_sink ss;
+    stdio_sink_init(&ss, fp);
+    base64_decode_bs(BinarySink_UPCAST(&ss), input);
+}
+
+strbuf *base64_decode_sb(ptrlen input)
+{
+    strbuf *sb = strbuf_new_nm();
+    base64_decode_bs(BinarySink_UPCAST(sb), input);
+    return sb;
+}

+ 40 - 0
source/putty/utils/base64_encode.c

@@ -0,0 +1,40 @@
+#include "misc.h"
+
+void base64_encode_bs(BinarySink *bs, ptrlen input, int cpl)
+{
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, input);
+    int linelen = 0;
+
+    while (get_avail(src)) {
+        size_t n = get_avail(src) < 3 ? get_avail(src) : 3;
+        ptrlen binatom = get_data(src, n);
+
+        char b64atom[4];
+        base64_encode_atom(binatom.ptr, binatom.len, b64atom);
+        for (size_t i = 0; i < 4; i++) {
+            if (cpl > 0 && linelen >= cpl) {
+                linelen = 0;
+                put_byte(bs, '\n');
+            }
+            put_byte(bs, b64atom[i]);
+            linelen++;
+        }
+    }
+    if (cpl > 0)
+        put_byte(bs, '\n');
+}
+
+void base64_encode_fp(FILE *fp, ptrlen input, int cpl)
+{
+    stdio_sink ss;
+    stdio_sink_init(&ss, fp);
+    base64_encode_bs(BinarySink_UPCAST(&ss), input, cpl);
+}
+
+strbuf *base64_encode_sb(ptrlen input, int cpl)
+{
+    strbuf *sb = strbuf_new_nm();
+    base64_encode_bs(BinarySink_UPCAST(sb), input, cpl);
+    return sb;
+}

+ 968 - 0
source/putty/utils/cert-expr.c

@@ -0,0 +1,968 @@
+/*
+ * Parser for the boolean expression language used to configure what
+ * host names an OpenSSH certificate will be trusted to sign for.
+ */
+
+/*
+
+Language specification
+======================
+
+Outer lexical layer: the input expression is broken up into tokens,
+with any whitespace between them discarded and ignored. The following
+tokens are special:
+
+  ( ) && || !
+
+and the remaining token type is an 'atom', which is any non-empty
+sequence of characters from the following set:
+
+  ABCDEFGHIJKLMNOPQRSTUVWXYZ
+  abcdefghijklmnopqrstuvwxyz
+  0123456789
+  .-_*?[]/:
+
+Inner lexical layer: once the boundaries of an 'atom' token have been
+determined by the outer lex layer, each atom is further classified
+into one of the following subtypes:
+
+ - If it contains no ':' or '/', it's taken to be a wildcard matching
+   hostnames, e.g. "*.example.com".
+
+ - If it begins with 'port:' followed by digits, it's taken to be a
+   single port number specification, e.g. "port:22".
+
+ - If it begins with 'port:' followed by two digit sequences separated
+   by '-', it's taken to be a port number range, e.g. "port:0-1023".
+
+ - Any other atom is reserved for future expansion. (See Rationale.)
+
+Syntax layer: all of those types of atom are interpreted as predicates
+applied to the (hostname, port) data configured for the SSH connection
+for which the certificate is being validated.
+
+Wildcards are handled using the syntax in wildcard.c. The dot-
+separated structure of hostnames is thus not special; the '*' in
+"*.example.com" will match any number of subdomains under example.com.
+
+More complex boolean expressions can be made by combining those
+predicates using the boolean operators and parentheses, in the obvious
+way: && and || are infix operators representing logical AND and OR, !
+is a prefix operator representing logical NOT, and parentheses
+indicate grouping.
+
+Each of && and || can associate freely with itself (that is, you can
+write "a && b && c" without having to parenthesise one or the other
+subexpression). But they are forbidden to associate with _each other_.
+That is, if you write "a && b || c" or "a || b && c", it's a syntax
+error, and you must add parentheses to indicate which operator was
+intended to have the higher priority.
+
+Rationale
+=========
+
+Atoms: restrictions
+-------------------
+
+The characters permitted in the 'atom' token don't include \, even
+though it's a special character defined by wildcard.c. That's because
+in this restricted context wildcards will never need it: no hostname
+contains a literal \, and neither does any hostname contain a literal
+instance of any of the wildcard characters that wildcard.c allows you
+to use \ to escape.
+
+Atoms: future extension
+-----------------------
+
+The specification of the 'atom' token is intended to leave space for
+more than one kind of future extension.
+
+Most obviously, additional special predicates similar to "port:", with
+different disambiguating prefixes. I don't know what things of that
+kind we might need, but space is left for them just in case.
+
+Also, the unused '/' in the permitted-characters spec is intended to
+leave open the possibility of allowing certificate acceptance to be
+based on IP address, because the usual CIDR syntax for specifying IP
+ranges (e.g. "192.168.1.0/24" or "2345:6789:abcd:ef01::/128") would be
+lexed as a single atom under these rules.
+
+For the moment, certificate acceptance rules based on IP address are
+not supported, because it's not clear what the semantics ought to be.
+There are two problems with using IP addresses for this purpose:
+
+ 1. Sometimes they come from the DNS, which means you can't trust
+    them. The whole idea of SSH is to end-to-end authenticate the host
+    key against only the input given _by the user_ to the client. Any
+    additional data provided by the network, such as the result of a
+    DNS lookup, is suspect.
+
+    On the other hand, sometimes the IP address *is* part of the user
+    input, because the user can provide an IP address rather than a
+    hostname as the intended connection destination. So there are two
+    kinds of IP address, and they should very likely be treated
+    differently.
+
+ 2. Sometimes the server's IP address is not even *known* by the
+    client, if you're connecting via a proxy and leaving DNS lookups
+    to the proxy.
+
+So, what should a boolean expression do if it's asked to accept or
+reject based on an IP address, and the IP address is unknown or
+untrustworthy? I'm not sure, and therefore, in the initial version of
+this expression system, I haven't implemented them at all.
+
+But the syntax is still available for a future extension to use, if we
+come up with good answers to these questions.
+
+(One possibility would be to evaluate the whole expression in Kleene
+three-valued logic, so that every subexpression has the possible
+answers TRUE, FALSE and UNKNOWN. If a definite IP address is not
+available, IP address predicates evaluate to UNKNOWN. Then, once the
+expression as a whole is evaluated, fail closed, by interpreting
+UNKNOWN as 'reject'. The effect would be that a positive _or_ negative
+constraint on the IP address would cause rejection if the IP address
+is not reliably known, because once the predicate itself has returned
+UNKNOWN, negating it still gives UNKNOWN. The only way you could still
+accept a certificate in that situation would be if the overall
+structure of the expression meant that the test of the IP address
+couldn't affect the result anyway, e.g. if it was ANDed with another
+subexpression that definitely evaluated to FALSE, or ORed with one
+that evaluated to TRUE. This system seems conceptually elegant to me,
+but the argument against it is that it's complicated and
+counterintuitive, which is not a property you want in something a user
+is writing for security purposes!)
+
+Operator precedence
+-------------------
+
+Why did I choose to make && and || refuse to associate with each
+other, instead of applying the usual C precedence rule that && beats
+||? Because I think the C precedence rule is essentially arbitrary, in
+the sense that when people are writing boolean expressions in practice
+based on predicates from the rest of their program, it's about equally
+common to want to nest an && within an || and vice versa. So the
+default precedence rule only gives the user what they actually wanted
+about 50% of the time, and leads to absent-minded errors about as
+often as it conveniently allows you to omit a pair of parens.
+
+With my mathematician hat on, it's not so arbitrary. I agree that if
+you're *going* to give || and && a relative priority then it makes
+more sense to make && the higher-priority one, because if you're
+thinking algebraically, && is more multiplicative and || is more
+additive. But the pure-maths contexts in which that's convenient have
+nothing to do with general boolean expressions in if statements.
+
+This boolean syntax is still close enough to that of C and its
+derivatives to allow easy enough expression interchange (not counting
+the fact that atoms would need rewriting). Any boolean expression
+structure accepted by this syntax is also legal C and means the same
+thing; any expression structure accepted by C is either legal and
+equivalent in this syntax, or will fail with an error. In no case is
+anything accepted but mapped to a different meaning.
+
+ */
+
+#include "putty.h"
+
+typedef enum Token {
+    TOK_LPAR, TOK_RPAR,
+    TOK_AND, TOK_OR, TOK_NOT,
+    TOK_ATOM,
+    TOK_END, TOK_ERROR
+} Token;
+
+static inline bool is_space(char c)
+{
+    return (c == ' ' || c == '\n' || c == '\r' || c == '\t' ||
+            c == '\f' || c == '\v');
+}
+
+static inline bool is_operator_char(char c)
+{
+    return (c == '(' || c == ')' || c == '&' || c == '|' || c == '!');
+}
+
+static inline bool is_atom_char(char c)
+{
+    return (('A' <= c && c <= 'Z') ||
+            ('a' <= c && c <= 'z') ||
+            ('0' <= c && c <= '9') ||
+            c == '.' || c == '-' || c == '_' || c == '*' || c == '?' ||
+            c == '[' || c == ']' || c == '/' || c == ':');
+}
+
+static Token lex(ptrlen *text, ptrlen *token, char **err)
+{
+    const char *p = text->ptr, *e = p + text->len;
+    Token type = TOK_ERROR;
+
+    /* Skip whitespace */
+    while (p < e && is_space(*p))
+        p++;
+
+    const char *start = p;
+
+    if (!(p < e)) {
+        type = TOK_END;
+        goto out;
+    }
+
+    if (is_operator_char(*p)) {
+        /* Match boolean-expression tokens */
+        static const struct operator {
+            ptrlen text;
+            Token type;
+        } operators[] = {
+            {PTRLEN_DECL_LITERAL("("), TOK_LPAR},
+            {PTRLEN_DECL_LITERAL(")"), TOK_RPAR},
+            {PTRLEN_DECL_LITERAL("&&"), TOK_AND},
+            {PTRLEN_DECL_LITERAL("||"), TOK_OR},
+            {PTRLEN_DECL_LITERAL("!"), TOK_NOT},
+        };
+
+        for (size_t i = 0; i < lenof(operators); i++) {
+            const struct operator *op = &operators[i];
+            if (e - p >= op->text.len &&
+                ptrlen_eq_ptrlen(op->text, make_ptrlen(p, op->text.len))) {
+                p += op->text.len;
+                type = op->type;
+                goto out;
+            }
+        }
+
+        /*
+         * Report an error if one of the operator characters is used
+         * in a way that doesn't match something in that table (e.g. a
+         * single &).
+         */
+        p++;
+        type = TOK_ERROR;
+        *err = dupstr("unrecognised boolean operator");
+        goto out;
+    } else if (is_atom_char(*p)) {
+        /*
+         * Match an 'atom' token, which is any non-empty sequence of
+         * characters from the combined set that allows hostname
+         * wildcards, IP address ranges and special predicates like
+         * port numbers.
+         */
+        do {
+            p++;
+        } while (p < e && is_atom_char(*p));
+
+        type = TOK_ATOM;
+        goto out;
+    } else {
+        /*
+         * Otherwise, report an error.
+         */
+        p++;
+        type = TOK_ERROR;
+        *err = dupstr("unexpected character in expression");
+        goto out;
+    }
+
+  out:
+    *token = make_ptrlen(start, p - start);
+    text->ptr = p;
+    text->len = e - p;
+    return type;
+}
+
+typedef enum Operator {
+    OP_AND, OP_OR, OP_NOT,
+    OP_HOSTNAME_WC, OP_PORT_RANGE
+} Operator;
+
+typedef struct ExprNode ExprNode;
+struct ExprNode {
+    Operator op;
+    ptrlen text;
+    union {
+        struct {
+            /* OP_AND, OP_OR */
+            ExprNode **subexprs;
+            size_t nsubexprs;
+        };
+        struct {
+            /* OP_NOT */
+            ExprNode *subexpr;
+        };
+        struct {
+            /* OP_HOSTNAME_WC */
+            char *wc;
+        };
+        struct {
+            /* OP_PORT_RANGE */
+            unsigned lo, hi;           /* both inclusive */
+        };
+    };
+};
+
+static ExprNode *exprnode_new(Operator op, ptrlen text)
+{
+    ExprNode *en = snew(ExprNode);
+    memset(en, 0, sizeof(*en));
+    en->op = op;
+    en->text = text;
+    return en;
+}
+
+static void exprnode_free(ExprNode *en)
+{
+    switch (en->op) {
+      case OP_AND:
+      case OP_OR:
+        for (size_t i = 0; i < en->nsubexprs; i++)
+            exprnode_free(en->subexprs[i]);
+        sfree(en->subexprs);
+        break;
+      case OP_NOT:
+        exprnode_free(en->subexpr);
+        break;
+      case OP_HOSTNAME_WC:
+        sfree(en->wc);
+        break;
+      case OP_PORT_RANGE:
+        break;
+      default:
+        unreachable("unhandled node type in exprnode_free");
+    }
+
+    sfree(en);
+}
+
+static unsigned ptrlen_to_port_number(ptrlen input)
+{
+    unsigned val = 0;
+    for (const char *p = input.ptr, *end = p + input.len; p < end; p++) {
+        assert('0' <= *p && *p <= '9'); /* expect parser to have checked */
+        val = 10 * val + (*p - '0');
+        if (val >= 65536)
+            val = 65536; /* normalise 'too large' to avoid integer overflow */
+    }
+    return val;
+}
+
+typedef struct ParserState ParserState;
+struct ParserState {
+    ptrlen currtext;
+    Token tok;
+    ptrlen toktext;
+    char *err;
+    ptrlen errloc;
+};
+
+static void error(ParserState *ps, char *errtext, ptrlen errloc)
+{
+    if (!ps->err) {
+        ps->err = errtext;
+        ps->errloc = errloc;
+    } else {
+        sfree(errtext);
+    }
+}
+
+static void advance(ParserState *ps)
+{
+    char *err = NULL;
+    ps->tok = lex(&ps->currtext, &ps->toktext, &err);
+    if (ps->tok == TOK_ERROR)
+        error(ps, err, ps->toktext);
+}
+
+static ExprNode *parse_atom(ParserState *ps);
+static ExprNode *parse_expr(ParserState *ps);
+
+static bool atom_is_hostname_wc(ptrlen toktext)
+{
+    return !ptrlen_contains(toktext, ":/");
+}
+
+static ExprNode *parse_atom(ParserState *ps)
+{
+    if (ps->tok == TOK_LPAR) {
+        ptrlen openpar = ps->toktext;
+        advance(ps);                   /* eat the ( */
+
+        ExprNode *subexpr = parse_expr(ps);
+        if (!subexpr)
+            return NULL;
+
+        if (ps->tok != TOK_RPAR) {
+            error(ps, dupstr("expected ')' after parenthesised subexpression"),
+                  subexpr->text);
+            exprnode_free(subexpr);
+            return NULL;
+        }
+
+        ptrlen closepar = ps->toktext;
+        advance(ps);                   /* eat the ) */
+
+        /* We can reuse the existing AST node, but we need to extend
+         * its bounds within the input expression to include the
+         * parentheses */
+        subexpr->text = make_ptrlen_startend(
+            openpar.ptr, ptrlen_end(closepar));
+        return subexpr;
+    }
+
+    if (ps->tok == TOK_NOT) {
+        ptrlen notloc = ps->toktext;
+        advance(ps);                   /* eat the ! */
+
+        ExprNode *subexpr = parse_atom(ps);
+        if (!subexpr)
+            return NULL;
+
+        ExprNode *en = exprnode_new(
+            OP_NOT, make_ptrlen_startend(
+                notloc.ptr, ptrlen_end(subexpr->text)));
+        en->subexpr = subexpr;
+        return en;
+    }
+
+    if (ps->tok == TOK_ATOM) {
+        if (atom_is_hostname_wc(ps->toktext)) {
+            /* Hostname wildcard. */
+            ExprNode *en = exprnode_new(OP_HOSTNAME_WC, ps->toktext);
+            en->wc = mkstr(ps->toktext);
+            advance(ps);
+            return en;
+        }
+
+        ptrlen tail;
+        if (ptrlen_startswith(ps->toktext, PTRLEN_LITERAL("port:"), &tail)) {
+            /* Port number (single or range). */
+            unsigned lo, hi;
+            char *minus;
+            static const char DIGITS[] = "0123456789\0";
+            bool parse_ok = false;
+
+            if (tail.len > 0 && ptrlen_contains_only(tail, DIGITS)) {
+                lo = ptrlen_to_port_number(tail);
+                if (lo >= 65536) {
+                    error(ps, dupstr("port number too large"), tail);
+                    return NULL;
+                }
+                hi = lo;
+                parse_ok = true;
+            } else if ((minus = memchr(tail.ptr, '-', tail.len)) != NULL) {
+                ptrlen pl_lo = make_ptrlen_startend(tail.ptr, minus);
+                ptrlen pl_hi = make_ptrlen_startend(minus+1, ptrlen_end(tail));
+                if (pl_lo.len > 0 && ptrlen_contains_only(pl_lo, DIGITS) &&
+                    pl_hi.len > 0 && ptrlen_contains_only(pl_hi, DIGITS)) {
+
+                    lo = ptrlen_to_port_number(pl_lo);
+                    if (lo >= 65536) {
+                        error(ps, dupstr("port number too large"), pl_lo);
+                        return NULL;
+                    }
+
+                    hi = ptrlen_to_port_number(pl_hi);
+                    if (hi >= 65536) {
+                        error(ps, dupstr("port number too large"), pl_hi);
+                        return NULL;
+                    }
+
+                    if (hi < lo) {
+                        error(ps, dupstr("port number range is backwards"),
+                              make_ptrlen_startend(pl_lo.ptr,
+                                                   ptrlen_end(pl_hi)));
+                        return NULL;
+                    }
+
+                    parse_ok = true;
+                }
+            }
+
+            if (!parse_ok) {
+                error(ps, dupstr("unable to parse port number specification"),
+                      ps->toktext);
+                return NULL;
+            }
+
+
+            ExprNode *en = exprnode_new(OP_PORT_RANGE, ps->toktext);
+            en->lo = lo;
+            en->hi = hi;
+            advance(ps);
+            return en;
+        }
+    }
+
+    error(ps, dupstr("expected a predicate or a parenthesised subexpression"),
+          ps->toktext);
+    return NULL;
+}
+
+static ExprNode *parse_expr(ParserState *ps)
+{
+    ExprNode *subexpr = parse_atom(ps);
+    if (!subexpr)
+        return NULL;
+
+    if (ps->tok != TOK_AND && ps->tok != TOK_OR)
+        return subexpr;
+
+    Token operator = ps->tok;
+    ExprNode *en = exprnode_new(ps->tok == TOK_AND ? OP_AND : OP_OR,
+                                subexpr->text);
+    size_t subexprs_size = 0;
+
+    sgrowarray(en->subexprs, subexprs_size, en->nsubexprs);
+    en->subexprs[en->nsubexprs++] = subexpr;
+
+    while (true) {
+        advance(ps);                   /* eat the operator */
+
+        subexpr = parse_atom(ps);
+        if (!subexpr) {
+            exprnode_free(en);
+            return NULL;
+        }
+        sgrowarray(en->subexprs, subexprs_size, en->nsubexprs);
+        en->subexprs[en->nsubexprs++] = subexpr;
+        en->text = make_ptrlen_startend(
+            en->text.ptr, ptrlen_end(subexpr->text));
+
+        if (ps->tok != TOK_AND && ps->tok != TOK_OR)
+            return en;
+
+        if (ps->tok != operator) {
+            error(ps, dupstr("expected parentheses to disambiguate && and || "
+                             "on either side of expression"), subexpr->text);
+            exprnode_free(en);
+            return NULL;
+        }
+    }
+}
+
+static ExprNode *parse(ptrlen expr, char **error_msg, ptrlen *error_loc)
+{
+    ParserState ps[1];
+    ps->currtext = expr;
+    ps->err = NULL;
+    advance(ps);
+
+    ExprNode *en = parse_expr(ps);
+    if (en && ps->tok != TOK_END) {
+        error(ps, dupstr("unexpected text at end of expression"),
+              make_ptrlen_startend(ps->toktext.ptr, ptrlen_end(expr)));
+        exprnode_free(en);
+        en = NULL;
+    }
+
+    if (!en) {
+        if (error_msg)
+            *error_msg = ps->err;
+        else
+            sfree(ps->err);
+        if (error_loc)
+            *error_loc = ps->errloc;
+        return NULL;
+    }
+
+    return en;
+}
+
+static bool eval(ExprNode *en, const char *hostname, unsigned port)
+{
+    switch (en->op) {
+      case OP_AND:
+        for (size_t i = 0; i < en->nsubexprs; i++)
+            if (!eval(en->subexprs[i], hostname, port))
+                return false;
+        return true;
+
+      case OP_OR:
+        for (size_t i = 0; i < en->nsubexprs; i++)
+            if (eval(en->subexprs[i], hostname, port))
+                return true;
+        return false;
+
+      case OP_NOT:
+        return !eval(en->subexpr, hostname, port);
+
+      case OP_HOSTNAME_WC:
+        return wc_match(en->wc, hostname);
+
+      case OP_PORT_RANGE:
+        return en->lo <= port && port <= en->hi;
+
+      default:
+        unreachable("unhandled node type in eval");
+    }
+}
+
+bool cert_expr_match_str(const char *expression,
+                         const char *hostname, unsigned port)
+{
+    ExprNode *en = parse(ptrlen_from_asciz(expression), NULL, NULL);
+    if (!en)
+        return false;
+
+    bool matched = eval(en, hostname, port);
+    exprnode_free(en);
+    return matched;
+}
+
+bool cert_expr_valid(const char *expression,
+                     char **error_msg, ptrlen *error_loc)
+{
+    ExprNode *en = parse(ptrlen_from_asciz(expression), error_msg, error_loc);
+    if (en) {
+        exprnode_free(en);
+        return true;
+    } else {
+        return false;
+    }
+}
+
+struct CertExprBuilder {
+    char **wcs;
+    size_t nwcs, wcsize;
+};
+
+CertExprBuilder *cert_expr_builder_new(void)
+{
+    CertExprBuilder *eb = snew(CertExprBuilder);
+    eb->wcs = NULL;
+    eb->nwcs = eb->wcsize = 0;
+    return eb;
+}
+
+void cert_expr_builder_free(CertExprBuilder *eb)
+{
+    for (size_t i = 0; i < eb->nwcs; i++)
+        sfree(eb->wcs[i]);
+    sfree(eb->wcs);
+    sfree(eb);
+}
+
+void cert_expr_builder_add(CertExprBuilder *eb, const char *wildcard)
+{
+    /* Check this wildcard is lexically valid as an atom */
+    ptrlen orig = ptrlen_from_asciz(wildcard), pl = orig;
+    ptrlen toktext;
+    char *err;
+    Token tok = lex(&pl, &toktext, &err);
+    if (!(tok == TOK_ATOM &&
+          toktext.ptr == orig.ptr &&
+          toktext.len == orig.len &&
+          atom_is_hostname_wc(toktext))) {
+        if (tok == TOK_ERROR)
+            sfree(err);
+        return;
+    }
+
+    sgrowarray(eb->wcs, eb->wcsize, eb->nwcs);
+    eb->wcs[eb->nwcs++] = mkstr(orig);
+}
+
+char *cert_expr_expression(CertExprBuilder *eb)
+{
+    strbuf *sb = strbuf_new();
+    for (size_t i = 0; i < eb->nwcs; i++) {
+        if (i)
+            put_dataz(sb, " || ");
+        put_dataz(sb, eb->wcs[i]);
+    }
+    return strbuf_to_str(sb);
+}
+
+#ifdef TEST
+
+void out_of_memory(void) { fprintf(stderr, "out of memory\n"); abort(); }
+
+static void exprnode_dump(BinarySink *bs, ExprNode *en, const char *origtext)
+{
+    put_fmt(bs, "(%zu:%zu ",
+            (size_t)((const char *)en->text.ptr - origtext),
+            (size_t)((const char *)ptrlen_end(en->text) - origtext));
+    switch (en->op) {
+      case OP_AND:
+      case OP_OR:
+        put_dataz(bs, en->op == OP_AND ? "and" : "or");
+        for (size_t i = 0; i < en->nsubexprs; i++) {
+            put_byte(bs, ' ');
+            exprnode_dump(bs, en->subexprs[i], origtext);
+        }
+        break;
+      case OP_NOT:
+        put_dataz(bs, "not ");
+        exprnode_dump(bs, en->subexpr, origtext);
+        break;
+      case OP_HOSTNAME_WC:
+        put_dataz(bs, "host-wc '");
+        put_dataz(bs, en->wc);
+        put_byte(bs, '\'');
+        break;
+      case OP_PORT_RANGE:
+        put_fmt(bs, "port-range %u %u", en->lo, en->hi);
+        break;
+      default:
+        unreachable("unhandled node type in exprnode_dump");
+    }
+    put_byte(bs, ')');
+}
+
+static const struct ParseTest {
+    const char *file;
+    int line;
+    const char *expr, *output;
+} parsetests[] = {
+#define T(expr_, output_) { \
+        .file=__FILE__, .line=__LINE__, .expr=expr_, .output=output_}
+
+    T("*.example.com", "(0:13 host-wc '*.example.com')"),
+    T("port:0", "(0:6 port-range 0 0)"),
+    T("port:22", "(0:7 port-range 22 22)"),
+    T("port:22-22", "(0:10 port-range 22 22)"),
+    T("port:65535", "(0:10 port-range 65535 65535)"),
+    T("port:0-1023", "(0:11 port-range 0 1023)"),
+
+    T("&", "ERR:0:1:unrecognised boolean operator"),
+    T("|", "ERR:0:1:unrecognised boolean operator"),
+    T(";", "ERR:0:1:unexpected character in expression"),
+    T("port:", "ERR:0:5:unable to parse port number specification"),
+    T("port:abc", "ERR:0:8:unable to parse port number specification"),
+    T("port:65536", "ERR:5:10:port number too large"),
+    T("port:65536-65537", "ERR:5:10:port number too large"),
+    T("port:0-65536", "ERR:7:12:port number too large"),
+    T("port:23-22", "ERR:5:10:port number range is backwards"),
+
+    T("a", "(0:1 host-wc 'a')"),
+    T("(a)", "(0:3 host-wc 'a')"),
+    T("((a))", "(0:5 host-wc 'a')"),
+    T(" (\n(\ra\t)\f)\v", "(1:10 host-wc 'a')"),
+    T("a&&b", "(0:4 and (0:1 host-wc 'a') (3:4 host-wc 'b'))"),
+    T("a||b", "(0:4 or (0:1 host-wc 'a') (3:4 host-wc 'b'))"),
+    T("a&&b&&c", "(0:7 and (0:1 host-wc 'a') (3:4 host-wc 'b') (6:7 host-wc 'c'))"),
+    T("a||b||c", "(0:7 or (0:1 host-wc 'a') (3:4 host-wc 'b') (6:7 host-wc 'c'))"),
+    T("a&&(b||c)", "(0:9 and (0:1 host-wc 'a') (3:9 or (4:5 host-wc 'b') (7:8 host-wc 'c')))"),
+    T("a||(b&&c)", "(0:9 or (0:1 host-wc 'a') (3:9 and (4:5 host-wc 'b') (7:8 host-wc 'c')))"),
+    T("(a&&b)||c", "(0:9 or (0:6 and (1:2 host-wc 'a') (4:5 host-wc 'b')) (8:9 host-wc 'c'))"),
+    T("(a||b)&&c", "(0:9 and (0:6 or (1:2 host-wc 'a') (4:5 host-wc 'b')) (8:9 host-wc 'c'))"),
+    T("!a&&b", "(0:5 and (0:2 not (1:2 host-wc 'a')) (4:5 host-wc 'b'))"),
+    T("a&&!b&&c", "(0:8 and (0:1 host-wc 'a') (3:5 not (4:5 host-wc 'b')) (7:8 host-wc 'c'))"),
+    T("!a||b", "(0:5 or (0:2 not (1:2 host-wc 'a')) (4:5 host-wc 'b'))"),
+    T("a||!b||c", "(0:8 or (0:1 host-wc 'a') (3:5 not (4:5 host-wc 'b')) (7:8 host-wc 'c'))"),
+
+    T("", "ERR:0:0:expected a predicate or a parenthesised subexpression"),
+    T("a &&", "ERR:4:4:expected a predicate or a parenthesised subexpression"),
+    T("a ||", "ERR:4:4:expected a predicate or a parenthesised subexpression"),
+    T("a b c d", "ERR:2:7:unexpected text at end of expression"),
+    T("(", "ERR:1:1:expected a predicate or a parenthesised subexpression"),
+    T("(a", "ERR:1:2:expected ')' after parenthesised subexpression"),
+    T("(a b", "ERR:1:2:expected ')' after parenthesised subexpression"),
+    T("a&&b&&c||d||e", "ERR:6:7:expected parentheses to disambiguate && and || on either side of expression"),
+    T("a||b||c&&d&&e", "ERR:6:7:expected parentheses to disambiguate && and || on either side of expression"),
+    T("!", "ERR:1:1:expected a predicate or a parenthesised subexpression"),
+
+    T("!a", "(0:2 not (1:2 host-wc 'a'))"),
+
+#undef T
+};
+
+static const struct EvalTest {
+    const char *file;
+    int line;
+    const char *expr;
+    const char *host;
+    unsigned port;
+    bool output;
+} evaltests[] = {
+#define T(expr_, host_, port_, output_) { \
+        .file=__FILE__, .line=__LINE__, \
+        .expr=expr_, .host=host_, .port=port_, .output=output_}
+
+    T("*.example.com", "hostname.example.com", 22, true),
+    T("*.example.com", "hostname.example.org", 22, false),
+    T("*.example.com", "hostname.dept.example.com", 22, true),
+    T("*.example.com && port:22", "hostname.example.com", 21, false),
+    T("*.example.com && port:22", "hostname.example.com", 22, true),
+    T("*.example.com && port:22", "hostname.example.com", 23, false),
+    T("*.example.com && port:22-24", "hostname.example.com", 21, false),
+    T("*.example.com && port:22-24", "hostname.example.com", 22, true),
+    T("*.example.com && port:22-24", "hostname.example.com", 23, true),
+    T("*.example.com && port:22-24", "hostname.example.com", 24, true),
+    T("*.example.com && port:22-24", "hostname.example.com", 25, false),
+
+    T("*a* && *b* && *c*", "", 22, false),
+    T("*a* && *b* && *c*", "a", 22, false),
+    T("*a* && *b* && *c*", "b", 22, false),
+    T("*a* && *b* && *c*", "c", 22, false),
+    T("*a* && *b* && *c*", "ab", 22, false),
+    T("*a* && *b* && *c*", "ac", 22, false),
+    T("*a* && *b* && *c*", "bc", 22, false),
+    T("*a* && *b* && *c*", "abc", 22, true),
+
+    T("*a* || *b* || *c*", "", 22, false),
+    T("*a* || *b* || *c*", "a", 22, true),
+    T("*a* || *b* || *c*", "b", 22, true),
+    T("*a* || *b* || *c*", "c", 22, true),
+    T("*a* || *b* || *c*", "ab", 22, true),
+    T("*a* || *b* || *c*", "ac", 22, true),
+    T("*a* || *b* || *c*", "bc", 22, true),
+    T("*a* || *b* || *c*", "abc", 22, true),
+
+    T("*a* && !*b* && *c*", "", 22, false),
+    T("*a* && !*b* && *c*", "a", 22, false),
+    T("*a* && !*b* && *c*", "b", 22, false),
+    T("*a* && !*b* && *c*", "c", 22, false),
+    T("*a* && !*b* && *c*", "ab", 22, false),
+    T("*a* && !*b* && *c*", "ac", 22, true),
+    T("*a* && !*b* && *c*", "bc", 22, false),
+    T("*a* && !*b* && *c*", "abc", 22, false),
+
+    T("*a* || !*b* || *c*", "", 22, true),
+    T("*a* || !*b* || *c*", "a", 22, true),
+    T("*a* || !*b* || *c*", "b", 22, false),
+    T("*a* || !*b* || *c*", "c", 22, true),
+    T("*a* || !*b* || *c*", "ab", 22, true),
+    T("*a* || !*b* || *c*", "ac", 22, true),
+    T("*a* || !*b* || *c*", "bc", 22, true),
+    T("*a* || !*b* || *c*", "abc", 22, true),
+
+#undef T
+};
+
+int main(int argc, char **argv)
+{
+    if (argc > 1) {
+        /*
+         * Parse an expression from the command line.
+         */
+
+        ptrlen expr = ptrlen_from_asciz(argv[1]);
+        char *error_msg;
+        ptrlen error_loc;
+        ExprNode *en = parse(expr, &error_msg, &error_loc);
+        if (!en) {
+            fprintf(stderr, "ERR:%zu:%zu:%s\n",
+                    (size_t)((const char *)error_loc.ptr - argv[1]),
+                    (size_t)((const char *)ptrlen_end(error_loc) - argv[1]),
+                    error_msg);
+            fprintf(stderr, "%.*s\n", PTRLEN_PRINTF(expr));
+            for (const char *p = expr.ptr, *e = error_loc.ptr; p<e; p++)
+                fputc(' ', stderr);
+            for (size_t i = 0; i < error_loc.len || i < 1; i++)
+                fputc('^', stderr);
+            fputc('\n', stderr);
+            sfree(error_msg);
+            return 1;
+        }
+
+        if (argc > 2) {
+            /*
+             * Test-evaluate against a host/port pair given on the
+             * command line.
+             */
+            const char *host = argv[2];
+            unsigned port = (argc > 3 ? strtoul(argv[3], NULL, 0) : 22);
+            bool result = eval(en, host, port);
+            printf("%s\n", result ? "accept" : "reject");
+        } else {
+            /*
+             * Just dump the result of parsing the expression.
+             */
+            stdio_sink ss[1];
+            stdio_sink_init(ss, stdout);
+            exprnode_dump(BinarySink_UPCAST(ss), en, expr.ptr);
+            put_byte(ss, '\n');
+        }
+
+        exprnode_free(en);
+
+        return 0;
+    } else {
+        /*
+         * Run our automated tests.
+         */
+        size_t pass = 0, fail = 0;
+
+        for (size_t i = 0; i < lenof(parsetests); i++) {
+            const struct ParseTest *test = &parsetests[i];
+
+            ptrlen expr = ptrlen_from_asciz(test->expr);
+            char *error_msg;
+            ptrlen error_loc;
+            ExprNode *en = parse(expr, &error_msg, &error_loc);
+
+            strbuf *output = strbuf_new();
+            if (!en) {
+                put_fmt(output, "ERR:%zu:%zu:%s",
+                        (size_t)((const char *)error_loc.ptr - test->expr),
+                        (size_t)((const char *)ptrlen_end(error_loc) -
+                                 test->expr),
+                        error_msg);
+                sfree(error_msg);
+            } else {
+                exprnode_dump(BinarySink_UPCAST(output), en, expr.ptr);
+                exprnode_free(en);
+            }
+
+            if (ptrlen_eq_ptrlen(ptrlen_from_strbuf(output),
+                                 ptrlen_from_asciz(test->output))) {
+                pass++;
+            } else {
+                fprintf(stderr, "FAIL: parsetests[%zu] @ %s:%d:\n"
+                        "  expression: %s\n"
+                        "  expected:   %s\n"
+                        "  actual:     %s\n",
+                        i, test->file, test->line, test->expr,
+                        test->output, output->s);
+                fail++;
+            }
+
+            strbuf_free(output);
+        }
+
+        for (size_t i = 0; i < lenof(evaltests); i++) {
+            const struct EvalTest *test = &evaltests[i];
+
+            ptrlen expr = ptrlen_from_asciz(test->expr);
+            char *error_msg;
+            ptrlen error_loc;
+            ExprNode *en = parse(expr, &error_msg, &error_loc);
+
+            if (!en) {
+                fprintf(stderr, "FAIL: evaltests[%zu] @ %s:%d:\n"
+                        "  expression:  %s\n"
+                        "  parse error: %zu:%zu:%s\n",
+                        i, test->file, test->line, test->expr,
+                        (size_t)((const char *)error_loc.ptr - test->expr),
+                        (size_t)((const char *)ptrlen_end(error_loc) -
+                                 test->expr),
+                        error_msg);
+                sfree(error_msg);
+            } else {
+                bool output = eval(en, test->host, test->port);
+                if (output == test->output) {
+                    pass++;
+                } else {
+                    fprintf(stderr, "FAIL: evaltests[%zu] @ %s:%d:\n"
+                            "  expression: %s\n"
+                            "  host:       %s\n"
+                            "  port:       %u\n"
+                            "  expected:   %s\n"
+                            "  actual:     %s\n",
+                            i, test->file, test->line, test->expr,
+                            test->host, test->port,
+                            test->output ? "accept" : "reject",
+                            output ? "accept" : "reject");
+                    fail++;
+                }
+                exprnode_free(en);
+            }
+        }
+
+        fprintf(stderr, "pass %zu fail %zu total %zu\n",
+                pass, fail, pass+fail);
+        return fail != 0;
+    }
+}
+
+#endif // TEST

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

@@ -336,7 +336,7 @@ char *conf_get_str_str(Conf *conf, int primary, const char *secondary)
 }
 
 char *conf_get_str_strs(Conf *conf, int primary,
-                       char *subkeyin, char **subkeyout)
+                        char *subkeyin, char **subkeyout)
 {
     struct constkey key;
     struct conf_entry *entry;
@@ -476,7 +476,7 @@ void conf_del_str_str(Conf *conf, int primary, const char *secondary)
         del234(conf->tree, entry);
         free_entry(entry);
     }
- }
+}
 
 void conf_set_filename(Conf *conf, int primary, const Filename *value)
 {

+ 22 - 0
source/putty/utils/host_ca_new_free.c

@@ -0,0 +1,22 @@
+#include "defs.h"
+#include "misc.h"
+#include "storage.h"
+
+host_ca *host_ca_new(void)
+{
+    host_ca *hca = snew(host_ca);
+    memset(hca, 0, sizeof(*hca));
+    hca->opts.permit_rsa_sha1 = false;
+    hca->opts.permit_rsa_sha256 = true;
+    hca->opts.permit_rsa_sha512 = true;
+    return hca;
+}
+
+void host_ca_free(host_ca *hca)
+{
+    sfree(hca->name);
+    sfree(hca->validity_expression);
+    if (hca->ca_public_key)
+        strbuf_free(hca->ca_public_key);
+    sfree(hca);
+}

+ 93 - 0
source/putty/utils/key_components.c

@@ -0,0 +1,93 @@
+#include "ssh.h"
+#include "mpint.h"
+
+key_components *key_components_new(void)
+{
+    key_components *kc = snew(key_components);
+    kc->ncomponents = 0;
+    kc->componentsize = 0;
+    kc->components = NULL;
+    return kc;
+}
+
+static void key_components_add_str(key_components *kc, const char *name,
+                                   KeyComponentType type, ptrlen data)
+{
+    sgrowarray(kc->components, kc->componentsize, kc->ncomponents);
+    size_t n = kc->ncomponents++;
+    kc->components[n].name = dupstr(name);
+    kc->components[n].type = type;
+    kc->components[n].str = strbuf_dup_nm(data);
+}
+
+void key_components_add_text(key_components *kc,
+                             const char *name, const char *value)
+{
+    key_components_add_str(kc, name, KCT_TEXT, ptrlen_from_asciz(value));
+}
+
+void key_components_add_text_pl(key_components *kc,
+                                const char *name, ptrlen value)
+{
+    key_components_add_str(kc, name, KCT_TEXT, value);
+}
+
+void key_components_add_binary(key_components *kc,
+                               const char *name, ptrlen value)
+{
+    key_components_add_str(kc, name, KCT_BINARY, value);
+}
+
+void key_components_add_mp(key_components *kc,
+                           const char *name, mp_int *value)
+{
+    sgrowarray(kc->components, kc->componentsize, kc->ncomponents);
+    size_t n = kc->ncomponents++;
+    kc->components[n].name = dupstr(name);
+    kc->components[n].type = KCT_MPINT;
+    kc->components[n].mp = mp_copy(value);
+}
+
+void key_components_add_uint(key_components *kc,
+                             const char *name, uintmax_t value)
+{
+    mp_int *mpvalue = mp_from_integer(value);
+    key_components_add_mp(kc, name, mpvalue);
+    mp_free(mpvalue);
+}
+
+void key_components_add_copy(key_components *kc,
+                             const char *name, const key_component *value)
+{
+    switch (value->type) {
+      case KCT_TEXT:
+      case KCT_BINARY:
+        key_components_add_str(kc, name, value->type,
+                               ptrlen_from_strbuf(value->str));
+        break;
+      case KCT_MPINT:
+        key_components_add_mp(kc, name, value->mp);
+        break;
+    }
+}
+
+void key_components_free(key_components *kc)
+{
+    for (size_t i = 0; i < kc->ncomponents; i++) {
+        key_component *comp = &kc->components[i];
+        sfree(comp->name);
+        switch (comp->type) {
+          case KCT_MPINT:
+            mp_free(comp->mp);
+            break;
+          case KCT_TEXT:
+          case KCT_BINARY:
+            strbuf_free(comp->str);
+            break;
+          default:
+            unreachable("bad key component type");
+        }
+    }
+    sfree(kc->components);
+    sfree(kc);
+}

+ 9 - 2
source/putty/utils/log_proxy_stderr.c

@@ -7,6 +7,12 @@
 void psb_init(ProxyStderrBuf *psb)
 {
     psb->size = 0;
+    psb->prefix = "proxy";
+}
+
+void psb_set_prefix(ProxyStderrBuf *psb, const char *prefix)
+{
+    psb->prefix = prefix;
 }
 
 void log_proxy_stderr(Plug *plug, ProxyStderrBuf *psb,
@@ -58,7 +64,7 @@ void log_proxy_stderr(Plug *plug, ProxyStderrBuf *psb,
                                     psb->buf[endpos-1] == '\r'))
                 endpos--;
             char *msg = dupprintf(
-                "proxy: %.*s", (int)(endpos - pos), psb->buf + pos);
+                "%s: %.*s", psb->prefix, (int)(endpos - pos), psb->buf + pos);
             plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, msg, 0);
             sfree(msg);
 
@@ -73,7 +79,8 @@ void log_proxy_stderr(Plug *plug, ProxyStderrBuf *psb,
          */
         if (pos == 0 && psb->size == lenof(psb->buf)) {
             char *msg = dupprintf(
-                "proxy (partial line): %.*s", (int)psb->size, psb->buf);
+                "%s (partial line): %.*s", psb->prefix, (int)psb->size,
+                psb->buf);
             plug_log(plug, PLUGLOG_PROXY_MSG, NULL, 0, msg, 0);
             sfree(msg);
 

+ 41 - 0
source/putty/utils/percent_decode.c

@@ -0,0 +1,41 @@
+/*
+ * Decode %-encoding in URL style.
+ */
+
+#include <ctype.h>
+
+#include "misc.h"
+
+void percent_decode_bs(BinarySink *bs, ptrlen data)
+{
+    for (const char *p = data.ptr, *e = ptrlen_end(data); p < e; p++) {
+        char c = *p;
+        if (c == '%' && e-p >= 3 &&
+            isxdigit((unsigned char)p[1]) &&
+            isxdigit((unsigned char)p[2])) {
+            char hex[3];
+            hex[0] = p[1];
+            hex[1] = p[2];
+            hex[2] = '\0';
+            put_byte(bs, strtoul(hex, NULL, 16));
+            p += 2;
+        } else {
+            put_byte(bs, c);
+        }
+    }
+
+}
+
+void percent_decode_fp(FILE *fp, ptrlen data)
+{
+    stdio_sink ss;
+    stdio_sink_init(&ss, fp);
+    percent_decode_bs(BinarySink_UPCAST(&ss), data);
+}
+
+strbuf *percent_decode_sb(ptrlen data)
+{
+    strbuf *sb = strbuf_new();
+    percent_decode_bs(BinarySink_UPCAST(sb), data);
+    return sb;
+}

+ 34 - 0
source/putty/utils/percent_encode.c

@@ -0,0 +1,34 @@
+/*
+ * %-encoding in URL style.
+ *
+ * Defaults to escaping % itself (necessary for decoding to even
+ * work), and any C0 escape character. Further bad characters can be
+ * provided in 'badchars'.
+ */
+
+#include "misc.h"
+
+void percent_encode_bs(BinarySink *bs, ptrlen data, const char *badchars)
+{
+    for (const char *p = data.ptr, *e = ptrlen_end(data); p < e; p++) {
+        char c = *p;
+        if (c == '%' || c < ' ' || (badchars && strchr(badchars, c)))
+            put_fmt(bs, "%%%02X", (unsigned char)c);
+        else
+            put_byte(bs, c);
+    }
+}
+
+void percent_encode_fp(FILE *fp, ptrlen data, const char *badchars)
+{
+    stdio_sink ss;
+    stdio_sink_init(&ss, fp);
+    percent_encode_bs(BinarySink_UPCAST(&ss), data, badchars);
+}
+
+strbuf *percent_encode_sb(ptrlen data, const char *badchars)
+{
+    strbuf *sb = strbuf_new();
+    percent_encode_bs(BinarySink_UPCAST(sb), data, badchars);
+    return sb;
+}

+ 16 - 0
source/putty/utils/ptrlen.c

@@ -54,6 +54,22 @@ bool ptrlen_endswith(ptrlen whole, ptrlen suffix, ptrlen *tail)
     return false;
 }
 
+bool ptrlen_contains(ptrlen input, const char *characters)
+{
+    for (const char *p = input.ptr, *end = p + input.len; p < end; p++)
+        if (strchr(characters, *p))
+            return true;
+    return false;
+}
+
+bool ptrlen_contains_only(ptrlen input, const char *characters)
+{
+    for (const char *p = input.ptr, *end = p + input.len; p < end; p++)
+        if (!strchr(characters, *p))
+            return false;
+    return true;
+}
+
 ptrlen ptrlen_get_word(ptrlen *input, const char *separators)
 {
     const char *p = input->ptr, *end = p + input->len;

+ 41 - 0
source/putty/utils/seat_dialog_text.c

@@ -0,0 +1,41 @@
+/*
+ * Helper routines for dealing with SeatDialogText structures.
+ */
+
+#include <stdarg.h>
+
+#include "putty.h"
+
+SeatDialogText *seat_dialog_text_new(void)
+{
+    SeatDialogText *sdt = snew(SeatDialogText);
+    sdt->nitems = sdt->itemsize = 0;
+    sdt->items = NULL;
+    return sdt;
+}
+
+void seat_dialog_text_free(SeatDialogText *sdt)
+{
+    for (size_t i = 0; i < sdt->nitems; i++)
+        sfree(sdt->items[i].text);
+    sfree(sdt->items);
+    sfree(sdt);
+}
+
+static void seat_dialog_text_append_v(
+    SeatDialogText *sdt, SeatDialogTextType type, const char *fmt, va_list ap)
+{
+    sgrowarray(sdt->items, sdt->itemsize, sdt->nitems);
+    SeatDialogTextItem *item = &sdt->items[sdt->nitems++];
+    item->type = type;
+    item->text = dupvprintf(fmt, ap);
+}
+
+void seat_dialog_text_append(SeatDialogText *sdt, SeatDialogTextType type,
+                             const char *fmt, ...)
+{
+    va_list ap;
+    va_start(ap, fmt);
+    seat_dialog_text_append_v(sdt, type, fmt, ap);
+    va_end(ap);
+}

+ 1 - 1
source/putty/utils/smemeq.c

@@ -8,7 +8,7 @@
 #include "defs.h"
 #include "misc.h"
 
-bool smemeq(const void *av, const void *bv, size_t len)
+unsigned smemeq(const void *av, const void *bv, size_t len)
 {
     const unsigned char *a = (const unsigned char *)av;
     const unsigned char *b = (const unsigned char *)bv;

+ 14 - 0
source/putty/utils/strbuf.c

@@ -112,3 +112,17 @@ void strbuf_finalise_agent_query(strbuf *buf_o)
     assert(buf->visible.len >= 5);
     PUT_32BIT_MSB_FIRST(buf->visible.u, buf->visible.len - 4);
 }
+
+strbuf *strbuf_dup(ptrlen string)
+{
+    strbuf *buf = strbuf_new();
+    put_datapl(buf, string);
+    return buf;
+}
+
+strbuf *strbuf_dup_nm(ptrlen string)
+{
+    strbuf *buf = strbuf_new_nm();
+    put_datapl(buf, string);
+    return buf;
+}

+ 13 - 1
source/putty/utils/tempseat.c

@@ -204,6 +204,17 @@ static bool tempseat_has_mixed_input_stream(Seat *seat)
     return seat_has_mixed_input_stream(ts->realseat);
 }
 
+static const SeatDialogPromptDescriptions *tempseat_prompt_descriptions(
+    Seat *seat)
+{
+    /* It might be OK to put this in the 'unreachable' category, but I
+     * think it's equally good to put it here, which allows for
+     * someone _preparing_ a prompt right now that they intend to
+     * present once the TempSeat has given way to the real one. */
+    TempSeat *ts = container_of(seat, TempSeat, seat);
+    return seat_prompt_descriptions(ts->realseat);
+}
+
 /* ----------------------------------------------------------------------
  * Methods that should never be called on a TempSeat, so we can put an
  * unreachable() in them.
@@ -237,7 +248,7 @@ static size_t tempseat_banner(Seat *seat, const void *data, size_t len)
 
 static SeatPromptResult tempseat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
-    char *keystr, const char *keydisp, char **key_fingerprints, bool mismatch,
+    char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
 {
     unreachable("confirm_ssh_host_key should never be called on TempSeat");
@@ -322,6 +333,7 @@ static const struct SeatVtable tempseat_vt = {
     .confirm_ssh_host_key = tempseat_confirm_ssh_host_key,
     .confirm_weak_crypto_primitive = tempseat_confirm_weak_crypto_primitive,
     .confirm_weak_cached_hostkey = tempseat_confirm_weak_cached_hostkey,
+    .prompt_descriptions = tempseat_prompt_descriptions,
     .is_utf8 = tempseat_is_utf8,
     .echoedit_update = tempseat_echoedit_update,
     .get_x_display = tempseat_get_x_display,

+ 16 - 16
source/putty/utils/tree234.c

@@ -73,7 +73,7 @@ tree234 *newtree234(cmpfn234 cmp)
 /*
  * Free a 2-3-4 tree (not including freeing the elements).
  */
-static void freenode234(node234 * n)
+static void freenode234(node234 *n)
 {
     if (!n)
         return;
@@ -84,7 +84,7 @@ static void freenode234(node234 * n)
     sfree(n);
 }
 
-void freetree234(tree234 * t)
+void freetree234(tree234 *t)
 {
     freenode234(t->root);
     sfree(t);
@@ -93,7 +93,7 @@ void freetree234(tree234 * t)
 /*
  * Internal function to count a node.
  */
-static int countnode234(node234 * n)
+static int countnode234(node234 *n)
 {
     int count = 0;
     int i;
@@ -122,7 +122,7 @@ static int elements234(node234 *n)
 /*
  * Count the elements in a tree.
  */
-int count234(tree234 * t)
+int count234(tree234 *t)
 {
     if (t->root)
         return countnode234(t->root);
@@ -134,7 +134,7 @@ int count234(tree234 * t)
  * Add an element e to a 2-3-4 tree t. Returns e on success, or if
  * an existing element compares equal, returns that.
  */
-static void *add234_internal(tree234 * t, void *e, int index)
+static void *add234_internal(tree234 *t, void *e, int index)
 {
     node234 *n, **np, *left, *right;
     void *orig_e = e;
@@ -463,14 +463,14 @@ static void *add234_internal(tree234 * t, void *e, int index)
     return orig_e;
 }
 
-void *add234(tree234 * t, void *e)
+void *add234(tree234 *t, void *e)
 {
     if (!t->cmp)                       /* tree is unsorted */
         return NULL;
 
     return add234_internal(t, e, -1);
 }
-void *addpos234(tree234 * t, void *e, int index)
+void *addpos234(tree234 *t, void *e, int index)
 {
     if (index < 0 ||                   /* index out of range */
         t->cmp)                        /* tree is sorted */
@@ -483,7 +483,7 @@ void *addpos234(tree234 * t, void *e, int index)
  * Look up the element at a given numeric index in a 2-3-4 tree.
  * Returns NULL if the index is out of range.
  */
-void *index234(tree234 * t, int index)
+void *index234(tree234 *t, int index)
 {
     node234 *n;
 
@@ -523,7 +523,7 @@ void *index234(tree234 * t, int index)
  * as NULL, in which case the compare function from the tree proper
  * will be used.
  */
-void *findrelpos234(tree234 * t, void *e, cmpfn234 cmp,
+void *findrelpos234(tree234 *t, void *e, cmpfn234 cmp,
                     int relation, int *index)
 {
     search234_state ss;
@@ -598,15 +598,15 @@ void *findrelpos234(tree234 * t, void *e, cmpfn234 cmp,
         *index = ss.index;
     return toret;
 }
-void *find234(tree234 * t, void *e, cmpfn234 cmp)
+void *find234(tree234 *t, void *e, cmpfn234 cmp)
 {
     return findrelpos234(t, e, cmp, REL234_EQ, NULL);
 }
-void *findrel234(tree234 * t, void *e, cmpfn234 cmp, int relation)
+void *findrel234(tree234 *t, void *e, cmpfn234 cmp, int relation)
 {
     return findrelpos234(t, e, cmp, relation, NULL);
 }
-void *findpos234(tree234 * t, void *e, cmpfn234 cmp, int *index)
+void *findpos234(tree234 *t, void *e, cmpfn234 cmp, int *index)
 {
     return findrelpos234(t, e, cmp, REL234_EQ, index);
 }
@@ -686,7 +686,7 @@ void search234_step(search234_state *state, int direction)
  * Delete an element e in a 2-3-4 tree. Does not free the element,
  * merely removes all links to it from the tree nodes.
  */
-static void *delpos234_internal(tree234 * t, int index)
+static void *delpos234_internal(tree234 *t, int index)
 {
     node234 *n;
     void *retval;
@@ -1024,13 +1024,13 @@ static void *delpos234_internal(tree234 * t, int index)
         }
     }
 }
-void *delpos234(tree234 * t, int index)
+void *delpos234(tree234 *t, int index)
 {
     if (index < 0 || index >= countnode234(t->root))
         return NULL;
     return delpos234_internal(t, index);
 }
-void *del234(tree234 * t, void *e)
+void *del234(tree234 *t, void *e)
 {
     int index;
     if (!findrelpos234(t, e, NULL, REL234_EQ, &index))
@@ -1095,7 +1095,7 @@ typedef struct {
     int elemcount;
 } chkctx;
 
-int chknode(chkctx * ctx, int level, node234 * node,
+int chknode(chkctx *ctx, int level, node234 *node,
             void *lowbound, void *highbound)
 {
     int nkids, nelems;

+ 4 - 4
source/putty/version.h

@@ -1,5 +1,5 @@
 /* Generated by automated build script */
-#define RELEASE 0.77
-#define TEXTVER "Release 0.77"
-#define SSHVER "-Release-0.77"
-#define BINARY_VERSION 0,77,0,0
+#define RELEASE 0.78
+#define TEXTVER "Release 0.78"
+#define SSHVER "-Release-0.78"
+#define BINARY_VERSION 0,78,0,0

+ 2 - 2
source/putty/windows/agent-client.c

@@ -62,8 +62,8 @@ static void wm_copydata_agent_query(strbuf *query, void **out, int *outlen)
             psd = (PSECURITY_DESCRIPTOR)
                 LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH);
             if (psd) {
-                if (p_InitializeSecurityDescriptor
-                    (psd, SECURITY_DESCRIPTOR_REVISION) &&
+                if (p_InitializeSecurityDescriptor(
+                        psd, SECURITY_DESCRIPTOR_REVISION) &&
                     p_SetSecurityDescriptorOwner(psd, usersid, false)) {
                     sa.nLength = sizeof(sa);
                     sa.bInheritHandle = true;

+ 4 - 5
source/putty/windows/gss.c

@@ -91,8 +91,6 @@ typedef struct winSsh_gss_ctx {
 
 const Ssh_gss_buf gss_mech_krb5={9,"\x2A\x86\x48\x86\xF7\x12\x01\x02\x02"};
 
-const char *gsslogmsg = NULL;
-
 static void ssh_sspi_bind_fns(struct ssh_gss_library *lib);
 
 static tree234 *libraries_to_never_unload;
@@ -227,7 +225,8 @@ struct ssh_gss_liblist *ssh_gss_setup(Conf *conf)
         lib->gsslogmsg = "Using SSPI from SECUR32.DLL";
         lib->handle = (void *)module;
 
-        GET_WINDOWS_FUNCTION(module, AcquireCredentialsHandleA);
+        /* No typecheck because Winelib thinks one PVOID is a PLUID */
+        GET_WINDOWS_FUNCTION_NO_TYPECHECK(module, AcquireCredentialsHandleA);
         GET_WINDOWS_FUNCTION(module, InitializeSecurityContextA);
         GET_WINDOWS_FUNCTION(module, FreeContextBuffer);
         GET_WINDOWS_FUNCTION(module, FreeCredentialsHandle);
@@ -664,8 +663,8 @@ static Ssh_gss_stat ssh_sspi_verify_mic(struct ssh_gss_library *lib,
     InputSecurityToken[1].pvBuffer = mic->value;
 
     winctx->maj_stat = p_VerifySignature(&winctx->context,
-                                       &InputBufferDescriptor,
-                                       0, &qop);
+                                         &InputBufferDescriptor,
+                                         0, &qop);
     return winctx->maj_stat;
 }
 

+ 7 - 0
source/putty/windows/handle-socket.c

@@ -388,6 +388,13 @@ Socket *make_handle_socket(HANDLE send_H, HANDLE recv_H, HANDLE stderr_H,
     return &hs->sock;
 }
 
+void handle_socket_set_psb_prefix(Socket *s, const char *prefix)
+{
+    HandleSocket *hs = container_of(s, HandleSocket, sock);
+    assert(hs->sock.vt == &HandleSocket_sockvt);
+    psb_set_prefix(&hs->psb, prefix);
+}
+
 static void sk_handle_deferred_close(Socket *s)
 {
     HandleSocket *hs = container_of(s, HandleSocket, sock);

Some files were not shown because too many files changed in this diff