瀏覽代碼

PuTTY Pre-release 0.83:2025-01-03.1e45199

Source commit: 1675133a3ca89ff21576d3c554b83fcb225d73c3
Martin Prikryl 9 月之前
父節點
當前提交
8459471bad

+ 2 - 0
source/putty/crypto/ecc-ssh.c

@@ -1615,6 +1615,7 @@ static const ecdh_keyalg ssh_ecdhkex_m_alg = {
     .getpublic = ssh_ecdhkex_m_getpublic,
     .getkey = ssh_ecdhkex_m_getkey,
     .description = ssh_ecdhkex_description,
+    .packet_naming_ctx = SSH2_PKTCTX_ECDHKEX,
 };
 const ssh_kex ssh_ec_kex_curve25519 = {
     .name = "curve25519-sha256",
@@ -1655,6 +1656,7 @@ static const ecdh_keyalg ssh_ecdhkex_w_alg = {
     .getpublic = ssh_ecdhkex_w_getpublic,
     .getkey = ssh_ecdhkex_w_getkey,
     .description = ssh_ecdhkex_description,
+    .packet_naming_ctx = SSH2_PKTCTX_ECDHKEX,
 };
 static const struct eckex_extra kex_extra_nistp256 = { ec_p256 };
 const ssh_kex ssh_ec_kex_nistp256 = {

+ 391 - 0
source/putty/crypto/kex-hybrid.c

@@ -0,0 +1,391 @@
+/*
+ * Centralised machinery for hybridised post-quantum + classical key
+ * exchange setups, using the same message structure as ECDH but the
+ * strings sent each way are the concatenation of a key or ciphertext
+ * of each type, and the output shared secret is obtained by hashing
+ * together both of the sub-methods' outputs.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include "putty.h"
+#include "ssh.h"
+#include "mpint.h"
+
+/* ----------------------------------------------------------------------
+ * Common definitions between client and server sides.
+ */
+
+typedef struct hybrid_alg hybrid_alg;
+
+struct hybrid_alg {
+    const ssh_hashalg *combining_hash;
+    const pq_kemalg *pq_alg;
+    const ssh_kex *classical_alg;
+    void (*reformat)(ptrlen input, BinarySink *output);
+};
+
+static char *hybrid_description(const ssh_kex *kex)
+{
+    const struct hybrid_alg *alg = kex->extra;
+
+    /* Bit of a bodge, but think up a short name to describe the
+     * classical algorithm */
+    const char *classical_name;
+    if (alg->classical_alg == &ssh_ec_kex_curve25519)
+        classical_name = "Curve25519";
+    else if (alg->classical_alg == &ssh_ec_kex_nistp256)
+        classical_name = "NIST P256";
+    else if (alg->classical_alg == &ssh_ec_kex_nistp384)
+        classical_name = "NIST P384";
+    else
+        unreachable("don't have a name for this classical alg");
+
+    return dupprintf("%s / %s hybrid key exchange",
+                     alg->pq_alg->description, classical_name);
+}
+
+static void reformat_mpint_be(ptrlen input, BinarySink *output, size_t bytes)
+{
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, input);
+    mp_int *mp = get_mp_ssh2(src);
+    assert(!get_err(src));
+    assert(get_avail(src) == 0);
+    for (size_t i = bytes; i-- > 0 ;)
+        put_byte(output, mp_get_byte(mp, i));
+    mp_free(mp);
+}
+
+static void reformat_mpint_be_32(ptrlen input, BinarySink *output)
+{
+    reformat_mpint_be(input, output, 32);
+}
+
+static void reformat_mpint_be_48(ptrlen input, BinarySink *output)
+{
+    reformat_mpint_be(input, output, 48);
+}
+
+/* ----------------------------------------------------------------------
+ * Client side.
+ */
+
+typedef struct hybrid_client_state hybrid_client_state;
+
+static const ecdh_keyalg hybrid_client_vt;
+
+struct hybrid_client_state {
+    const hybrid_alg *alg;
+    strbuf *pq_ek;
+    pq_kem_dk *pq_dk;
+    ecdh_key *classical;
+    ecdh_key ek;
+};
+
+static ecdh_key *hybrid_client_new(const ssh_kex *kex, bool is_server)
+{
+    assert(!is_server);
+    hybrid_client_state *s = snew(hybrid_client_state);
+    s->alg = kex->extra;
+    s->ek.vt = &hybrid_client_vt;
+    s->pq_ek = strbuf_new();
+    s->pq_dk = pq_kem_keygen(s->alg->pq_alg, BinarySink_UPCAST(s->pq_ek));
+    s->classical = ecdh_key_new(s->alg->classical_alg, is_server);
+    return &s->ek;
+}
+
+static void hybrid_client_free(ecdh_key *ek)
+{
+    hybrid_client_state *s = container_of(ek, hybrid_client_state, ek);
+    strbuf_free(s->pq_ek);
+    pq_kem_free_dk(s->pq_dk);
+    ecdh_key_free(s->classical);
+    sfree(s);
+}
+
+/*
+ * In the client, getpublic is called first: we make up a KEM key
+ * pair, and send the public key along with a classical DH value.
+ */
+static void hybrid_client_getpublic(ecdh_key *ek, BinarySink *bs)
+{
+    hybrid_client_state *s = container_of(ek, hybrid_client_state, ek);
+    put_datapl(bs, ptrlen_from_strbuf(s->pq_ek));
+    ecdh_key_getpublic(s->classical, bs);
+}
+
+/*
+ * In the client, getkey is called second, after the server sends its
+ * response: we use our KEM private key to decapsulate the server's
+ * ciphertext.
+ */
+static bool hybrid_client_getkey(ecdh_key *ek, ptrlen remoteKey, BinarySink *bs)
+{
+    hybrid_client_state *s = container_of(ek, hybrid_client_state, ek);
+
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, remoteKey);
+
+    ssh_hash *h = ssh_hash_new(s->alg->combining_hash);
+
+    ptrlen pq_ciphertext = get_data(src, s->alg->pq_alg->c_len);
+    if (get_err(src)) {
+        ssh_hash_free(h);
+        return false;                  /* not enough data */
+    }
+    if (!pq_kem_decaps(s->pq_dk, BinarySink_UPCAST(h), pq_ciphertext)) {
+        ssh_hash_free(h);
+        return false;                  /* pq ciphertext didn't validate */
+    }
+
+    ptrlen classical_data = get_data(src, get_avail(src));
+    strbuf *classical_key = strbuf_new();
+    if (!ecdh_key_getkey(s->classical, classical_data,
+                         BinarySink_UPCAST(classical_key))) {
+        ssh_hash_free(h);
+        return false;                  /* classical DH key didn't validate */
+    }
+    s->alg->reformat(ptrlen_from_strbuf(classical_key), BinarySink_UPCAST(h));
+    strbuf_free(classical_key);
+
+    /*
+     * Finish up: compute the final output hash and return it encoded
+     * as a string.
+     */
+    unsigned char hashdata[MAX_HASH_LEN];
+    ssh_hash_final(h, hashdata);
+    put_stringpl(bs, make_ptrlen(hashdata, s->alg->combining_hash->hlen));
+    smemclr(hashdata, sizeof(hashdata));
+
+    return true;
+}
+
+static const ecdh_keyalg hybrid_client_vt = {
+    .new = hybrid_client_new, /* but normally the selector calls this */
+    .free = hybrid_client_free,
+    .getpublic = hybrid_client_getpublic,
+    .getkey = hybrid_client_getkey,
+    .description = hybrid_description,
+    .packet_naming_ctx = SSH2_PKTCTX_HYBRIDKEX,
+};
+
+/* ----------------------------------------------------------------------
+ * Server side.
+ */
+
+typedef struct hybrid_server_state hybrid_server_state;
+
+static const ecdh_keyalg hybrid_server_vt;
+
+struct hybrid_server_state {
+    const hybrid_alg *alg;
+    strbuf *pq_ciphertext;
+    ecdh_key *classical;
+    ecdh_key ek;
+};
+
+static ecdh_key *hybrid_server_new(const ssh_kex *kex, bool is_server)
+{
+    assert(is_server);
+    hybrid_server_state *s = snew(hybrid_server_state);
+    s->alg = kex->extra;
+    s->ek.vt = &hybrid_server_vt;
+    s->pq_ciphertext = strbuf_new_nm();
+    s->classical = ecdh_key_new(s->alg->classical_alg, is_server);
+    return &s->ek;
+}
+
+static void hybrid_server_free(ecdh_key *ek)
+{
+    hybrid_server_state *s = container_of(ek, hybrid_server_state, ek);
+    strbuf_free(s->pq_ciphertext);
+    ecdh_key_free(s->classical);
+    sfree(s);
+}
+
+/*
+ * In the server, getkey is called first: we receive a KEM encryption
+ * key from the client and encapsulate a secret with it. We write the
+ * output secret to bs; the data we'll send to the client is saved to
+ * return from getpublic.
+ */
+static bool hybrid_server_getkey(ecdh_key *ek, ptrlen remoteKey, BinarySink *bs)
+{
+    hybrid_server_state *s = container_of(ek, hybrid_server_state, ek);
+
+    BinarySource src[1];
+    BinarySource_BARE_INIT_PL(src, remoteKey);
+
+    ssh_hash *h = ssh_hash_new(s->alg->combining_hash);
+
+    ptrlen pq_ek = get_data(src, s->alg->pq_alg->ek_len);
+    if (get_err(src)) {
+        ssh_hash_free(h);
+        return false;                  /* not enough data */
+    }
+    if (!pq_kem_encaps(s->alg->pq_alg,
+                       BinarySink_UPCAST(s->pq_ciphertext),
+                       BinarySink_UPCAST(h), pq_ek)) {
+        ssh_hash_free(h);
+        return false;                  /* pq encryption key didn't validate */
+    }
+
+    ptrlen classical_data = get_data(src, get_avail(src));
+    strbuf *classical_key = strbuf_new();
+    if (!ecdh_key_getkey(s->classical, classical_data,
+                         BinarySink_UPCAST(classical_key))) {
+        ssh_hash_free(h);
+        return false;                  /* classical DH key didn't validate */
+    }
+    s->alg->reformat(ptrlen_from_strbuf(classical_key), BinarySink_UPCAST(h));
+    strbuf_free(classical_key);
+
+    /*
+     * Finish up: compute the final output hash and return it encoded
+     * as a string.
+     */
+    unsigned char hashdata[MAX_HASH_LEN];
+    ssh_hash_final(h, hashdata);
+    put_stringpl(bs, make_ptrlen(hashdata, s->alg->combining_hash->hlen));
+    smemclr(hashdata, sizeof(hashdata));
+
+    return true;
+}
+
+static void hybrid_server_getpublic(ecdh_key *ek, BinarySink *bs)
+{
+    hybrid_server_state *s = container_of(ek, hybrid_server_state, ek);
+    put_datapl(bs, ptrlen_from_strbuf(s->pq_ciphertext));
+    ecdh_key_getpublic(s->classical, bs);
+}
+
+static const ecdh_keyalg hybrid_server_vt = {
+    .new = hybrid_server_new, /* but normally the selector calls this */
+    .free = hybrid_server_free,
+    .getkey = hybrid_server_getkey,
+    .getpublic = hybrid_server_getpublic,
+    .description = hybrid_description,
+    .packet_naming_ctx = SSH2_PKTCTX_HYBRIDKEX,
+};
+
+/* ----------------------------------------------------------------------
+ * Selector vtable that instantiates the appropriate one of the above,
+ * depending on is_server.
+ */
+
+static ecdh_key *hybrid_selector_new(const ssh_kex *kex, bool is_server)
+{
+    if (is_server)
+        return hybrid_server_new(kex, is_server);
+    else
+        return hybrid_client_new(kex, is_server);
+}
+
+static const ecdh_keyalg hybrid_selector_vt = {
+    /* This is a never-instantiated vtable which only implements the
+     * functions that don't require an instance. */
+    .new = hybrid_selector_new,
+    .description = hybrid_description,
+    .packet_naming_ctx = SSH2_PKTCTX_HYBRIDKEX,
+};
+
+/* ----------------------------------------------------------------------
+ * Actual KEX methods.
+ */
+
+static const hybrid_alg ssh_ntru_curve25519_hybrid = {
+    .combining_hash = &ssh_sha512,
+    .pq_alg = &ssh_ntru,
+    .classical_alg = &ssh_ec_kex_curve25519,
+    .reformat = reformat_mpint_be_32,
+};
+
+static const ssh_kex ssh_ntru_curve25519 = {
+    .name = "sntrup761x25519-sha512",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha512,
+    .ecdh_vt = &hybrid_selector_vt,
+    .extra = &ssh_ntru_curve25519_hybrid,
+};
+
+static const ssh_kex ssh_ntru_curve25519_openssh = {
+    .name = "[email protected]",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha512,
+    .ecdh_vt = &hybrid_selector_vt,
+    .extra = &ssh_ntru_curve25519_hybrid,
+};
+
+static const ssh_kex *const ntru_hybrid_list[] = {
+    &ssh_ntru_curve25519,
+    &ssh_ntru_curve25519_openssh,
+};
+
+const ssh_kexes ssh_ntru_hybrid_kex = {
+    lenof(ntru_hybrid_list), ntru_hybrid_list,
+};
+
+static const hybrid_alg ssh_mlkem768_curve25519_hybrid = {
+    .combining_hash = &ssh_sha256,
+    .pq_alg = &ssh_mlkem768,
+    .classical_alg = &ssh_ec_kex_curve25519,
+    .reformat = reformat_mpint_be_32,
+};
+
+static const ssh_kex ssh_mlkem768_curve25519 = {
+    .name = "mlkem768x25519-sha256",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &hybrid_selector_vt,
+    .extra = &ssh_mlkem768_curve25519_hybrid,
+};
+
+static const ssh_kex *const mlkem_curve25519_hybrid_list[] = {
+    &ssh_mlkem768_curve25519,
+};
+
+const ssh_kexes ssh_mlkem_curve25519_hybrid_kex = {
+    lenof(mlkem_curve25519_hybrid_list), mlkem_curve25519_hybrid_list,
+};
+
+static const hybrid_alg ssh_mlkem768_p256_hybrid = {
+    .combining_hash = &ssh_sha256,
+    .pq_alg = &ssh_mlkem768,
+    .classical_alg = &ssh_ec_kex_nistp256,
+    .reformat = reformat_mpint_be_32,
+};
+
+static const ssh_kex ssh_mlkem768_p256 = {
+    .name = "mlkem768nistp256-sha256",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha256,
+    .ecdh_vt = &hybrid_selector_vt,
+    .extra = &ssh_mlkem768_p256_hybrid,
+};
+
+static const hybrid_alg ssh_mlkem1024_p384_hybrid = {
+    .combining_hash = &ssh_sha384,
+    .pq_alg = &ssh_mlkem1024,
+    .classical_alg = &ssh_ec_kex_nistp384,
+    .reformat = reformat_mpint_be_48,
+};
+
+static const ssh_kex ssh_mlkem1024_p384 = {
+    .name = "mlkem1024nistp384-sha384",
+    .main_type = KEXTYPE_ECDH,
+    .hash = &ssh_sha384,
+    .ecdh_vt = &hybrid_selector_vt,
+    .extra = &ssh_mlkem1024_p384_hybrid,
+};
+
+static const ssh_kex *const mlkem_nist_hybrid_list[] = {
+    &ssh_mlkem1024_p384,
+    &ssh_mlkem768_p256,
+};
+
+const ssh_kexes ssh_mlkem_nist_hybrid_kex = {
+    lenof(mlkem_nist_hybrid_list), mlkem_nist_hybrid_list,
+};

+ 1090 - 0
source/putty/crypto/mlkem.c

@@ -0,0 +1,1090 @@
+/*
+ * Implementation of ML-KEM, previously known as 'Crystals: Kyber'.
+ */
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include "putty.h"
+#include "ssh.h"
+#include "mlkem.h"
+#include "smallmoduli.h"
+
+/* ----------------------------------------------------------------------
+ * General definitions.
+ */
+
+/*
+ * Arithmetic in this system works mod 3329, which is prime, and
+ * congruent to 1 mod 256 (in fact it's 13*256 + 1), meaning that
+ * 256th roots of unity exist.
+ */
+#define Q 3329
+
+/*
+ * Parameter structure describing a particular instance of ML-KEM.
+ */
+struct mlkem_params {
+    int k;            /* dimensions of the matrices used */
+    int eta_1, eta_2; /* parameters for mlkem_matrix_poly_cbd calls */
+    int d_u, d_v;     /* bit counts to use in lossy compressed encoding */
+};
+
+/*
+ * Specific parameter sets.
+ */
+const mlkem_params mlkem_params_512 = {
+    .k = 2, .eta_1 = 3, .eta_2 = 2, .d_u = 10, .d_v = 4,
+};
+const mlkem_params mlkem_params_768 = {
+    .k = 3, .eta_1 = 2, .eta_2 = 2, .d_u = 10, .d_v = 4,
+};
+const mlkem_params mlkem_params_1024 = {
+    .k = 4, .eta_1 = 2, .eta_2 = 2, .d_u = 11, .d_v = 5,
+};
+#define KMAX 4
+
+/* ----------------------------------------------------------------------
+ * Number-theoretic transform on ring elements.
+ *
+ * The ring R used by ML-KEM is (Z/qZ)[X] / <X^256+1> (where q=3329 as
+ * above). If the quotient polynomial were X^256-1 then it would split
+ * into 256 linear factors, so that R could be expressed as the direct
+ * sum of 256 rings (Z/qZ)[X] / <X-zeta^i> (where zeta is some fixed
+ * primitive 256th root of unity mod q), each isomorphic to Z/qZ
+ * itself. But X^256+1 only splits into 128 _quadratic_ factors, and
+ * hence we can only decompose R as the direct sum of rings of the
+ * form (Z/qZ)[X] / <X^2-zeta^j> for odd j, each a quadratic extension
+ * of Z/qZ, and all mutually nonisomorphic. This means the NTT runs
+ * one pass fewer than you'd "normally" expect, and also, multiplying
+ * two elements of R in their NTT representation is not quite as
+ * trivial as it would normally be - within each component ring of the
+ * direct sum you have to do the multiplication slightly differently
+ * depending on the power of zeta in its quotient polynomial.
+ *
+ * We take zeta=17 to be the canonical primitive 256th root of unity
+ * for NTT purposes.
+ */
+
+/*
+ * First 128 powers of zeta, reordered by bit-reversing the 7-bit
+ * index. That is, the nth element of this array contains
+ * zeta^(bitrev7(n)). Used by the NTT itself.
+ */
+static const uint16_t powers_reversed_order[128] = {
+    1, 1729, 2580, 3289, 2642, 630, 1897, 848, 1062, 1919, 193, 797, 2786,
+    3260, 569, 1746, 296, 2447, 1339, 1476, 3046, 56, 2240, 1333, 1426, 2094,
+    535, 2882, 2393, 2879, 1974, 821, 289, 331, 3253, 1756, 1197, 2304, 2277,
+    2055, 650, 1977, 2513, 632, 2865, 33, 1320, 1915, 2319, 1435, 807, 452,
+    1438, 2868, 1534, 2402, 2647, 2617, 1481, 648, 2474, 3110, 1227, 910, 17,
+    2761, 583, 2649, 1637, 723, 2288, 1100, 1409, 2662, 3281, 233, 756, 2156,
+    3015, 3050, 1703, 1651, 2789, 1789, 1847, 952, 1461, 2687, 939, 2308, 2437,
+    2388, 733, 2337, 268, 641, 1584, 2298, 2037, 3220, 375, 2549, 2090, 1645,
+    1063, 319, 2773, 757, 2099, 561, 2466, 2594, 2804, 1092, 403, 1026, 1143,
+    2150, 2775, 886, 1722, 1212, 1874, 1029, 2110, 2935, 885, 2154,
+};
+
+/*
+ * First 128 _odd_ powers of zeta: the nth element is
+ * zeta^(2*bitrev7(n)+1). Each of these is used for multiplication in
+ * one of the 128 quadratic-extension rings in the NTT decomposition.
+ */
+static const uint16_t powers_odd_reversed_order[128] = {
+    17, 3312, 2761, 568, 583, 2746, 2649, 680, 1637, 1692, 723, 2606, 2288,
+    1041, 1100, 2229, 1409, 1920, 2662, 667, 3281, 48, 233, 3096, 756, 2573,
+    2156, 1173, 3015, 314, 3050, 279, 1703, 1626, 1651, 1678, 2789, 540, 1789,
+    1540, 1847, 1482, 952, 2377, 1461, 1868, 2687, 642, 939, 2390, 2308, 1021,
+    2437, 892, 2388, 941, 733, 2596, 2337, 992, 268, 3061, 641, 2688, 1584,
+    1745, 2298, 1031, 2037, 1292, 3220, 109, 375, 2954, 2549, 780, 2090, 1239,
+    1645, 1684, 1063, 2266, 319, 3010, 2773, 556, 757, 2572, 2099, 1230, 561,
+    2768, 2466, 863, 2594, 735, 2804, 525, 1092, 2237, 403, 2926, 1026, 2303,
+    1143, 2186, 2150, 1179, 2775, 554, 886, 2443, 1722, 1607, 1212, 2117, 1874,
+    1455, 1029, 2300, 2110, 1219, 2935, 394, 885, 2444, 2154, 1175,
+};
+
+/*
+ * Convert a ring element into NTT representation.
+ *
+ * The input v is an array of 256 uint16_t, giving the coefficients of
+ * a polynomial in X, with v[i] being the coefficient of X^i.
+ *
+ * v is modified in place. On output, adjacent pairs of elements of v
+ * give the coefficients of a smaller polynomial in X, with the pair
+ * v[2i],v[2i+1] being the coefficients of X^0 and X^1 respectively in
+ * the ring (Z/qZ)[X] / <X^2 - k>, where k = powers_odd_reversed_order[i].
+ */
+static void mlkem_ntt(uint16_t *v)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+    size_t next_power = 1;
+
+    for (size_t len = 128; len >= 2; len /= 2) {
+        for (size_t start = 0; start < 256; start += 2*len) {
+            uint16_t mult = powers_reversed_order[next_power++];
+            for (size_t j = start; j < start + len; j++) {
+                uint16_t t = reduce(mult * v[j + len], Q, Qrecip);
+                v[j + len] = reduce(v[j] + Q - t, Q, Qrecip);
+                v[j] = reduce(v[j] + t, Q, Qrecip);
+            }
+        }
+    }
+}
+
+/*
+ * Convert back from NTT representation. Exactly inverts mlkem_ntt().
+ */
+static void mlkem_inverse_ntt(uint16_t *v)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+    size_t next_power = 127;
+
+    for (size_t len = 2; len <= 128; len *= 2) {
+        for (size_t start = 0; start < 256; start += 2*len) {
+            uint16_t mult = powers_reversed_order[next_power--];
+            for (size_t j = start; j < start + len; j++) {
+                uint16_t t = v[j];
+                v[j] = reduce(t + v[j + len], Q, Qrecip);
+                v[j + len] = reduce(mult * (v[j + len] + Q - t), Q, Qrecip);
+            }
+        }
+    }
+
+    for (size_t i = 0; i < 256; i++)
+        v[i] = reduce(v[i] * 3303, Q, Qrecip);
+}
+
+/*
+ * Multiply two elements of R in NTT representation.
+ *
+ * The output can alias an input completely, but mustn't alias one
+ * partially.
+ */
+static void mlkem_multiply_ntts(
+    uint16_t *out, const uint16_t *a, const uint16_t *b)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+
+    for (size_t i = 0; i < 128; i++) {
+        uint16_t a0 = a[2*i], a1 = a[2*i+1];
+        uint16_t b0 = b[2*i], b1 = b[2*i+1];
+        uint16_t mult = powers_odd_reversed_order[i];
+        uint16_t a1b1 = reduce(a1 * b1, Q, Qrecip);
+        out[2*i] = reduce(a0 * b0 + a1b1 * mult, Q, Qrecip);
+        out[2*i+1] = reduce(a0 * b1 + a1 * b0, Q, Qrecip);
+    }
+}
+
+/* ----------------------------------------------------------------------
+ * Operations on matrices over the ring R.
+ *
+ * Most of these don't mind whether the matrix contains ring elements
+ * represented directly as polynomials, or in NTT form. The exception
+ * is that mlkem_matrix_mul requires it to be in NTT form (because
+ * multiplying is a huge pain in the ordinary representation).
+ */
+
+typedef struct mlkem_matrix mlkem_matrix;
+struct mlkem_matrix {
+    unsigned nrows, ncols;
+
+    /*
+     * (nrows * ncols * 256) 16-bit integers. Each 256-word block
+     * contains an element of R; the blocks are in in row-major order,
+     * so that (data + 256*(ncols*y + x)) points at the start of the
+     * element in row y column x.
+     */
+    uint16_t *data;
+};
+
+/* Storage used for multiple matrices, to free all at once afterwards */
+typedef struct mlkem_matrix_storage mlkem_matrix_storage;
+struct mlkem_matrix_storage {
+    uint16_t *data;
+    size_t n;                          /* number of ring elements */
+};
+
+/*
+ * Allocate space for multiple matrices. All the arrays of uint16_t
+ * are allocated as a single big array. This makes it easy to free the
+ * whole lot in one go afterwards.
+ *
+ * It also means that the arrays have a fixed memory relationship to
+ * each other, which matters not at all during live use, but
+ * eliminates spurious control-flow divergences in testsc based on
+ * accidents of memory allocation when vectorised code checks two
+ * memory regions to see if they alias. (The compiler-generated
+ * aliasing check must do two comparisons, one for each direction, and
+ * the order of those two regions in memory affects whether the first
+ * comparison decides the second one is necessary.)
+ *
+ * The variadic arguments for this function consist of a sequence of
+ * triples (mlkem_matrix *m, int nrows, int ncols), terminated by a
+ * null matrix pointer.
+ */
+static void mlkem_matrix_alloc(mlkem_matrix_storage *storage, ...)
+{
+    va_list ap;
+    mlkem_matrix *m;
+
+    storage->n = 0;
+    va_start(ap, storage);
+    while ((m = va_arg(ap, mlkem_matrix *)) != NULL) {
+        int nrows = va_arg(ap, int), ncols = va_arg(ap, int);
+        storage->n += nrows * ncols;
+    }
+    va_end(ap);
+
+    storage->data = snewn(256 * storage->n, uint16_t);
+    size_t pos = 0;
+    va_start(ap, storage);
+    while ((m = va_arg(ap, mlkem_matrix *)) != NULL) {
+        int nrows = va_arg(ap, int), ncols = va_arg(ap, int);
+        m->nrows = nrows;
+        m->ncols = ncols;
+        m->data = storage->data + 256 * pos;
+        pos += nrows * ncols;
+    }
+    va_end(ap);
+}
+
+/* Clear and free the storage allocated by mlkem_matrix_alloc. */
+static void mlkem_matrix_storage_free(mlkem_matrix_storage *storage)
+{
+    smemclr(storage->data, 256 * storage->n * sizeof(uint16_t));
+    sfree(storage->data);
+}
+
+/* Add two matrices. */
+static void mlkem_matrix_add(mlkem_matrix *out, const mlkem_matrix *left,
+                             const mlkem_matrix *right)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+
+    assert(out->nrows == left->nrows);
+    assert(out->ncols == left->ncols);
+    assert(out->nrows == right->nrows);
+    assert(out->ncols == right->ncols);
+
+    for (size_t i = 0; i < out->nrows; i++) {
+        for (size_t j = 0; j < out->ncols; j++) {
+            const uint16_t *lv = left->data + 256*(i * left->ncols + j);
+            const uint16_t *rv = right->data + 256*(i * right->ncols + j);
+            uint16_t *ov = out->data + 256*(i * out->ncols + j);
+            for (size_t p = 0; p < 256; p++)
+                ov[p] = reduce(lv[p] + rv[p] , Q, Qrecip);
+        }
+    }
+}
+
+/* Subtract matrices. */
+static void mlkem_matrix_sub(mlkem_matrix *out, const mlkem_matrix *left,
+                             const mlkem_matrix *right)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+
+    assert(out->nrows == left->nrows);
+    assert(out->ncols == left->ncols);
+    assert(out->nrows == right->nrows);
+    assert(out->ncols == right->ncols);
+
+    for (size_t i = 0; i < out->nrows; i++) {
+        for (size_t j = 0; j < out->ncols; j++) {
+            const uint16_t *lv = left->data + 256*(i * left->ncols + j);
+            const uint16_t *rv = right->data + 256*(i * right->ncols + j);
+            uint16_t *ov = out->data + 256*(i * out->ncols + j);
+            for (size_t p = 0; p < 256; p++)
+                ov[p] = reduce(lv[p] + Q - rv[p] , Q, Qrecip);
+        }
+    }
+}
+
+/* Convert every element of a matrix into NTT representation. */
+static void mlkem_matrix_ntt(mlkem_matrix *m)
+{
+    for (size_t i = 0; i < m->nrows * m->ncols; i++)
+        mlkem_ntt(m->data + i * 256);
+}
+
+/* Convert every element of a matrix out of NTT representation. */
+static void mlkem_matrix_inverse_ntt(mlkem_matrix *m)
+{
+    for (size_t i = 0; i < m->nrows * m->ncols; i++)
+        mlkem_inverse_ntt(m->data + i * 256);
+}
+
+/*
+ * Multiply two matrices, assuming their elements to be currently in
+ * NTT representation.
+ *
+ * The left input must have the same number of columns as the right
+ * has rows, in the usual fashion. The output matrix is overwritten.
+ *
+ * If 'left_transposed' is true then the left matrix is used as if
+ * transposed.
+ */
+static void mlkem_matrix_mul(mlkem_matrix *out, const mlkem_matrix *left,
+                             const mlkem_matrix *right, bool left_transposed)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+    size_t left_nrows = (left_transposed ? left->ncols : left->nrows);
+    size_t left_ncols = (left_transposed ? left->nrows : left->ncols);
+
+    assert(out->nrows == left_nrows);
+    assert(left_ncols == right->nrows);
+    assert(right->ncols == out->ncols);
+
+    uint16_t work[256];
+
+    for (size_t i = 0; i < out->nrows; i++) {
+        for (size_t j = 0; j < out->ncols; j++) {
+            uint16_t *thisout = out->data + 256 * (i * out->ncols + j);
+            memset(thisout, 0, 256 * sizeof(uint16_t));
+            for (size_t k = 0; k < right->nrows; k++) {
+                size_t left_index = left_transposed ?
+                    k * left->ncols + i : i * left->ncols + k;
+                const uint16_t *lv = left->data + 256*left_index;
+                const uint16_t *rv = right->data + 256*(k * right->ncols + j);
+                mlkem_multiply_ntts(work, lv, rv);
+                for (size_t p = 0; p < 256; p++)
+                    thisout[p] = reduce(thisout[p] + work[p], Q, Qrecip);
+            }
+        }
+    }
+
+    smemclr(work, sizeof(work));
+}
+
+/* ----------------------------------------------------------------------
+ * Random sampling functions to make up various kinds of randomised
+ * matrix and vector.
+ */
+
+static void mlkem_sample_ntt(uint16_t *output, ptrlen seed); /* forward ref */
+
+/*
+ * Invent a matrix based on a 32-bit random seed rho.
+ *
+ * This matrix is logically part of the public (encryption) key: it's
+ * not transmitted explicitly, but the seed is, so that the receiver
+ * can reconstruct the same matrix. As a result, this function
+ * _doesn't_ have to worry about side channel resistance, or even
+ * leaving data lying around in arrays.
+ */
+static void mlkem_matrix_from_seed(mlkem_matrix *m, const void *rho)
+{
+    for (unsigned r = 0; r < m->nrows; r++) {
+        for (unsigned c = 0; c < m->ncols; c++) {
+            unsigned char seedbuf[34];
+            memcpy(seedbuf, rho, 32);
+            seedbuf[32] = c;
+            seedbuf[33] = r;
+            mlkem_sample_ntt(m->data + 256 * (r * m->nrows + c),
+                             make_ptrlen(seedbuf, sizeof(seedbuf)));
+        }
+    }
+}
+
+/*
+ * Invent a single element of the ring R, uniformly at random, derived
+ * in a specified way from the input random seed.
+ *
+ * Used as a subroutine of mlkem_matrix_from_seed() above. So, for the
+ * same reasons, this doesn't have to worry about side channels,
+ * making the 'rejection sampling' generation technique easy.
+ *
+ * The name SampleNTT (in the official spec) reflects the fact that
+ * the output elements are regarded as being in NTT representation.
+ * But since the NTT is a bijection, and the sampling is from the
+ * uniform probability distribution over R, nothing in this function
+ * actually needs to worry about that.
+ */
+static void mlkem_sample_ntt(uint16_t *output, ptrlen seed)
+{
+    ShakeXOF *sx = shake128_xof_from_input(seed);
+    unsigned char bytebuf[4];
+    bytebuf[3] = '\0';
+
+    for (size_t pos = 0; pos < 256 ;) {
+        /* Read 3 bytes into the low-order end of bytebuf. The fourth
+         * byte is always 0, so this gives us a random 24-bit integer. */
+        shake_xof_read(sx, &bytebuf, 3);
+        uint32_t random24 = GET_32BIT_LSB_FIRST(bytebuf);
+
+        /*
+         * Split that integer up into two 12-bit ones, and use each
+         * one if it's in range (taking care for the second one that
+         * we didn't just reach the end of the buffer).
+         *
+         * This function is only used for generating matrices from an
+         * element of the public key, so we can use data-dependent
+         * control flow here without worrying about giving away
+         * secrets.
+         */
+        uint16_t d1 = random24 & 0xFFF;
+        uint16_t d2 = random24 >> 12;
+        if (d1 < Q)
+            output[pos++] = d1;
+        if (d2 < Q && pos < 256)
+            output[pos++] = d2;
+    }
+
+    shake_xof_free(sx);
+}
+
+/*
+ * Invent a random vector, with its elements _not_ in NTT
+ * representation, and all the coefficients very small integers (a lot
+ * smaller than q) of one sign or the other.
+ *
+ * eta is a parameter of the probability distribution, sigma is an
+ * input 32-byte random seed. Each element of the vector is made by a
+ * separate hash operation based on sigma plus a distinguishing
+ * integer suffix; 'offset' indicates the starting point for those
+ * suffixes, so that the ith output value has suffix (offset+i).
+ */
+static void mlkem_matrix_poly_cbd(
+    mlkem_matrix *v, int eta, const void *sigma, int offset)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(Q);
+
+    unsigned char seedbuf[33];
+    memcpy(seedbuf, sigma, 32);
+
+    unsigned char *randombuf = snewn(eta * 64, unsigned char);
+
+    for (unsigned r = 0; r < v->nrows * v->ncols; r++) {
+        seedbuf[32] = r + offset;
+        ShakeXOF *sx = shake256_xof_from_input(make_ptrlen(seedbuf, 33));
+        shake_xof_read(sx, randombuf, eta * 64);
+        shake_xof_free(sx);
+
+        for (size_t i = 0; i < 256; i++) {
+            unsigned x = 0, y = 0;
+            for (size_t j = 0; j < eta; j++) {
+                size_t bitpos = 2 * i * eta + j;
+                x += 1 & ((randombuf[bitpos >> 3]) >> (bitpos & 7));
+            }
+            for (size_t j = 0; j < eta; j++) {
+                size_t bitpos = 2 * i * eta + eta + j;
+                y += 1 & ((randombuf[bitpos >> 3]) >> (bitpos & 7));
+            }
+            v->data[256 * r + i] = reduce(x + Q - y, Q, Qrecip);
+        }
+    }
+    smemclr(seedbuf, sizeof(seedbuf));
+    smemclr(randombuf, eta * 64);
+    sfree(randombuf);
+}
+
+/* ----------------------------------------------------------------------
+ * Byte-encoding and decoding functions.
+ */
+
+/*
+ * Losslessly encode one or more elements of the ring R.
+ *
+ * Each polynomial coefficient, in the range [0,q), is represented as
+ * a 12-bit integer. So encoding an entire ring element requires
+ * (256*12)/8 = 384 bytes, and if that 384-byte string were
+ * interpreted as a little-endian 3072-bit integer D, then the
+ * coefficient of X^i could be recovered as (D >> (12*i)) & 0xFFF.
+ *
+ * The input is expected to be an array of 256*n uint16_t (often the
+ * 'data' pointer in an mlkem_matrix). The output is 384*n bytes.
+ */
+static void mlkem_byte_encode_lossless(
+    void *outv, const uint16_t *in, size_t n)
+{
+    unsigned char *out = (unsigned char *)outv;
+    uint32_t buffer = 0, bufbits = 0;
+    for (size_t i = 0; i < 256*n; i++) {
+        buffer |= (uint32_t) in[i] << bufbits;
+        bufbits += 12;
+        while (bufbits >= 8) {
+            *out++ = buffer & 0xFF;
+            buffer >>= 8;
+            bufbits -= 8;
+        }
+    }
+}
+
+/*
+ * Decode a string written by mlkem_byte_encode_lossless.
+ *
+ * Each 12-bit value extracted from the input data is checked to make
+ * sure it's in the range [0,q); if it's out of range, the whole
+ * function fails and returns false. (But it need not do so in
+ * constant time, because that's an "abandon the whole connection"
+ * error, not a "subtly make things not work for the attacker" error.)
+ */
+static bool mlkem_byte_decode_lossless(
+    uint16_t *out, const void *inv, size_t n)
+{
+    const unsigned char *in = (const unsigned char *)inv;
+    uint32_t buffer = 0, bufbits = 0;
+    for (size_t i = 0; i < 384*n; i++) {
+        buffer |= (uint32_t) in[i] << bufbits;
+        bufbits += 8;
+        while (bufbits >= 12) {
+            uint16_t value = buffer & 0xFFF;
+            if (value >= Q)
+                return false;
+            *out++ = value;
+            buffer >>= 12;
+            bufbits -= 12;
+        }
+    }
+
+    return true;
+}
+
+/*
+ * Lossily encode one or more elements of R, using d bits for each
+ * polynomial coefficient, for some d < 12. Each output d-bit value is
+ * obtained as if by regarding the input coefficient as an integer in
+ * the range [0,q), multiplying by 2^d/q, and rounding to the nearest
+ * integer. (Since q is odd, 'round to nearest' can't have a tie.)
+ *
+ * This means that a large enough input coefficient can round up to
+ * 2^d itself. In that situation the output d-bit value is 0.
+ */
+static void mlkem_byte_encode_compressed(
+    void *outv, const uint16_t *in, unsigned d, size_t n)
+{
+    const uint64_t Qrecip = reciprocal_for_reduction(2*Q);
+
+    unsigned char *out = (unsigned char *)outv;
+    uint32_t buffer = 0, bufbits = 0;
+    for (size_t i = 0; i < 256*n; i++) {
+        uint32_t dividend = ((uint32_t)in[i] << (d+1)) + Q;
+        uint32_t quotient;
+        reduce_with_quot(dividend, &quotient, 2*Q, Qrecip);
+        buffer |= (uint32_t) (quotient & ((1 << d) - 1)) << bufbits;
+        bufbits += d;
+        while (bufbits >= 8) {
+            *out++ = buffer & 0xFF;
+            buffer >>= 8;
+            bufbits -= 8;
+        }
+    }
+}
+
+/*
+ * Decode the lossily encoded output of mlkem_byte_encode_compressed.
+ *
+ * Each d-bit chunk of the encoding is converted back into a
+ * polynomial coefficient as if by multiplying by q/2^d and then
+ * rounding to nearest. Unlike the rounding in the encode step, this
+ * _can_ have a tie when an unrounded value is half way between two
+ * integers. Ties are broken by rounding up (as if the whole rounding
+ * were performed by the simple rounding method of adding 1/2 and then
+ * truncating).
+ *
+ * Unlike the lossless decode function, this one can't fail input
+ * validation, because any d-bit value generates some legal
+ * coefficient.
+ */
+static void mlkem_byte_decode_compressed(
+    uint16_t *out, const void *inv, unsigned d, size_t n)
+{
+    const unsigned char *in = (const unsigned char *)inv;
+    uint32_t buffer = 0, bufbits = 0;
+    for (size_t i = 0; i < 32*d*n; i++) {
+        buffer |= (uint32_t) in[i] << bufbits;
+        bufbits += 8;
+        while (bufbits >= d) {
+            uint32_t value = buffer & ((1 << d) - 1);
+            *out++ = (value * (2*Q) + (1 << d)) >> (d + 1);;
+            buffer >>= d;
+            bufbits -= d;
+        }
+    }
+}
+
+/* ----------------------------------------------------------------------
+ * The top-level ML-KEM functions.
+ */
+
+/*
+ * Innermost keygen function, exposed for side-channel testing, with
+ * separate random values rho (public) and sigma (private), so that
+ * testsc can vary sigma while leaving rho the same.
+ */
+void mlkem_keygen_rho_sigma(
+    BinarySink *ek_out, BinarySink *dk_out, const mlkem_params *params,
+    const void *rho, const void *sigma, const void *z)
+{
+    mlkem_matrix_storage storage[1];
+    mlkem_matrix a[1], s[1], e[1], t[1];
+    mlkem_matrix_alloc(storage,
+                       a, params->k, params->k,
+                       s, params->k, 1,
+                       e, params->k, 1,
+                       t, params->k, 1,
+                       (mlkem_matrix *)NULL);
+
+    /*
+     * Make a random k x k matrix A (regarded as in NTT form).
+     */
+    mlkem_matrix_from_seed(a, rho);
+
+    /*
+     * Make two column vectors s and e, with all components having
+     * small polynomial coefficients, and then convert them _into_ NTT
+     * form.
+     */
+    mlkem_matrix_poly_cbd(s, params->eta_1, sigma, 0);
+    mlkem_matrix_poly_cbd(e, params->eta_1, sigma, params->k);
+    mlkem_matrix_ntt(s);
+    mlkem_matrix_ntt(e);
+
+    /*
+     * Compute the vector t = As + e.
+     */
+    mlkem_matrix_mul(t, a, s, false);
+    mlkem_matrix_add(t, t, e);
+
+    /*
+     * The encryption key is the vector t, plus the random seed rho
+     * from which anyone can reconstruct the matrix A.
+     */
+    unsigned char ek[1568];
+    mlkem_byte_encode_lossless(ek, t->data, params->k);
+    memcpy(ek + 384 * params->k, rho, 32);
+    size_t eklen = 384 * params->k + 32;
+    put_data(ek_out, ek, eklen);
+
+    /*
+     * The decryption key (for the internal "K-PKE" public-key system)
+     * is the vector s.
+     */
+    unsigned char dk[1536];
+    mlkem_byte_encode_lossless(dk, s->data, params->k);
+    size_t dklen = 384 * params->k;
+
+    /*
+     * The decapsulation key, for the full ML-KEM, consists of
+     *  - the decryption key as above
+     *  - the encryption key
+     *  - an extra hash of the encryption key
+     *  - the random value z used for "implicit rejection", aka
+     *    constructing a useless output value if tampering is
+     *    detected. (I think so an attacker can't tell the difference
+     *    between "I was rumbled" and "I was undetected but my attempt
+     *    didn't generate the right key">)
+     */
+    put_data(dk_out, dk, dklen);
+    put_data(dk_out, ek, eklen);
+    ssh_hash *h = ssh_hash_new(&ssh_sha3_256);
+    put_data(h, ek, eklen);
+    unsigned char ekhash[32];
+    ssh_hash_final(h, ekhash);
+    put_data(dk_out, ekhash, 32);
+    put_data(dk_out, z, 32);
+
+    mlkem_matrix_storage_free(storage);
+    smemclr(ek, sizeof(ek));
+    smemclr(ekhash, sizeof(ekhash));
+    smemclr(dk, sizeof(dk));
+}
+
+/*
+ * Internal keygen function as described in the official spec, taking
+ * random values d and z and deterministically constructing a key from
+ * them. The test vectors are expressed in terms of this.
+ */
+void mlkem_keygen_internal(
+    BinarySink *ek, BinarySink *dk, const mlkem_params *params,
+    const void *d, const void *z)
+{
+    /* Hash the input randomness d to make two 32-byte values rho and sigma */
+    unsigned char rho_sigma[64];
+    ssh_hash *h = ssh_hash_new(&ssh_sha3_512);
+    put_data(h, d, 32);
+    put_byte(h, params->k);
+    ssh_hash_final(h, rho_sigma);
+    mlkem_keygen_rho_sigma(ek, dk, params, rho_sigma, rho_sigma + 32, z);
+    smemclr(rho_sigma, sizeof(rho_sigma));
+}
+
+/*
+ * Keygen function for live use, making up the values at random.
+ */
+void mlkem_keygen(
+    BinarySink *ek, BinarySink *dk, const mlkem_params *params)
+{
+    unsigned char dz[64];
+    random_read(dz, 64);
+    mlkem_keygen_internal(ek, dk, params, dz, dz + 32);
+    smemclr(dz, sizeof(dz));
+}
+
+/*
+ * Internal encapsulation function from the official spec, taking a
+ * random value m as input and behaving deterministically. Again used
+ * for test vectors.
+ */
+bool mlkem_encaps_internal(
+    BinarySink *c_out, BinarySink *k_out,
+    const mlkem_params *params, ptrlen ek, const void *m)
+{
+    mlkem_matrix_storage storage[1];
+    mlkem_matrix t[1], a[1], y[1], e1[1], e2[1], mu[1], u[1], v[1];
+    mlkem_matrix_alloc(storage,
+                       t, params->k, 1,
+                       a, params->k, params->k,
+                       y, params->k, 1,
+                       e1, params->k, 1,
+                       e2, 1, 1,
+                       mu, 1, 1,
+                       u, params->k, 1,
+                       v, 1, 1,
+                       (mlkem_matrix *)NULL);
+
+    /*
+     * Validate input: ek must be the correct length, and its encoded
+     * ring elements must not include any 16-bit integer intended to
+     * represent a value mod q which is not in fact in the range [0,q).
+     *
+     * We test the latter property by decoding the matrix t, and
+     * checking the success status returned by the decode.
+     */
+    if (ek.len != 384 * params->k + 32 ||
+        !mlkem_byte_decode_lossless(t->data, ek.ptr, params->k)) {
+        mlkem_matrix_storage_free(storage);
+        return false;
+    }
+
+    /*
+     * Regenerate the same matrix A used by key generation, from the
+     * seed string rho at the end of ek.
+     */
+    mlkem_matrix_from_seed(a, (const unsigned char *)ek.ptr + 384 * params->k);
+
+    /*
+     * Hash the input randomness m, to get the value k we'll use as
+     * the output shared secret, plus some randomness for making up
+     * the vectors below.
+     */
+    unsigned char kr[64];
+    unsigned char ekhash[32];
+    ssh_hash *h;
+    /* Hash the encryption key */
+    h = ssh_hash_new(&ssh_sha3_256);
+    put_datapl(h, ek);
+    ssh_hash_final(h, ekhash);
+    /* Hash the input randomness m with that hash */
+    h = ssh_hash_new(&ssh_sha3_512);
+    put_data(h, m, 32);
+    put_data(h, ekhash, 32);
+    ssh_hash_final(h, kr);
+    const unsigned char *k = kr, *r = kr + 32;
+
+    /*
+     * Invent random k-element vectors y and e1, and a random scalar
+     * e2 (here represented as a 1x1 matrix for the sake of not
+     * proliferating internal helper functions). All are generated by
+     * poly_cbd (i.e. their ring elements have polynomial coefficients
+     * of small magnitude). y needs to be in NTT form.
+     *
+     * These generations all use r as their seed, which was the second
+     * half of the 64-byte hash of the input m. We pass different
+     * 'offset' values to mlkem_matrix_poly_cbd() to ensure the
+     * generations are probabilistically independent.
+     */
+    mlkem_matrix_poly_cbd(y, params->eta_1, r, 0);
+    mlkem_matrix_ntt(y);
+
+    mlkem_matrix_poly_cbd(e1, params->eta_2, r, params->k);
+    mlkem_matrix_poly_cbd(e2, params->eta_2, r, 2 * params->k);
+
+    /*
+     * Invent a random scalar mu (again imagined as a 1x1 matrix),
+     * this time by doing lossy decompression of the random value m at
+     * 1 bit per polynomial coefficient. That is, all the polynomial
+     * coefficients of mu are either 0 or 1665 = (q+1)/2.
+     *
+     * This generation reuses the _input_ random value m, not either
+     * half of the hash we made of it.
+     */
+    mlkem_byte_decode_compressed(mu->data, m, 1, 1);
+
+    /*
+     * Calculate a k-element vector u = A^T y + e1.
+     *
+     * A and y are in NTT representation, but e1 is not, and we don't
+     * want the output to be in NTT form either. So we perform an
+     * inverse NTT after the multiplication.
+     */
+    mlkem_matrix_mul(u, a, y, true);   /* regard a as transposed */
+    mlkem_matrix_inverse_ntt(u);
+    mlkem_matrix_add(u, u, e1);
+
+    /*
+     * Calculate a scalar v = t^T y + e2 + mu.
+     *
+     * (t and y are column vectors, so t^T y is just a scalar - you
+     * could think of it as the dot product t.y if you preferred.)
+     *
+     * Similarly to above, we multiply t and y which are in NTT
+     * representation, and then perform an inverse NTT before adding
+     * e2 and mu, which aren't.
+     */
+    mlkem_matrix_mul(v, t, y, true);   /* regard t as transposed */
+    mlkem_matrix_inverse_ntt(v);
+    mlkem_matrix_add(v, v, e2);
+    mlkem_matrix_add(v, v, mu);
+
+    /*
+     * The ciphertext consists of u and v, both encoded lossily, with
+     * different numbers of bits retained per element.
+     */
+    char c[1568];
+    mlkem_byte_encode_compressed(c, u->data, params->d_u, params->k);
+    mlkem_byte_encode_compressed(c + 32 * params->k * params->d_u,
+                                 v->data, params->d_v, 1);
+    put_data(c_out, c, 32 * (params->k * params->d_u + params->d_v));
+
+    /*
+     * The output shared secret is just half of the hash of m (the
+     * first half, which we didn't use for generating vectors above).
+     */
+    put_data(k_out, k, 32);
+
+    smemclr(kr, sizeof(kr));
+    mlkem_matrix_storage_free(storage);
+
+    return true;
+}
+
+/*
+ * Encapsulation function for live use, using the real RNG..
+ */
+bool mlkem_encaps(BinarySink *ciphertext, BinarySink *kout,
+                  const mlkem_params *params, ptrlen ek)
+{
+    unsigned char m[32];
+    random_read(m, 32);
+    bool success = mlkem_encaps_internal(ciphertext, kout, params, ek, m);
+    smemclr(m, sizeof(m));
+    return success;
+}
+
+/*
+ * Decapsulation.
+ */
+bool mlkem_decaps(BinarySink *k_out, const mlkem_params *params,
+                  ptrlen dk, ptrlen c)
+{
+    /*
+     * Validation: check the input strings are the right lengths.
+     */
+    if (dk.len != 768 * params->k + 96)
+        return false;
+    if (c.len != 32 * (params->d_u * params->k + params->d_v))
+        return false;
+
+    /*
+     * Further validation: extract the encryption key from the middle
+     * of dk, hash it, and check the hash matches.
+     */
+    const unsigned char *dkp = (const unsigned char *)dk.ptr;
+    const unsigned char *cp = (const unsigned char *)c.ptr;
+    ptrlen ek = make_ptrlen(dkp + 384*params->k, 384*params->k + 32);
+    ssh_hash *h;
+    unsigned char ekhash[32];
+    h = ssh_hash_new(&ssh_sha3_256);
+    put_datapl(h, ek);
+    ssh_hash_final(h, ekhash);
+    if (!smemeq(ekhash, dkp + 768*params->k + 32, 32))
+        return false;
+
+    mlkem_matrix_storage storage[1];
+    mlkem_matrix u[1], v[1], s[1], w[1];
+    mlkem_matrix_alloc(storage,
+                       u, params->k, 1,
+                       v, 1, 1,
+                       s, params->k, 1,
+                       w, 1, 1,
+                       (mlkem_matrix *)NULL);
+    /*
+     * Decode the vector u and the scalar v from the ciphertext. These
+     * won't come out exactly the same as the originals, because of
+     * the lossy compression.
+     */
+    mlkem_byte_decode_compressed(u->data, cp, params->d_u, params->k);
+    mlkem_matrix_ntt(u);
+    mlkem_byte_decode_compressed(v->data, cp + 32 * params->d_u * params->k,
+                                 params->d_v, 1);
+
+    /*
+     * Decode the vector s from the private key.
+     */
+    mlkem_byte_decode_lossless(s->data, dkp, params->k);
+
+    /*
+     * Calculate the scalar w = v - s^T u.
+     *
+     * s and u are in NTT representation, but v isn't, so we
+     * inverse-NTT the product before doing the subtraction. Therefore
+     * w is not in NTT form either.
+     */
+    mlkem_matrix_mul(w, s, u, true);   /* regard s as transposed */
+    mlkem_matrix_inverse_ntt(w);
+    mlkem_matrix_sub(w, v, w);
+
+    /*
+     * The aim is that this reconstructs something close enough to the
+     * random vector mu that was made from the input secret m to
+     * encapsulation, on the grounds that mu's polynomial coefficients
+     * were very widely separated (on opposite sides of the cyclic
+     * additive group of Z/qZ) and the noise added during encryption
+     * all had _small_ polynomial coefficients.
+     *
+     * So we now re-encode this lossily at 1 bit per polynomial
+     * coefficient, and hope that it reconstructs the actual string m.
+     *
+     * However, this _is_ only a hope! The ML-KEM decryption is not a
+     * true mathematical inverse to encryption. With extreme bad luck,
+     * the noise can add up enough that it flips a bit of m, and
+     * everything fails. The parameters are chosen to make this happen
+     * with negligible probability (the same kind of low probability
+     * that makes you not worry about spontaneous hash collisions),
+     * but it's not actually impossible.
+     */
+    unsigned char m[32];
+    mlkem_byte_encode_compressed(m, w->data, 1, 1);
+
+    /*
+     * Now do the key _encapsulation_ again from scratch, using that
+     * secret m as input, and check that it generates the identical
+     * ciphertext. This should catch the above theoretical failure,
+     * but also, it's a defence against malicious intervention in the
+     * key exchange.
+     *
+     * This is also where we get the output secret k from: the
+     * encapsulation function creates it as half of the hash of m.
+     */
+    unsigned char c_regen[1568], k[32];
+    buffer_sink c_sink[1], k_sink[1];
+    buffer_sink_init(c_sink, c_regen, sizeof(c_regen));
+    buffer_sink_init(k_sink, k, sizeof(k));
+    bool success = mlkem_encaps_internal(
+        BinarySink_UPCAST(c_sink), BinarySink_UPCAST(k_sink), params, ek, m);
+    /* If any application of ML-KEM uses a dk given to it by someone
+     * else, then perhaps they have to worry about being given an
+     * invalid one? But in our application we always expect this to
+     * succeed, because dk is generated and used at the same end of
+     * the SSH connection, within the same process, and nobody is
+     * interfering with it. */
+    assert(success && "We generated this dk ourselves, how can it be bad?");
+
+    /*
+     * If mlkem_encaps_internal returned success but delivered the
+     * wrong ciphertext, that's a failure, but we must be careful not
+     * to let the attacker know exactly what went wrong. So we
+     * generate a plausible but wrong substitute output secret.
+     *
+     * k_reject is that secret; for constant-time reasons we generate
+     * it unconditionally.
+     */
+    unsigned char k_reject[32];
+    h = ssh_hash_new(&ssh_shake256_32bytes);
+    put_data(h, dkp + 768 * params->k + 64, 32);
+    put_datapl(h, c);
+    ssh_hash_final(h, k_reject);
+
+    /*
+     * Now replace k with k_reject if the ciphertexts didn't match.
+     */
+    assert((void *)c_sink->out == (void *)(c_regen + c.len));
+    unsigned match = smemeq(c.ptr, c_regen, c.len);
+    unsigned mask = match - 1;
+    for (size_t i = 0; i < 32; i++)
+        k[i] ^= mask & (k[i] ^ k_reject[i]);
+
+    /*
+     * And we're done! Free everything and return whichever secret we
+     * chose.
+     */
+    put_data(k_out, k, 32);
+    mlkem_matrix_storage_free(storage);
+    smemclr(m, sizeof(m));
+    smemclr(c_regen, sizeof(c_regen));
+    smemclr(k, sizeof(k));
+    smemclr(k_reject, sizeof(k_reject));
+    return true;
+}
+
+/* ----------------------------------------------------------------------
+ * Implement the pq_kemalg vtable in terms of the above functions.
+ */
+
+struct mlkem_dk {
+    strbuf *encoded;
+    pq_kem_dk dk;
+};
+
+static pq_kem_dk *mlkem_vt_keygen(const pq_kemalg *alg, BinarySink *ek)
+{
+    struct mlkem_dk *mdk = snew(struct mlkem_dk);
+    mdk->dk.vt = alg;
+    mdk->encoded = strbuf_new_nm();
+    mlkem_keygen(ek, BinarySink_UPCAST(mdk->encoded), alg->extra);
+    return &mdk->dk;
+}
+
+static bool mlkem_vt_encaps(const pq_kemalg *alg, BinarySink *c, BinarySink *k,
+                            ptrlen ek)
+{
+    return mlkem_encaps(c, k, alg->extra, ek);
+}
+
+static bool mlkem_vt_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c)
+{
+    struct mlkem_dk *mdk = container_of(dk, struct mlkem_dk, dk);
+    return mlkem_decaps(k, mdk->dk.vt->extra,
+                        ptrlen_from_strbuf(mdk->encoded), c);
+}
+
+static void mlkem_vt_free_dk(pq_kem_dk *dk)
+{
+    struct mlkem_dk *mdk = container_of(dk, struct mlkem_dk, dk);
+    strbuf_free(mdk->encoded);
+    sfree(mdk);
+}
+
+const pq_kemalg ssh_mlkem512 = {
+    .keygen = mlkem_vt_keygen,
+    .encaps = mlkem_vt_encaps,
+    .decaps = mlkem_vt_decaps,
+    .free_dk = mlkem_vt_free_dk,
+    .extra = &mlkem_params_512,
+    .description = "ML-KEM-512",
+    .ek_len = 384 * 2 + 32,
+    .c_len = 32 * (10 * 2 + 4),
+};
+
+const pq_kemalg ssh_mlkem768 = {
+    .keygen = mlkem_vt_keygen,
+    .encaps = mlkem_vt_encaps,
+    .decaps = mlkem_vt_decaps,
+    .free_dk = mlkem_vt_free_dk,
+    .extra = &mlkem_params_768,
+    .description = "ML-KEM-768",
+    .ek_len = 384 * 3 + 32,
+    .c_len = 32 * (10 * 3 + 4),
+};
+
+const pq_kemalg ssh_mlkem1024 = {
+    .keygen = mlkem_vt_keygen,
+    .encaps = mlkem_vt_encaps,
+    .decaps = mlkem_vt_decaps,
+    .free_dk = mlkem_vt_free_dk,
+    .extra = &mlkem_params_1024,
+    .description = "ML-KEM-1024",
+    .ek_len = 384 * 4 + 32,
+    .c_len = 32 * (11 * 4 + 5),
+};

+ 89 - 0
source/putty/crypto/mlkem.h

@@ -0,0 +1,89 @@
+/*
+ * Internal functions for the ML-KEM cryptosystem, exposed in a header
+ * that is expected to be included only by mlkem.c and test programs.
+ */
+
+#ifndef PUTTY_CRYPTO_MLKEM_H
+#define PUTTY_CRYPTO_MLKEM_H
+
+typedef struct mlkem_params mlkem_params;
+
+extern const mlkem_params mlkem_params_512;
+extern const mlkem_params mlkem_params_768;
+extern const mlkem_params mlkem_params_1024;
+
+/*
+ * ML-KEM key generation.
+ *
+ * The official spec gives two APIs for this function: an outer one
+ * that invents random data from an implicit PRNG parameter, and an
+ * inner one that takes the randomness as explicit input for running
+ * test vectors.
+ *
+ * To make side-channel testing easier, I introduce a third API inside
+ * the latter. The spec's "inner" function takes a parameter 'd'
+ * containing 32 bytes of randomness, which it immediately expands
+ * into a 64-byte hash and then uses the two halves of that hash for
+ * different purposes. My even-more-inner function expects the caller
+ * to have done that hashing already, and to present the two 32-byte
+ * half-hashes rho and sigma separately.
+ *
+ * Rationale: it would be difficult to make the keygen running time
+ * independent of rho, becase the required technique for constructing
+ * a matrix from rho uses rejection sampling, so timing will depend on
+ * how many samples were rejected. Happily, it's also not _necessary_
+ * to make the timing independent of rho, because rho is part of the
+ * _public_ key, so it's sent in clear over the wire anyway. So for
+ * testsc purposes, it's convenient to regard rho as fixed and vary
+ * sigma, so that the timing variations due to rho don't show up as
+ * failures in the test.
+ *
+ * Inputs: 'd', 'z', 'rho' and 'sigma' are all 32-byte random strings.
+ *
+ * Return: the encryption and decryption keys are written to the two
+ * provided BinarySinks.
+ */
+void mlkem_keygen(
+    BinarySink *ek, BinarySink *dk, const mlkem_params *params);
+void mlkem_keygen_internal(
+    BinarySink *ek, BinarySink *dk, const mlkem_params *params,
+    const void *d, const void *z);
+void mlkem_keygen_rho_sigma(
+    BinarySink *ek, BinarySink *dk, const mlkem_params *params,
+    const void *rho, const void *sigma, const void *z);
+
+/*
+ * ML-KEM key encapsulation, with only two forms, the outer (random)
+ * and inner (for test vectors) versions from the spec.
+ *
+ * Inputs: the encryption key from keygen. 'm' should be a 32-byte
+ * random string if provided.
+ *
+ * Returns: if successful, returns true, and writes to the two
+ * BinarySinks a ciphertext to send to the other side, and our copy of
+ * the output shared secret k. If failure, returns false, and the
+ * strbuf pointers aren't filled in at all.
+ */
+bool mlkem_encaps(BinarySink *ciphertext, BinarySink *kout,
+                  const mlkem_params *params, ptrlen ek);
+bool mlkem_encaps_internal(BinarySink *ciphertext, BinarySink *kout,
+                           const mlkem_params *params, ptrlen ek,
+                           const void *m);
+
+/*
+ * ML-KEM key decapsulation. This doesn't use any randomness, so even
+ * the official spec only presents one version of it. (Actually it
+ * defines two functions, but the outer one adds nothing over the
+ * inner one.)
+ *
+ * Inputs: the decryption key from keygen, and the ciphertext output
+ * from encapsulation.
+ *
+ * Returns: false on validation failure, and true otherwise
+ * (regardless of whether the ciphertext was implicitly rejected). The
+ * shared secret k is written to the provided BinarySink.
+ */
+bool mlkem_decaps(BinarySink *k, const mlkem_params *params,
+                  ptrlen dk, ptrlen c);
+
+#endif /* PUTTY_CRYPTO_MLKEM_H */

+ 101 - 413
source/putty/crypto/ntru.c

@@ -79,58 +79,14 @@
 #include "ssh.h"
 #include "mpint.h"
 #include "ntru.h"
-
-/* ----------------------------------------------------------------------
- * Preliminaries: we're going to need to do modular arithmetic on
- * small values (considerably smaller than 2^16), and we need to do it
- * without using integer division which might not be time-safe.
- *
- * The strategy for this is the same as I used in
- * mp_mod_known_integer: see there for the proofs. The basic idea is
- * that we precompute the reciprocal of our modulus as a fixed-point
- * number, and use that to get an approximate quotient which we
- * subtract off. For these integer sizes, precomputing a fixed-point
- * reciprocal of the form (2^48 / modulus) leaves us at most off by 1
- * in the quotient, so there's a single (time-safe) trial subtraction
- * at the end.
- *
- * (It's possible that some speed could be gained by not reducing
- * fully at every step. But then you'd have to carefully identify all
- * the places in the algorithm where things are compared to zero. This
- * was the easiest way to get it all working in the first place.)
- */
-
-/* Precompute the reciprocal */
-static uint64_t reciprocal_for_reduction(uint16_t q)
-{
-    return ((uint64_t)1 << 48) / q;
-}
-
-/* Reduce x mod q, assuming qrecip == reciprocal_for_reduction(q) */
-static uint16_t reduce(uint32_t x, uint16_t q, uint64_t qrecip)
-{
-    uint64_t unshifted_quot = x * qrecip;
-    uint64_t quot = unshifted_quot >> 48;
-    uint16_t reduced = x - quot * q;
-    reduced -= q * (1 & ((q-1 - reduced) >> 15));
-    return reduced;
-}
-
-/* Reduce x mod q as above, but also return the quotient */
-static uint16_t reduce_with_quot(uint32_t x, uint32_t *quot_out,
-                                 uint16_t q, uint64_t qrecip)
-{
-    uint64_t unshifted_quot = x * qrecip;
-    uint64_t quot = unshifted_quot >> 48;
-    uint16_t reduced = x - quot * q;
-    uint64_t extraquot = (1 & ((q-1 - reduced) >> 15));
-    reduced -= extraquot * q;
-    *quot_out = quot + extraquot;
-    return reduced;
-}
+#include "smallmoduli.h"
 
 /* Invert x mod q, assuming it's nonzero. (For time-safety, no check
- * is made for zero; it just returns 0.) */
+ * is made for zero; it just returns 0.)
+ *
+ * Expects qrecip == reciprocal_for_reduction(q). (But it's passed in
+ * as a parameter to save recomputing it, on the theory that the
+ * caller will have had it lying around already in most cases.) */
 static uint16_t invert(uint16_t x, uint16_t q, uint64_t qrecip)
 {
     /* Fermat inversion: compute x^(q-2), since x^(q-1) == 1. */
@@ -1476,14 +1432,7 @@ static void ntru_session_hash(
 }
 
 /* ----------------------------------------------------------------------
- * Top-level key exchange and SSH integration.
- *
- * Although this system borrows the ECDH packet structure, it's unlike
- * true ECDH in that it is completely asymmetric between client and
- * server. So we have two separate vtables of methods for the two
- * sides of the system, and a third vtable containing only the class
- * methods, in particular a constructor which chooses which one to
- * instantiate.
+ * Top-level KEM functions.
  */
 
 /*
@@ -1495,399 +1444,138 @@ static void ntru_session_hash(
 #define q_LIVE 4591
 #define w_LIVE 286
 
-static char *ssh_ntru_description(const ssh_kex *kex)
-{
-    return dupprintf("NTRU Prime / Curve25519 hybrid key exchange");
-}
-
-/*
- * State structure for the client, which takes the role of inventing a
- * key pair and decrypting a secret plaintext sent to it by the server.
- */
-typedef struct ntru_client_key {
+struct ntru_dk {
     NTRUKeyPair *keypair;
-    ecdh_key *curve25519;
-
-    ecdh_key ek;
-} ntru_client_key;
-
-static void ssh_ntru_client_free(ecdh_key *dh);
-static void ssh_ntru_client_getpublic(ecdh_key *dh, BinarySink *bs);
-static bool ssh_ntru_client_getkey(ecdh_key *dh, ptrlen remoteKey,
-                                   BinarySink *bs);
-
-static const ecdh_keyalg ssh_ntru_client_vt = {
-    /* This vtable has no 'new' method, because it's constructed via
-     * the selector vt below */
-    .free = ssh_ntru_client_free,
-    .getpublic = ssh_ntru_client_getpublic,
-    .getkey = ssh_ntru_client_getkey,
-    .description = ssh_ntru_description,
+    strbuf *encoded;
+    pq_kem_dk dk;
 };
 
-static ecdh_key *ssh_ntru_client_new(void)
-{
-    ntru_client_key *nk = snew(ntru_client_key);
-    nk->ek.vt = &ssh_ntru_client_vt;
-
-    nk->keypair = ntru_keygen(p_LIVE, q_LIVE, w_LIVE);
-    nk->curve25519 = ecdh_key_new(&ssh_ec_kex_curve25519, false);
-
-    return &nk->ek;
-}
-
-static void ssh_ntru_client_free(ecdh_key *dh)
+static pq_kem_dk *ntru_vt_keygen(const pq_kemalg *alg, BinarySink *ek)
 {
-    ntru_client_key *nk = container_of(dh, ntru_client_key, ek);
-    ntru_keypair_free(nk->keypair);
-    ecdh_key_free(nk->curve25519);
-    sfree(nk);
+    struct ntru_dk *ndk = snew(struct ntru_dk);
+    ndk->dk.vt = alg;
+    ndk->encoded = strbuf_new_nm();
+    ndk->keypair = ntru_keygen(p_LIVE, q_LIVE, w_LIVE);
+    ntru_encode_pubkey(ndk->keypair->h, p_LIVE, q_LIVE, ek);
+    return &ndk->dk;
 }
 
-static void ssh_ntru_client_getpublic(ecdh_key *dh, BinarySink *bs)
+static bool ntru_vt_encaps(const pq_kemalg *alg, BinarySink *c, BinarySink *k,
+                           ptrlen ek)
 {
-    ntru_client_key *nk = container_of(dh, ntru_client_key, ek);
-
-    /*
-     * The client's public information is a single SSH string
-     * containing the NTRU public key and the Curve25519 public point
-     * concatenated. So write both of those into the output
-     * BinarySink.
-     */
-    ntru_encode_pubkey(nk->keypair->h, p_LIVE, q_LIVE, bs);
-    ecdh_key_getpublic(nk->curve25519, bs);
-}
-
-static bool ssh_ntru_client_getkey(ecdh_key *dh, ptrlen remoteKey,
-                                   BinarySink *bs)
-{
-    ntru_client_key *nk = container_of(dh, ntru_client_key, ek);
-
-    /*
-     * We expect the server to have sent us a string containing a
-     * ciphertext, a confirmation hash, and a Curve25519 public point.
-     * Extract all three.
-     */
     BinarySource src[1];
-    BinarySource_BARE_INIT_PL(src, remoteKey);
+    BinarySource_BARE_INIT_PL(src, ek);
 
-    uint16_t *ciphertext = snewn(p_LIVE, uint16_t);
-    ptrlen ciphertext_encoded = ntru_decode_ciphertext(
-        ciphertext, nk->keypair, src);
-    ptrlen confirmation_hash = get_data(src, 32);
-    ptrlen curve25519_remoteKey = get_data(src, 32);
+    uint16_t *pubkey = snewn(p_LIVE, uint16_t);
+    ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src);
 
     if (get_err(src) || get_avail(src)) {
-        /* Hard-fail if the input wasn't exactly the right length */
-        ring_free(ciphertext, p_LIVE);
+        /* Hard-fail if the input wasn't exactly the right length */ 
+        ring_free(pubkey, p_LIVE);
         return false;
     }
 
-    /*
-     * Main hash object which will combine the NTRU and Curve25519
-     * outputs.
-     */
-    ssh_hash *h = ssh_hash_new(&ssh_sha512);
-
-    /* Reusable buffer for storing various hash outputs. */
-    uint8_t hashdata[64];
-
-    /*
-     * NTRU side.
-     */
-    {
-        /* Decrypt the ciphertext to recover the server's plaintext */
-        uint16_t *plaintext = snewn(p_LIVE, uint16_t);
-        ntru_decrypt(plaintext, ciphertext, nk->keypair);
-
-        /* Make the confirmation hash */
-        ntru_confirmation_hash(hashdata, plaintext, nk->keypair->h,
-                               p_LIVE, q_LIVE);
-
-        /* Check it matches the one the server sent */
-        unsigned ok = smemeq(hashdata, confirmation_hash.ptr, 32);
-
-        /* If not, substitute in rho for the plaintext in the session hash */
-        unsigned mask = ok-1;
-        for (size_t i = 0; i < p_LIVE; i++)
-            plaintext[i] ^= mask & (plaintext[i] ^ nk->keypair->rho[i]);
-
-        /* Compute the session hash, whether or not we did that */
-        ntru_session_hash(hashdata, ok, plaintext, p_LIVE, ciphertext_encoded,
-                          confirmation_hash);
-
-        /* Free temporary values */
-        ring_free(plaintext, p_LIVE);
-        ring_free(ciphertext, p_LIVE);
-
-        /* And put the NTRU session hash into the main hash object. */
-        put_data(h, hashdata, 32);
-    }
-
-    /*
-     * Curve25519 side.
-     */
-    {
-        strbuf *otherkey = strbuf_new_nm();
-
-        /* Call out to Curve25519 to compute the shared secret from that
-         * kex method */
-        bool ok = ecdh_key_getkey(nk->curve25519, curve25519_remoteKey,
-                                  BinarySink_UPCAST(otherkey));
-
-        /* If that failed (which only happens if the other end does
-         * something wrong, like sending a low-order curve point
-         * outside the subgroup it's supposed to), we might as well
-         * just abort and return failure. That's what we'd have done
-         * in standalone Curve25519. */
-        if (!ok) {
-            ssh_hash_free(h);
-            smemclr(hashdata, sizeof(hashdata));
-            strbuf_free(otherkey);
-            return false;
-        }
-
-        /*
-         * ecdh_key_getkey will have returned us a chunk of data
-         * containing an encoded mpint, which is how the Curve25519
-         * output normally goes into the exchange hash. But in this
-         * context we want to treat it as a fixed big-endian 32 bytes,
-         * so extract it from its encoding and put it into the main
-         * hash object in the new format.
-         */
-        BinarySource src[1];
-        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(otherkey));
-        mp_int *curvekey = get_mp_ssh2(src);
-
-        for (unsigned i = 32; i-- > 0 ;)
-            put_byte(h, mp_get_byte(curvekey, i));
+    /* Invent a valid NTRU plaintext. */
+    uint16_t *plaintext = snewn(p_LIVE, uint16_t);
+    ntru_gen_short(plaintext, p_LIVE, w_LIVE);
 
-        mp_free(curvekey);
-        strbuf_free(otherkey);
-    }
-
-    /*
-     * Finish up: compute the final output hash (full 64 bytes of
-     * SHA-512 this time), and return it encoded as a string.
-     */
-    ssh_hash_final(h, hashdata);
-    put_stringpl(bs, make_ptrlen(hashdata, sizeof(hashdata)));
-    smemclr(hashdata, sizeof(hashdata));
+    /* Encrypt the plaintext, and encode the ciphertext into a strbuf,
+     * so we can reuse it for both the session hash and sending to the
+     * client. */
+    uint16_t *ciphertext = snewn(p_LIVE, uint16_t);
+    ntru_encrypt(ciphertext, plaintext, pubkey, p_LIVE, q_LIVE);
+    strbuf *ciphertext_encoded = strbuf_new_nm();
+    ntru_encode_ciphertext(ciphertext, p_LIVE, q_LIVE,
+                           BinarySink_UPCAST(ciphertext_encoded));
+    put_datapl(c, ptrlen_from_strbuf(ciphertext_encoded));
+
+    /* Compute the confirmation hash, and append that to the data sent
+     * to the other side. */
+    uint8_t confhash[32];
+    ntru_confirmation_hash(confhash, plaintext, pubkey, p_LIVE, q_LIVE);
+    put_data(c, confhash, 32);
+
+    /* Compute the session hash, i.e. the output shared secret. */
+    uint8_t sesshash[32];
+    ntru_session_hash(sesshash, 1, plaintext, p_LIVE,
+                      ptrlen_from_strbuf(ciphertext_encoded),
+                      make_ptrlen(confhash, 32));
+    put_data(k, sesshash, 32);
+
+    ring_free(pubkey, p_LIVE);
+    ring_free(plaintext, p_LIVE);
+    ring_free(ciphertext, p_LIVE);
+    strbuf_free(ciphertext_encoded);
+    smemclr(confhash, sizeof(confhash));
+    smemclr(sesshash, sizeof(sesshash));
 
     return true;
 }
 
-/*
- * State structure for the server, which takes the role of inventing a
- * secret plaintext and sending it to the client encrypted with the
- * public key the client sent.
- */
-typedef struct ntru_server_key {
-    uint16_t *plaintext;
-    strbuf *ciphertext_encoded, *confirmation_hash;
-    ecdh_key *curve25519;
-
-    ecdh_key ek;
-} ntru_server_key;
-
-static void ssh_ntru_server_free(ecdh_key *dh);
-static void ssh_ntru_server_getpublic(ecdh_key *dh, BinarySink *bs);
-static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey,
-                                   BinarySink *bs);
-
-static const ecdh_keyalg ssh_ntru_server_vt = {
-    /* This vtable has no 'new' method, because it's constructed via
-     * the selector vt below */
-    .free = ssh_ntru_server_free,
-    .getpublic = ssh_ntru_server_getpublic,
-    .getkey = ssh_ntru_server_getkey,
-    .description = ssh_ntru_description,
-};
-
-static ecdh_key *ssh_ntru_server_new(void)
-{
-    ntru_server_key *nk = snew(ntru_server_key);
-    nk->ek.vt = &ssh_ntru_server_vt;
-
-    nk->plaintext = snewn(p_LIVE, uint16_t);
-    nk->ciphertext_encoded = strbuf_new_nm();
-    nk->confirmation_hash = strbuf_new_nm();
-    ntru_gen_short(nk->plaintext, p_LIVE, w_LIVE);
-
-    nk->curve25519 = ecdh_key_new(&ssh_ec_kex_curve25519, false);
-
-    return &nk->ek;
-}
-
-static void ssh_ntru_server_free(ecdh_key *dh)
+static bool ntru_vt_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c)
 {
-    ntru_server_key *nk = container_of(dh, ntru_server_key, ek);
-    ring_free(nk->plaintext, p_LIVE);
-    strbuf_free(nk->ciphertext_encoded);
-    strbuf_free(nk->confirmation_hash);
-    ecdh_key_free(nk->curve25519);
-    sfree(nk);
-}
+    struct ntru_dk *ndk = container_of(dk, struct ntru_dk, dk);
 
-static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey,
-                                   BinarySink *bs)
-{
-    ntru_server_key *nk = container_of(dh, ntru_server_key, ek);
-
-    /*
-     * In the server, getkey is called first, with the public
-     * information received from the client. We expect the client to
-     * have sent us a string containing a public key and a Curve25519
-     * public point.
-     */
+    /* Expect a string containing a ciphertext and a confirmation hash. */
     BinarySource src[1];
-    BinarySource_BARE_INIT_PL(src, remoteKey);
+    BinarySource_BARE_INIT_PL(src, c);
 
-    uint16_t *pubkey = snewn(p_LIVE, uint16_t);
-    ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src);
-    ptrlen curve25519_remoteKey = get_data(src, 32);
+    uint16_t *ciphertext = snewn(p_LIVE, uint16_t);
+    ptrlen ciphertext_encoded = ntru_decode_ciphertext(
+        ciphertext, ndk->keypair, src);
+    ptrlen confirmation_hash = get_data(src, 32);
 
     if (get_err(src) || get_avail(src)) {
-        /* Hard-fail if the input wasn't exactly the right length */ 
-        ring_free(pubkey, p_LIVE);
+        /* Hard-fail if the input wasn't exactly the right length */
+        ring_free(ciphertext, p_LIVE);
         return false;
     }
 
-    /*
-     * Main hash object which will combine the NTRU and Curve25519
-     * outputs.
-     */
-    ssh_hash *h = ssh_hash_new(&ssh_sha512);
-
-    /* Reusable buffer for storing various hash outputs. */
-    uint8_t hashdata[64];
+    /* Decrypt the ciphertext to recover the sender's plaintext */
+    uint16_t *plaintext = snewn(p_LIVE, uint16_t);
+    ntru_decrypt(plaintext, ciphertext, ndk->keypair);
 
-    /*
-     * NTRU side.
-     */
-    {
-        /* Encrypt the plaintext we generated at construction time,
-         * and encode the ciphertext into a strbuf so we can reuse it
-         * for both the session hash and sending to the client. */
-        uint16_t *ciphertext = snewn(p_LIVE, uint16_t);
-        ntru_encrypt(ciphertext, nk->plaintext, pubkey, p_LIVE, q_LIVE);
-        ntru_encode_ciphertext(ciphertext, p_LIVE, q_LIVE,
-                               BinarySink_UPCAST(nk->ciphertext_encoded));
-        ring_free(ciphertext, p_LIVE);
+    /* Make the confirmation hash */
+    uint8_t confhash[32];
+    ntru_confirmation_hash(confhash, plaintext, ndk->keypair->h,
+                           p_LIVE, q_LIVE);
 
-        /* Compute the confirmation hash, and write it into another
-         * strbuf. */
-        ntru_confirmation_hash(hashdata, nk->plaintext, pubkey,
-                               p_LIVE, q_LIVE);
-        put_data(nk->confirmation_hash, hashdata, 32);
+    /* Check it matches the one the server sent */
+    unsigned ok = smemeq(confhash, confirmation_hash.ptr, 32);
 
-        /* Compute the session hash (which is easy on the server side,
-         * requiring no conditional substitution). */
-        ntru_session_hash(hashdata, 1, nk->plaintext, p_LIVE,
-                          ptrlen_from_strbuf(nk->ciphertext_encoded),
-                          ptrlen_from_strbuf(nk->confirmation_hash));
+    /* If not, substitute in rho for the plaintext in the session hash */
+    unsigned mask = ok-1;
+    for (size_t i = 0; i < p_LIVE; i++)
+        plaintext[i] ^= mask & (plaintext[i] ^ ndk->keypair->rho[i]);
 
-        /* And put the NTRU session hash into the main hash object. */
-        put_data(h, hashdata, 32);
+    /* Compute the session hash, whether or not we did that */
+    uint8_t sesshash[32];
+    ntru_session_hash(sesshash, ok, plaintext, p_LIVE, ciphertext_encoded,
+                      confirmation_hash);
+    put_data(k, sesshash, 32);
 
-        /* Now we can free the public key */
-        ring_free(pubkey, p_LIVE);
-    }
-
-    /*
-     * Curve25519 side.
-     */
-    {
-        strbuf *otherkey = strbuf_new_nm();
-
-        /* Call out to Curve25519 to compute the shared secret from that
-         * kex method */
-        bool ok = ecdh_key_getkey(nk->curve25519, curve25519_remoteKey,
-                                  BinarySink_UPCAST(otherkey));
-        /* As on the client side, abort if Curve25519 reported failure */
-        if (!ok) {
-            ssh_hash_free(h);
-            smemclr(hashdata, sizeof(hashdata));
-            strbuf_free(otherkey);
-            return false;
-        }
-
-        /* As on the client side, decode Curve25519's mpint so we can
-         * re-encode it appropriately for our hash preimage */
-        BinarySource src[1];
-        BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(otherkey));
-        mp_int *curvekey = get_mp_ssh2(src);
-
-        for (unsigned i = 32; i-- > 0 ;)
-            put_byte(h, mp_get_byte(curvekey, i));
-
-        mp_free(curvekey);
-        strbuf_free(otherkey);
-    }
-
-    /*
-     * Finish up: compute the final output hash (full 64 bytes of
-     * SHA-512 this time), and return it encoded as a string.
-     */
-    ssh_hash_final(h, hashdata);
-    put_stringpl(bs, make_ptrlen(hashdata, sizeof(hashdata)));
-    smemclr(hashdata, sizeof(hashdata));
+    ring_free(plaintext, p_LIVE);
+    ring_free(ciphertext, p_LIVE);
+    smemclr(confhash, sizeof(confhash));
+    smemclr(sesshash, sizeof(sesshash));
 
     return true;
 }
 
-static void ssh_ntru_server_getpublic(ecdh_key *dh, BinarySink *bs)
-{
-    ntru_server_key *nk = container_of(dh, ntru_server_key, ek);
-
-    /*
-     * In the server, this function is called after getkey, so we
-     * already have all our pieces prepared. Just concatenate them all
-     * into the 'server's public data' string to go in ECDH_REPLY.
-     */
-    put_datapl(bs, ptrlen_from_strbuf(nk->ciphertext_encoded));
-    put_datapl(bs, ptrlen_from_strbuf(nk->confirmation_hash));
-    ecdh_key_getpublic(nk->curve25519, bs);
-}
-
-/* ----------------------------------------------------------------------
- * Selector vtable that instantiates the appropriate one of the above,
- * depending on is_server.
- */
-static ecdh_key *ssh_ntru_new(const ssh_kex *kex, bool is_server)
+static void ntru_vt_free_dk(pq_kem_dk *dk)
 {
-    if (is_server)
-        return ssh_ntru_server_new();
-    else
-        return ssh_ntru_client_new();
+    struct ntru_dk *ndk = container_of(dk, struct ntru_dk, dk);
+    strbuf_free(ndk->encoded);
+    ntru_keypair_free(ndk->keypair);
+    sfree(ndk);
 }
 
-static const ecdh_keyalg ssh_ntru_selector_vt = {
-    /* This is a never-instantiated vtable which only implements the
-     * functions that don't require an instance. */
-    .new = ssh_ntru_new,
-    .description = ssh_ntru_description,
-};
-
-static const ssh_kex ssh_ntru_curve25519_openssh = {
-    .name = "[email protected]",
-    .main_type = KEXTYPE_ECDH,
-    .hash = &ssh_sha512,
-    .ecdh_vt = &ssh_ntru_selector_vt,
-};
-
-static const ssh_kex ssh_ntru_curve25519 = {
-    /* Same as [email protected] but with an
-     * IANA-assigned name */
-    .name = "sntrup761x25519-sha512",
-    .main_type = KEXTYPE_ECDH,
-    .hash = &ssh_sha512,
-    .ecdh_vt = &ssh_ntru_selector_vt,
-};
-
-static const ssh_kex *const hybrid_list[] = {
-    &ssh_ntru_curve25519,
-    &ssh_ntru_curve25519_openssh,
+const pq_kemalg ssh_ntru = {
+    .keygen = ntru_vt_keygen,
+    .encaps = ntru_vt_encaps,
+    .decaps = ntru_vt_decaps,
+    .free_dk = ntru_vt_free_dk,
+    .description = "NTRU Prime",
+    .ek_len = 1158,
+    .c_len = 1039,
 };
-
-const ssh_kexes ssh_ntru_hybrid_kex = { lenof(hybrid_list), hybrid_list };

+ 73 - 1
source/putty/crypto/sha3.c

@@ -326,4 +326,76 @@ static void shake256_reset(ssh_hash *hash)
         HASHALG_NAMES_BARE("SHAKE" #param),                     \
     }
 
-DEFINE_SHAKE(256, 114);
+DEFINE_SHAKE(256, 114);                /* used by Ed448 */
+DEFINE_SHAKE(256, 32);                 /* used by ML-KEM */
+
+struct ShakeXOF {
+    keccak_state state;
+    unsigned char *buf;
+    size_t bytes_per_transform, pos;
+};
+
+static ShakeXOF *shake_xof_from_input(unsigned bits, ptrlen data)
+{
+    ShakeXOF *sx = snew_plus(ShakeXOF, 200 * 64);
+    sx->buf = snew_plus_get_aux(sx);
+
+    /* Initialise as if we were generating 0 bytes of hash. That way,
+     * keccak_output will do the final accumulation but generate no data. */
+    keccak_shake_init(&sx->state, bits, 0);
+    keccak_accumulate(&sx->state, data.ptr, data.len);
+    keccak_output(&sx->state, NULL);
+
+    sx->bytes_per_transform = 200 - bits/4;
+    sx->pos = 0;
+
+    return sx;
+}
+
+ShakeXOF *shake128_xof_from_input(ptrlen data)
+{
+    return shake_xof_from_input(128, data);
+}
+
+ShakeXOF *shake256_xof_from_input(ptrlen data)
+{
+    return shake_xof_from_input(256, data);
+}
+
+void shake_xof_read(ShakeXOF *sx, void *output_v, size_t size)
+{
+    unsigned char *output = (unsigned char *)output_v;
+
+    while (size > 0) {
+        if (sx->pos == 0) {
+            /* Copy the 64-bit words from the Keccak state into the
+             * output buffer of bytes */
+            for (unsigned y = 0; y < 5; y++)
+                for (unsigned x = 0; x < 5; x++)
+                    PUT_64BIT_LSB_FIRST(sx->buf + 8 * (5*y+x),
+                                        sx->state.A[x][y]);
+        }
+
+        /* Read a chunk from the byte buffer */
+        size_t this_size = sx->bytes_per_transform - sx->pos;
+        if (this_size > size)
+            this_size = size;
+        memcpy(output, sx->buf + sx->pos, this_size);
+        sx->pos += this_size;
+        output += this_size;
+        size -= this_size;
+
+        /* Retransform the Keccak state if we've run out of data */
+        if (sx->pos >= sx->bytes_per_transform) {
+            keccak_transform(sx->state.A);
+            sx->pos = 0;
+        }
+    }
+}
+
+void shake_xof_free(ShakeXOF *sx)
+{
+    smemclr(sx->buf, 200 * 64);
+    smemclr(sx, sizeof(*sx));
+    sfree(sx);
+}

+ 54 - 0
source/putty/crypto/smallmoduli.h

@@ -0,0 +1,54 @@
+/*
+ * Shared code between algorithms whose state consists of a large
+ * collection of residues mod a small prime.
+ */
+
+/*
+ * We need to do modular arithmetic on small values (considerably
+ * smaller than 2^16), and we need to do it without using integer
+ * division which might not be time-safe. Input values might not fit
+ * in a 16-bit int, because we'll also be multiplying mod q.
+ *
+ * The strategy for this is the same as I used in
+ * mp_mod_known_integer: see there for the proofs. The basic idea is
+ * that we precompute the reciprocal of our modulus as a fixed-point
+ * number, and use that to get an approximate quotient which we
+ * subtract off. For these integer sizes, precomputing a fixed-point
+ * reciprocal of the form (2^48 / modulus) leaves us at most off by 1
+ * in the quotient, so there's a single (time-safe) trial subtraction
+ * at the end.
+ *
+ * (It's possible that some speed could be gained by not reducing
+ * fully at every step. But then you'd have to carefully identify all
+ * the places in the algorithm where things are compared to zero. This
+ * was the easiest way to get it all working in the first place.)
+ */
+
+/* Precompute the reciprocal */
+static inline uint64_t reciprocal_for_reduction(uint16_t q)
+{
+    return ((uint64_t)1 << 48) / q;
+}
+
+/* Reduce x mod q, assuming qrecip == reciprocal_for_reduction(q) */
+static inline uint16_t reduce(uint32_t x, uint16_t q, uint64_t qrecip)
+{
+    uint64_t unshifted_quot = x * qrecip;
+    uint64_t quot = unshifted_quot >> 48;
+    uint16_t reduced = x - quot * q;
+    reduced -= q * (1 & ((q-1 - reduced) >> 15));
+    return reduced;
+}
+
+/* Reduce x mod q as above, but also return the quotient */
+static inline uint16_t reduce_with_quot(uint32_t x, uint32_t *quot_out,
+                                        uint16_t q, uint64_t qrecip)
+{
+    uint64_t unshifted_quot = x * qrecip;
+    uint64_t quot = unshifted_quot >> 48;
+    uint16_t reduced = x - quot * q;
+    uint64_t extraquot = (1 & ((q-1 - reduced) >> 15));
+    reduced -= extraquot * q;
+    *quot_out = quot + extraquot;
+    return reduced;
+}

+ 3 - 0
source/putty/defs.h

@@ -187,10 +187,13 @@ 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 pq_kemalg pq_kemalg;
+typedef struct pq_kem_dk pq_kem_dk;
 typedef struct NTRUKeyPair NTRUKeyPair;
 typedef struct NTRUEncodeSchedule NTRUEncodeSchedule;
 typedef struct RFC6979 RFC6979;
 typedef struct RFC6979Result RFC6979Result;
+typedef struct ShakeXOF ShakeXOF;
 
 typedef struct dlgparam dlgparam;
 typedef struct dlgcontrol dlgcontrol;

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

@@ -2385,6 +2385,12 @@ 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{ML-KEM / Curve25519 hybrid} and \q{ML-KEM NIST ECDH hybrid}:
+similar hybrid constructs of \i{ML-KEM}, another lattice-based key
+exchange method intended to be \i{quantum-resistant}. In the former,
+ML-KEM is hybridised with Curve25519; in the latter, with NIST P384
+or P256.
+
 \b \q{\i{ECDH}}: elliptic curve Diffie-Hellman key exchange,
 with a variety of standard curves and hash algorithms.
 

+ 2 - 1
source/putty/doc/index.but

@@ -699,7 +699,8 @@ saved sessions from
 \IM{Streamlined NTRU Prime} Streamlined NTRU Prime
 \IM{Streamlined NTRU Prime} NTRU Prime
 
-\IM{quantum attacks} quantum attacks, resistance to
+\IM{quantum attacks}{quantum-resistant} quantum attacks, resistance to
+\IM{quantum attacks}{quantum-resistant} post-quantum algorithm
 
 \IM{repeat key exchange} repeat key exchange
 \IM{repeat key exchange} key exchange, repeat

+ 39 - 3
source/putty/doc/plink.but

@@ -34,12 +34,12 @@ to include a \c{set} command like the one above.
 This section describes the basics of how to use Plink for
 interactive logins and for automated processes.
 
-Once you've got a console window to type into, you can just type
-\c{plink} on its own to bring up a usage message.  This tells you the
+Once you've got a console window to type into, you can type
+\c{plink --help} to bring up a usage message.  This tells you the
 version of Plink you're using, and gives you a brief summary of how to
 use Plink:
 
-\c C:\>plink
+\c C:\>plink --help
 \c Plink: command-line connection utility
 \c Release 0.82
 \c Usage: plink [options] [user@]host [command]
@@ -243,6 +243,10 @@ This may help Plink's behaviour when it is used in automated
 scripts: using \c{-batch}, if something goes wrong at connection
 time, the batch job will fail rather than hang.
 
+If another program is invoking Plink on your behalf, then you might
+need to arrange that that program passes \c{-batch} to Plink. See
+\k{plink-git} for an example involving Git.
+
 \S2{plink-option-s} \I{-s-plink}\c{-s}: remote command is SSH subsystem
 
 If you specify the \c{-s} option, Plink passes the specified command
@@ -395,6 +399,38 @@ particular web area:
 Any non-interactive command you could usefully run on the server
 command line, you can run in a batch file using Plink in this way.
 
+\H{plink-git} Using Plink with \i{Git}
+
+To use Plink for Git operations performed over SSH, you can set the
+environment variable \i\c{GIT_SSH_COMMAND} to point to Plink.
+
+For example, if you've run PuTTY's full Windows installer and it has
+installed Plink in the default location, you might do this:
+
+\c set GIT_SSH_COMMAND="C:\Program Files\PuTTY\plink.exe"
+
+or if you've put Plink somewhere else then you can do a similar thing
+with a different path.
+
+This environment variable accepts a whole command line, not just an
+executable file name. So you can add Plink options to the end of it if
+you like. For example, if you're using Git in a batch-mode context,
+where your Git jobs are running unattended and nobody is available to
+answer interactive prompts, you might also append the \cq{-batch}
+option (\k{plink-option-batch}):
+
+\c set GIT_SSH_COMMAND="C:\Program Files\PuTTY\plink.exe" -batch
+
+and then if Plink unexpectedly prints a prompt of some kind (for
+example, because the SSH server's host key has changed), your batch
+job will terminate with an error message, instead of stopping and
+waiting for user input that will never arrive.
+
+(However, you don't \e{always} want to do this with Git. If you're
+using Git interactively, you might \e{want} Plink to stop for
+interactive prompts \dash for example, to let you enter a password for
+the SSH server.)
+
 \H{plink-cvs} Using Plink with \i{CVS}
 
 To use Plink with CVS, you need to set the environment variable

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

@@ -32,12 +32,12 @@ to include a \c{set} command like the one above.
 
 \H{pscp-usage} PSCP Usage
 
-Once you've got a console window to type into, you can just type
-\c{pscp} on its own to bring up a usage message.  This tells you the
+Once you've got a console window to type into, you can type
+\c{pscp -h} to bring up a usage message. This tells you the
 version of PSCP you're using, and gives you a brief summary of how to
 use PSCP:
 
-\c C:\>pscp
+\c C:\>pscp -h
 \c PuTTY Secure Copy client
 \c Release 0.82
 \c Usage: pscp [options] [user@]host:source target

+ 55 - 14
source/putty/doc/puttydoc.txt

@@ -3848,6 +3848,12 @@ Chapter 4: Configuring PuTTY
            that commonly-used method, and hopefully also resistant to a new
            class of attacks.
 
+        -  `ML-KEM / Curve25519 hybrid' and `ML-KEM NIST ECDH hybrid':
+           similar hybrid constructs of ML-KEM, another lattice-based key
+           exchange method intended to be quantum-resistant. In the former,
+           ML-KEM is hybridised with Curve25519; in the latter, with NIST
+           P384 or P256.
+
         -  `ECDH': elliptic curve Diffie-Hellman key exchange, with a
            variety of standard curves and hash algorithms.
 
@@ -5637,12 +5643,11 @@ Chapter 5: Using PSCP to transfer files securely
 
    5.2 PSCP Usage
 
-       Once you've got a console window to type into, you can just type
-       `pscp' on its own to bring up a usage message. This tells you the
-       version of PSCP you're using, and gives you a brief summary of how
-       to use PSCP:
+       Once you've got a console window to type into, you can type `pscp -
+       h' to bring up a usage message. This tells you the version of PSCP
+       you're using, and gives you a brief summary of how to use PSCP:
 
-         C:\>pscp
+         C:\>pscp -h
          PuTTY Secure Copy client
          Release 0.82
          Usage: pscp [options] [user@]host:source target
@@ -6559,12 +6564,12 @@ Chapter 7: Using the command-line connection tool Plink
        This section describes the basics of how to use Plink for
        interactive logins and for automated processes.
 
-       Once you've got a console window to type into, you can just type
-       `plink' on its own to bring up a usage message. This tells you the
-       version of Plink you're using, and gives you a brief summary of how
-       to use Plink:
+       Once you've got a console window to type into, you can type `plink -
+       -help' to bring up a usage message. This tells you the version of
+       Plink you're using, and gives you a brief summary of how to use
+       Plink:
 
-         C:\>plink
+         C:\>plink --help
          Plink: command-line connection utility
          Release 0.82
          Usage: plink [options] [user@]host [command]
@@ -6764,6 +6769,10 @@ Chapter 7: Using the command-line connection tool Plink
        scripts: using `-batch', if something goes wrong at connection time,
        the batch job will fail rather than hang.
 
+       If another program is invoking Plink on your behalf, then you might
+       need to arrange that that program passes `-batch' to Plink. See
+       section 7.4 for an example involving Git.
+
 7.2.3.2 `-s': remote command is SSH subsystem
 
        If you specify the `-s' option, Plink passes the specified command
@@ -6915,7 +6924,39 @@ Chapter 7: Using the command-line connection tool Plink
        Any non-interactive command you could usefully run on the server
        command line, you can run in a batch file using Plink in this way.
 
-   7.4 Using Plink with CVS
+   7.4 Using Plink with Git
+
+       To use Plink for Git operations performed over SSH, you can set the
+       environment variable `GIT_SSH_COMMAND' to point to Plink.
+
+       For example, if you've run PuTTY's full Windows installer and it has
+       installed Plink in the default location, you might do this:
+
+         set GIT_SSH_COMMAND="C:\Program Files\PuTTY\plink.exe"
+
+       or if you've put Plink somewhere else then you can do a similar
+       thing with a different path.
+
+       This environment variable accepts a whole command line, not just an
+       executable file name. So you can add Plink options to the end of
+       it if you like. For example, if you're using Git in a batch-mode
+       context, where your Git jobs are running unattended and nobody is
+       available to answer interactive prompts, you might also append the
+       `-batch' option (section 7.2.3.1):
+
+         set GIT_SSH_COMMAND="C:\Program Files\PuTTY\plink.exe" -batch
+
+       and then if Plink unexpectedly prints a prompt of some kind (for
+       example, because the SSH server's host key has changed), your batch
+       job will terminate with an error message, instead of stopping and
+       waiting for user input that will never arrive.
+
+       (However, you don't _always_ want to do this with Git. If you're
+       using Git interactively, you might _want_ Plink to stop for
+       interactive prompts - for example, to let you enter a password for
+       the SSH server.)
+
+   7.5 Using Plink with CVS
 
        To use Plink with CVS, you need to set the environment variable
        `CVS_RSH' to point to Plink:
@@ -6934,7 +6975,7 @@ Chapter 7: Using the command-line connection tool Plink
 
          cvs -d :ext:sessionname:/path/to/repository co module
 
-   7.5 Using Plink with WinCVS
+   7.6 Using Plink with WinCVS
 
        Plink can also be used with WinCVS. Firstly, arrange for Plink to be
        able to connect to a remote host non-interactively, as described in
@@ -6947,7 +6988,7 @@ Chapter 7: Using the command-line connection tool Plink
        on the `Preferences' dialogue box.
 
        Next, select `Command Line' from the WinCVS `Admin' menu, and type a
-       CVS command as in section 7.4, for example:
+       CVS command as in section 7.5, for example:
 
          cvs -d :ext:user@hostname:/path/to/repository co module
 
@@ -12837,4 +12878,4 @@ Appendix I: PuTTY privacy considerations
        cannot make any promises about the behaviour of modified versions
        distributed by other people.
 
-[PuTTY release 0.82]
+[PuTTY pre-release 0.83:2025-01-03.1e45199]

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

@@ -1 +1 @@
-\versionid PuTTY release 0.82
+\versionid PuTTY pre-release 0.83:2025-01-03.1e45199

+ 2 - 0
source/putty/misc.h

@@ -29,6 +29,8 @@ char *dupstr(const char *s);
 wchar_t *dupwcs(const wchar_t *s);
 char *dupcat_fn(const char *s1, ...);
 #define dupcat(...) dupcat_fn(__VA_ARGS__, (const char *)NULL)
+wchar_t *dupwcscat_fn(const wchar_t *s1, ...);
+#define dupwcscat(...) dupwcscat_fn(__VA_ARGS__, (const wchar_t *)NULL)
 char *dupprintf(const char *fmt, ...) PRINTF_LIKE(1, 2);
 char *dupvprintf(const char *fmt, va_list ap);
 void burnstr(char *string);

+ 3 - 0
source/putty/proxy/cproxy.c

@@ -22,6 +22,7 @@ strbuf *chap_response(ptrlen challenge, ptrlen password)
 {
     strbuf *sb = strbuf_new_nm();
     const ssh2_macalg *alg = &ssh_hmac_md5;
+    enable_dit(); /* just in case main() forgot */
     mac_simple(alg, password, challenge, strbuf_append(sb, alg->len));
     return sb;
 }
@@ -75,6 +76,8 @@ void http_digest_response(BinarySink *bs, ptrlen username, ptrlen password,
     const ssh_hashalg *alg = httphashalgs[hash];
     size_t hashlen = httphashlengths[hash];
 
+    enable_dit(); /* just in case main() forgot */
+
     unsigned char ncbuf[4];
     PUT_32BIT_MSB_FIRST(ncbuf, nonce_count);
 

+ 17 - 0
source/putty/putty.h

@@ -4,6 +4,21 @@
 #include <stddef.h>                    /* for wchar_t */
 #include <limits.h>                    /* for INT_MAX */
 
+/*
+ * Declared before including platform.h, because that will refer to it
+ *
+ * An enum for different types of file that a GUI file requester might
+ * focus on. (Our requesters never _insist_ on a particular file type
+ * or extension - there's always an escape hatch to select any file
+ * you want - but the default can be configured.)
+ */
+typedef enum {
+    FILTER_ALL_FILES, /* no particular focus */
+    FILTER_KEY_FILES, /* .ppk */
+    FILTER_DYNLIB_FILES, /* whatever the host platform uses as shared libs */
+    FILTER_SOUND_FILES, /* whatever kind of sound file we can use as bell */
+} FilereqFilter;
+
 #include "defs.h"
 #include "platform.h"
 #include "network.h"
@@ -388,6 +403,8 @@ enum {
     KEX_RSA,
     KEX_ECDH,
     KEX_NTRU_HYBRID,
+    KEX_MLKEM_25519_HYBRID,
+    KEX_MLKEM_NIST_HYBRID,
     KEX_MAX
 };
 

+ 2 - 0
source/putty/settings.c

@@ -30,6 +30,8 @@ static const struct keyvalwhere ciphernames[] = {
  * in sync with those. */
 static const struct keyvalwhere kexnames[] = {
     { "ntru-curve25519",    KEX_NTRU_HYBRID, -1, +1 },
+    { "mlkem-curve25519",   KEX_MLKEM_25519_HYBRID, KEX_NTRU_HYBRID, +1 },
+    { "mlkem-nist",         KEX_MLKEM_NIST_HYBRID, KEX_MLKEM_25519_HYBRID, +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 },

+ 80 - 0
source/putty/ssh.h

@@ -136,6 +136,7 @@ typedef enum {
     SSH2_PKTCTX_DHGROUP,
     SSH2_PKTCTX_DHGEX,
     SSH2_PKTCTX_ECDHKEX,
+    SSH2_PKTCTX_HYBRIDKEX,
     SSH2_PKTCTX_GSSKEX,
     SSH2_PKTCTX_RSAKEX
 } Pkt_KCtx;
@@ -992,6 +993,12 @@ struct ecdh_keyalg {
     void (*getpublic)(ecdh_key *key, BinarySink *bs);
     bool (*getkey)(ecdh_key *key, ptrlen remoteKey, BinarySink *bs);
     char *(*description)(const ssh_kex *kex);
+
+    /* Some things that use this vtable are genuinely elliptic-curve
+     * Diffie-Hellman. Others are hybrid PQ + classical kex methods.
+     * Provide a packet-naming context for use in the SSH log. (Purely
+     * cosmetic.) */
+    Pkt_KCtx packet_naming_ctx;
 };
 static inline ecdh_key *ecdh_key_new(const ssh_kex *kex, bool is_server)
 { return kex->ecdh_vt->new(kex, is_server); }
@@ -1005,6 +1012,52 @@ static inline bool ecdh_key_getkey(ecdh_key *key, ptrlen remoteKey,
 static inline char *ecdh_keyalg_description(const ssh_kex *kex)
 { return kex->ecdh_vt->description(kex); }
 
+/*
+ * vtable for post-quantum key encapsulation methods (things like NTRU
+ * and ML-KEM).
+ *
+ * These work in an asymmetric way that's conceptually more like the
+ * old RSA kex (either SSH-1 or SSH-2) than like Diffie-Hellman. One
+ * party generates a keypair and sends the public key; the other party
+ * invents a secret and encrypts it with the public key; the first
+ * party receives the ciphertext and decrypts it, and now both parties
+ * have the secret.
+ */
+struct pq_kem_dk {
+    const pq_kemalg *vt;
+};
+struct pq_kemalg {
+    /* Generate a key pair, writing the public encryption key in wire
+     * format to ek. Return the decryption key. */
+    pq_kem_dk *(*keygen)(const pq_kemalg *alg, BinarySink *ek);
+    /* Invent and encrypt a secret, writing the ciphertext in wire
+     * format to c and the secret itself to k. Returns false on any
+     * kind of really obvious validation failure of the encryption key. */
+    bool (*encaps)(const pq_kemalg *alg, BinarySink *c, BinarySink *k,
+                   ptrlen ek);
+    /* Decrypt the secret and write it to k. Returns false on
+     * validation failure. However, more competent cryptographic
+     * attacks are rejected in a way that's not obvious, returning a
+     * useless pseudorandom secret. */
+    bool (*decaps)(pq_kem_dk *dk, BinarySink *k, ptrlen c);
+    /* Free a decryption key. */
+    void (*free_dk)(pq_kem_dk *dk);
+
+    const void *extra;
+    const char *description;
+    size_t ek_len, c_len;
+};
+
+static inline pq_kem_dk *pq_kem_keygen(const pq_kemalg *alg, BinarySink *ek)
+{ return alg->keygen(alg, ek); }
+static inline bool pq_kem_encaps(const pq_kemalg *alg, BinarySink *c,
+                                 BinarySink *k, ptrlen ek)
+{ return alg->encaps(alg, c, k, ek); }
+static inline bool pq_kem_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c)
+{ return dk->vt->decaps(dk, k, c); }
+static inline void pq_kem_free_dk(pq_kem_dk *dk)
+{ dk->vt->free_dk(dk); }
+
 /*
  * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5
  * as the mechanism.
@@ -1167,6 +1220,7 @@ extern const ssh_hashalg ssh_sha3_224;
 extern const ssh_hashalg ssh_sha3_256;
 extern const ssh_hashalg ssh_sha3_384;
 extern const ssh_hashalg ssh_sha3_512;
+extern const ssh_hashalg ssh_shake256_32bytes;
 extern const ssh_hashalg ssh_shake256_114bytes;
 extern const ssh_hashalg ssh_blake2b;
 extern const ssh_kexes ssh_diffiehellman_group1;
@@ -1194,6 +1248,12 @@ 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 pq_kemalg ssh_ntru;
+extern const ssh_kexes ssh_mlkem_curve25519_hybrid_kex;
+extern const ssh_kexes ssh_mlkem_nist_hybrid_kex;
+extern const pq_kemalg ssh_mlkem512;
+extern const pq_kemalg ssh_mlkem768;
+extern const pq_kemalg ssh_mlkem1024;
 extern const ssh_keyalg ssh_dsa;
 extern const ssh_keyalg ssh_rsa;
 extern const ssh_keyalg ssh_rsa_sha256;
@@ -1234,6 +1294,13 @@ 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);
 
+/* Shake128/256 extendable output functions (like a hash except you don't
+ * commit up front to how much data you want to get out of it) */
+ShakeXOF *shake128_xof_from_input(ptrlen data);
+ShakeXOF *shake256_xof_from_input(ptrlen data);
+void shake_xof_read(ShakeXOF *sx, void *output_v, size_t size);
+void shake_xof_free(ShakeXOF *sx);
+
 /*
  * On some systems, you have to detect hardware crypto acceleration by
  * asking the local OS API rather than OS-agnostically asking the CPU
@@ -1245,6 +1312,7 @@ bool platform_pmull_neon_available(void);
 bool platform_sha256_neon_available(void);
 bool platform_sha1_neon_available(void);
 bool platform_sha512_neon_available(void);
+bool platform_dit_available(void);
 
 /*
  * PuTTY version number formatted as an SSH version string.
@@ -1728,6 +1796,8 @@ void platform_ssh_share_cleanup(const char *name);
     K(y, SSH2_MSG_KEXRSA_DONE, 32, SSH2_PKTCTX_RSAKEX)                  \
     K(y, SSH2_MSG_KEX_ECDH_INIT, 30, SSH2_PKTCTX_ECDHKEX)               \
     K(y, SSH2_MSG_KEX_ECDH_REPLY, 31, SSH2_PKTCTX_ECDHKEX)              \
+    K(y, SSH2_MSG_KEX_HYBRID_INIT, 30, SSH2_PKTCTX_HYBRIDKEX)           \
+    K(y, SSH2_MSG_KEX_HYBRID_REPLY, 31, SSH2_PKTCTX_HYBRIDKEX)          \
     X(y, SSH2_MSG_USERAUTH_REQUEST, 50)                                 \
     X(y, SSH2_MSG_USERAUTH_FAILURE, 51)                                 \
     X(y, SSH2_MSG_USERAUTH_SUCCESS, 52)                                 \
@@ -1979,3 +2049,13 @@ enum {
     PLUGIN_NOTYPE = 256, /* packet too short to have a type */
     PLUGIN_EOF = 257 /* EOF from auth plugin */
 };
+
+/*
+ * CPU features for security
+ */
+
+#if HAVE_ARM_DIT
+void enable_dit(void);
+#else
+#define enable_dit() ((void)0)
+#endif

+ 7 - 4
source/putty/ssh/gss.h

@@ -3,6 +3,13 @@
 #include "putty.h"
 #include "pgssapi.h"
 
+/* This struct is defined even in NO_GSSAPI mode, so that stubs/no-gss.c can
+ * return an instance of it containing no libraries */
+struct ssh_gss_liblist {
+    struct ssh_gss_library *libraries;
+    int nlibraries;
+};
+
 #ifndef NO_GSSAPI
 
 #define SSH2_GSS_OIDTYPE 0x06
@@ -49,10 +56,6 @@ struct ssh_gss_library;
  * The free function cleans up the structure, and its associated
  * libraries (if any).
  */
-struct ssh_gss_liblist {
-    struct ssh_gss_library *libraries;
-    int nlibraries;
-};
 struct ssh_gss_liblist *ssh_gss_setup(Conf *conf);
 void ssh_gss_cleanup(struct ssh_gss_liblist *list);
 

+ 1 - 1
source/putty/ssh/kex2-client.c

@@ -190,7 +190,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                      ssh_hash_alg(s->exhash)->text_name);
         sfree(desc);
 
-        s->ppl.bpp->pls->kctx = SSH2_PKTCTX_ECDHKEX;
+        s->ppl.bpp->pls->kctx = s->kex_alg->ecdh_vt->packet_naming_ctx;
 
         s->ecdh_key = ecdh_key_new(s->kex_alg, false);
 

+ 2 - 0
source/putty/ssh/ssh.c

@@ -954,6 +954,8 @@ static char *ssh_init(const BackendVtable *vt, Seat *seat,
 {
     Ssh *ssh;
 
+    enable_dit(); /* just in case main() forgot */
+
     ssh = snew(Ssh);
     memset(ssh, 0, sizeof(Ssh));
 

+ 9 - 1
source/putty/ssh/transport2.c

@@ -615,6 +615,14 @@ static void ssh2_write_kexinit_lists(
             preferred_kex[n_preferred_kex++] =
                 &ssh_ntru_hybrid_kex;
             break;
+          case KEX_MLKEM_25519_HYBRID:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_mlkem_curve25519_hybrid_kex;
+            break;
+          case KEX_MLKEM_NIST_HYBRID:
+            preferred_kex[n_preferred_kex++] =
+                &ssh_mlkem_nist_hybrid_kex;
+            break;
           case KEX_WARN:
             /* Flag for later. Don't bother if it's the last in
              * the list. */
@@ -1166,7 +1174,7 @@ static ScanKexinitsResult ssh2_scan_kexinits(
              * Otherwise, any match failure _is_ a fatal error.
              */
             ScanKexinitsResult skr = {
-                .success = false, .error = SKR_UNKNOWN_ID,
+                .success = false, .error = SKR_NO_AGREEMENT,
                 .kind = kexlist_descr[i], .desc = slists[i],
             };
             return skr;

+ 3 - 1
source/putty/sshrand.c

@@ -93,8 +93,10 @@ void random_save_seed(void)
 
 void random_ref(void)
 {
-    if (!random_active++)
+    if (!random_active++) {
+        enable_dit(); /* just in case main() forgot */
         random_create(&ssh_sha256);
+    }
 }
 
 void random_setup_custom(const ssh_hashalg *hash)

+ 4 - 1
source/putty/utils/memory.c

@@ -35,7 +35,10 @@ void *safemalloc(size_t factor1, size_t factor2, size_t addend)
 #ifdef MINEFIELD
     p = minefield_c_malloc(size);
 #elif defined ALLOCATION_ALIGNMENT
-    p = aligned_alloc(ALLOCATION_ALIGNMENT, size);
+    /* aligned_alloc requires the allocation size to be rounded up */
+    p = aligned_alloc(
+        ALLOCATION_ALIGNMENT,
+        (size + ALLOCATION_ALIGNMENT - 1) & ~(ALLOCATION_ALIGNMENT-1));
 #else
     p = malloc(size);
 #endif

+ 5 - 3
source/putty/utils/tree234.c

@@ -1398,7 +1398,8 @@ void findtest(void)
 
             lo = 0;
             hi = arraylen - 1;
-            while (lo <= hi) {
+            assert(lo <= hi);
+            do {
                 mid = (lo + hi) / 2;
                 c = strcmp(p, array[mid]);
                 if (c < 0)
@@ -1407,7 +1408,7 @@ void findtest(void)
                     lo = mid + 1;
                 else
                     break;
-            }
+            } while (lo <= hi);
 
             if (c == 0) {
                 if (rel == REL234_LT)
@@ -1428,10 +1429,11 @@ void findtest(void)
                     ret = NULL;
             }
 
+            index = -1;
             realret = findrelpos234(tree, p, NULL, rel, &index);
             if (realret != ret) {
                 error("find(\"%s\",%s) gave %s should be %s",
-                      p, relnames[j], realret, ret);
+                      p, relnames[j], realret ? realret : "NULL", ret);
             }
             if (realret && index != mid) {
                 error("find(\"%s\",%s) gave %d should be %d",

+ 4 - 4
source/putty/version.h

@@ -1,5 +1,5 @@
 /* Generated by automated build script */
-#define RELEASE 0.82
-#define TEXTVER "Release 0.82"
-#define SSHVER "-Release-0.82"
-#define BINARY_VERSION 0,82,0,0
+#define PRERELEASE 0.83
+#define TEXTVER "Pre-release 0.83:2025-01-03.1e45199"
+#define SSHVER "-Prerelease-0.83:20250103.1e45199"
+#define BINARY_VERSION 0,82,33807,0

+ 16 - 27
source/putty/windows/platform.h

@@ -66,6 +66,7 @@ struct Filename {
     char *cpath, *utf8path;
 };
 Filename *filename_from_wstr(const wchar_t *str);
+const wchar_t *filename_to_wstr(const Filename *fn);
 FILE *f_open(const Filename *filename, const char *mode, bool isprivate);
 
 #ifndef SUPERSEDE_FONTSPEC_FOR_TESTING
@@ -295,27 +296,6 @@ void write_aclip(HWND hwnd, int clipboard, char *, int);
  */
 #define sk_getxdmdata(socket, lenp) (NULL)
 
-/*
- * File-selector filter strings used in the config box. On Windows,
- * these strings are of exactly the type needed to go in
- * `lpstrFilter' in an OPENFILENAME structure.
- */
-typedef const wchar_t *FILESELECT_FILTER_TYPE;
-#define FILTER_KEY_FILES (L"PuTTY Private Key Files (*.ppk)\0*.ppk\0" \
-                          L"All Files (*.*)\0*\0\0\0")
-#define FILTER_WAVE_FILES (L"Wave Files (*.wav)\0*.WAV\0"       \
-                           L"All Files (*.*)\0*\0\0\0")
-#define FILTER_DYNLIB_FILES (L"Dynamic Library Files (*.dll)\0*.dll\0" \
-                             L"All Files (*.*)\0*\0\0\0")
-
-/* char-based versions of the above, for outlying uses of file selectors. */
-#define FILTER_KEY_FILES_C ("PuTTY Private Key Files (*.ppk)\0*.ppk\0" \
-                            "All Files (*.*)\0*\0\0\0")
-#define FILTER_WAVE_FILES_C ("Wave Files (*.wav)\0*.WAV\0" \
-                             "All Files (*.*)\0*\0\0\0")
-#define FILTER_DYNLIB_FILES_C ("Dynamic Library Files (*.dll)\0*.dll\0" \
-                               "All Files (*.*)\0*\0\0\0")
-
 /*
  * Exports from network.c.
  */
@@ -416,12 +396,21 @@ void init_common_controls(void);       /* also does some DLL-loading */
 /*
  * Exports from utils.
  */
-typedef struct filereq_tag filereq; /* cwd for file requester */
-bool request_file(filereq *state, OPENFILENAME *of, bool preserve, bool save);
-bool request_file_w(filereq *state, OPENFILENAMEW *of,
-                    bool preserve, bool save);
-filereq *filereq_new(void);
-void filereq_free(filereq *state);
+typedef struct filereq_saved_dir filereq_saved_dir;
+filereq_saved_dir *filereq_saved_dir_new(void);
+void filereq_saved_dir_free(filereq_saved_dir *state);
+Filename *request_file(
+    HWND hwnd, const char *title, Filename *initial, bool save,
+    filereq_saved_dir *dir, bool preserve_cwd, FilereqFilter filter);
+struct request_multi_file_return {
+    Filename **filenames;
+    size_t nfilenames;
+};
+struct request_multi_file_return *request_multi_file(
+    HWND hwnd, const char *title, Filename *initial, bool save,
+    filereq_saved_dir *dir, bool preserve_cwd, FilereqFilter filter);
+void request_multi_file_free(struct request_multi_file_return *);
+
 void pgp_fingerprints_msgbox(HWND owner);
 int message_box(HWND owner, LPCTSTR text, LPCTSTR caption, DWORD style,
                 bool utf8, DWORD helpctxid);

+ 5 - 0
source/putty/windows/utils/filename.c

@@ -47,6 +47,11 @@ const char *filename_to_str(const Filename *fn)
     return fn->cpath;                  /* FIXME */
 }
 
+const wchar_t *filename_to_wstr(const Filename *fn)
+{
+    return fn->wpath;
+}
+
 bool filename_equal(const Filename *f1, const Filename *f2)
 {
     /* wpath is primary: two filenames refer to the same file if they