Pārlūkot izejas kodu

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

Source commit: 1675133a3ca89ff21576d3c554b83fcb225d73c3
Martin Prikryl 9 mēneši atpakaļ
vecāks
revīzija
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,
     .getpublic = ssh_ecdhkex_m_getpublic,
     .getkey = ssh_ecdhkex_m_getkey,
     .getkey = ssh_ecdhkex_m_getkey,
     .description = ssh_ecdhkex_description,
     .description = ssh_ecdhkex_description,
+    .packet_naming_ctx = SSH2_PKTCTX_ECDHKEX,
 };
 };
 const ssh_kex ssh_ec_kex_curve25519 = {
 const ssh_kex ssh_ec_kex_curve25519 = {
     .name = "curve25519-sha256",
     .name = "curve25519-sha256",
@@ -1655,6 +1656,7 @@ static const ecdh_keyalg ssh_ecdhkex_w_alg = {
     .getpublic = ssh_ecdhkex_w_getpublic,
     .getpublic = ssh_ecdhkex_w_getpublic,
     .getkey = ssh_ecdhkex_w_getkey,
     .getkey = ssh_ecdhkex_w_getkey,
     .description = ssh_ecdhkex_description,
     .description = ssh_ecdhkex_description,
+    .packet_naming_ctx = SSH2_PKTCTX_ECDHKEX,
 };
 };
 static const struct eckex_extra kex_extra_nistp256 = { ec_p256 };
 static const struct eckex_extra kex_extra_nistp256 = { ec_p256 };
 const ssh_kex ssh_ec_kex_nistp256 = {
 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 "ssh.h"
 #include "mpint.h"
 #include "mpint.h"
 #include "ntru.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
 /* 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)
 static uint16_t invert(uint16_t x, uint16_t q, uint64_t qrecip)
 {
 {
     /* Fermat inversion: compute x^(q-2), since x^(q-1) == 1. */
     /* 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 q_LIVE 4591
 #define w_LIVE 286
 #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;
     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 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)) {
     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;
         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;
     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 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)) {
     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;
         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;
     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),                     \
         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 dh_ctx dh_ctx;
 typedef struct ecdh_key ecdh_key;
 typedef struct ecdh_key ecdh_key;
 typedef struct ecdh_keyalg ecdh_keyalg;
 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 NTRUKeyPair NTRUKeyPair;
 typedef struct NTRUEncodeSchedule NTRUEncodeSchedule;
 typedef struct NTRUEncodeSchedule NTRUEncodeSchedule;
 typedef struct RFC6979 RFC6979;
 typedef struct RFC6979 RFC6979;
 typedef struct RFC6979Result RFC6979Result;
 typedef struct RFC6979Result RFC6979Result;
+typedef struct ShakeXOF ShakeXOF;
 
 
 typedef struct dlgparam dlgparam;
 typedef struct dlgparam dlgparam;
 typedef struct dlgcontrol dlgcontrol;
 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
 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.
 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,
 \b \q{\i{ECDH}}: elliptic curve Diffie-Hellman key exchange,
 with a variety of standard curves and hash algorithms.
 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} Streamlined NTRU Prime
 \IM{Streamlined NTRU Prime} 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} repeat key exchange
 \IM{repeat key exchange} key exchange, repeat
 \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
 This section describes the basics of how to use Plink for
 interactive logins and for automated processes.
 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
 version of Plink you're using, and gives you a brief summary of how to
 use Plink:
 use Plink:
 
 
-\c C:\>plink
+\c C:\>plink --help
 \c Plink: command-line connection utility
 \c Plink: command-line connection utility
 \c Release 0.82
 \c Release 0.82
 \c Usage: plink [options] [user@]host [command]
 \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
 scripts: using \c{-batch}, if something goes wrong at connection
 time, the batch job will fail rather than hang.
 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
 \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
 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
 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.
 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}
 \H{plink-cvs} Using Plink with \i{CVS}
 
 
 To use Plink with CVS, you need to set the environment variable
 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
 \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
 version of PSCP you're using, and gives you a brief summary of how to
 use PSCP:
 use PSCP:
 
 
-\c C:\>pscp
+\c C:\>pscp -h
 \c PuTTY Secure Copy client
 \c PuTTY Secure Copy client
 \c Release 0.82
 \c Release 0.82
 \c Usage: pscp [options] [user@]host:source target
 \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
            that commonly-used method, and hopefully also resistant to a new
            class of attacks.
            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
         -  `ECDH': elliptic curve Diffie-Hellman key exchange, with a
            variety of standard curves and hash algorithms.
            variety of standard curves and hash algorithms.
 
 
@@ -5637,12 +5643,11 @@ Chapter 5: Using PSCP to transfer files securely
 
 
    5.2 PSCP Usage
    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
          PuTTY Secure Copy client
          Release 0.82
          Release 0.82
          Usage: pscp [options] [user@]host:source target
          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
        This section describes the basics of how to use Plink for
        interactive logins and for automated processes.
        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
          Plink: command-line connection utility
          Release 0.82
          Release 0.82
          Usage: plink [options] [user@]host [command]
          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,
        scripts: using `-batch', if something goes wrong at connection time,
        the batch job will fail rather than hang.
        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
 7.2.3.2 `-s': remote command is SSH subsystem
 
 
        If you specify the `-s' option, Plink passes the specified command
        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
        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.
        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
        To use Plink with CVS, you need to set the environment variable
        `CVS_RSH' to point to Plink:
        `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
          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
        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
        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.
        on the `Preferences' dialogue box.
 
 
        Next, select `Command Line' from the WinCVS `Admin' menu, and type a
        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
          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
        cannot make any promises about the behaviour of modified versions
        distributed by other people.
        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);
 wchar_t *dupwcs(const wchar_t *s);
 char *dupcat_fn(const char *s1, ...);
 char *dupcat_fn(const char *s1, ...);
 #define dupcat(...) dupcat_fn(__VA_ARGS__, (const char *)NULL)
 #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 *dupprintf(const char *fmt, ...) PRINTF_LIKE(1, 2);
 char *dupvprintf(const char *fmt, va_list ap);
 char *dupvprintf(const char *fmt, va_list ap);
 void burnstr(char *string);
 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();
     strbuf *sb = strbuf_new_nm();
     const ssh2_macalg *alg = &ssh_hmac_md5;
     const ssh2_macalg *alg = &ssh_hmac_md5;
+    enable_dit(); /* just in case main() forgot */
     mac_simple(alg, password, challenge, strbuf_append(sb, alg->len));
     mac_simple(alg, password, challenge, strbuf_append(sb, alg->len));
     return sb;
     return sb;
 }
 }
@@ -75,6 +76,8 @@ void http_digest_response(BinarySink *bs, ptrlen username, ptrlen password,
     const ssh_hashalg *alg = httphashalgs[hash];
     const ssh_hashalg *alg = httphashalgs[hash];
     size_t hashlen = httphashlengths[hash];
     size_t hashlen = httphashlengths[hash];
 
 
+    enable_dit(); /* just in case main() forgot */
+
     unsigned char ncbuf[4];
     unsigned char ncbuf[4];
     PUT_32BIT_MSB_FIRST(ncbuf, nonce_count);
     PUT_32BIT_MSB_FIRST(ncbuf, nonce_count);
 
 

+ 17 - 0
source/putty/putty.h

@@ -4,6 +4,21 @@
 #include <stddef.h>                    /* for wchar_t */
 #include <stddef.h>                    /* for wchar_t */
 #include <limits.h>                    /* for INT_MAX */
 #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 "defs.h"
 #include "platform.h"
 #include "platform.h"
 #include "network.h"
 #include "network.h"
@@ -388,6 +403,8 @@ enum {
     KEX_RSA,
     KEX_RSA,
     KEX_ECDH,
     KEX_ECDH,
     KEX_NTRU_HYBRID,
     KEX_NTRU_HYBRID,
+    KEX_MLKEM_25519_HYBRID,
+    KEX_MLKEM_NIST_HYBRID,
     KEX_MAX
     KEX_MAX
 };
 };
 
 

+ 2 - 0
source/putty/settings.c

@@ -30,6 +30,8 @@ static const struct keyvalwhere ciphernames[] = {
  * in sync with those. */
  * in sync with those. */
 static const struct keyvalwhere kexnames[] = {
 static const struct keyvalwhere kexnames[] = {
     { "ntru-curve25519",    KEX_NTRU_HYBRID, -1, +1 },
     { "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 },
     { "ecdh",               KEX_ECDH,       -1, +1 },
     /* This name is misleading: it covers both SHA-256 and SHA-1 variants */
     /* This name is misleading: it covers both SHA-256 and SHA-1 variants */
     { "dh-gex-sha1",        KEX_DHGEX,      -1, -1 },
     { "dh-gex-sha1",        KEX_DHGEX,      -1, -1 },

+ 80 - 0
source/putty/ssh.h

@@ -136,6 +136,7 @@ typedef enum {
     SSH2_PKTCTX_DHGROUP,
     SSH2_PKTCTX_DHGROUP,
     SSH2_PKTCTX_DHGEX,
     SSH2_PKTCTX_DHGEX,
     SSH2_PKTCTX_ECDHKEX,
     SSH2_PKTCTX_ECDHKEX,
+    SSH2_PKTCTX_HYBRIDKEX,
     SSH2_PKTCTX_GSSKEX,
     SSH2_PKTCTX_GSSKEX,
     SSH2_PKTCTX_RSAKEX
     SSH2_PKTCTX_RSAKEX
 } Pkt_KCtx;
 } Pkt_KCtx;
@@ -992,6 +993,12 @@ struct ecdh_keyalg {
     void (*getpublic)(ecdh_key *key, BinarySink *bs);
     void (*getpublic)(ecdh_key *key, BinarySink *bs);
     bool (*getkey)(ecdh_key *key, ptrlen remoteKey, BinarySink *bs);
     bool (*getkey)(ecdh_key *key, ptrlen remoteKey, BinarySink *bs);
     char *(*description)(const ssh_kex *kex);
     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)
 static inline ecdh_key *ecdh_key_new(const ssh_kex *kex, bool is_server)
 { return kex->ecdh_vt->new(kex, 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)
 static inline char *ecdh_keyalg_description(const ssh_kex *kex)
 { return kex->ecdh_vt->description(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
  * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5
  * as the mechanism.
  * 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_256;
 extern const ssh_hashalg ssh_sha3_384;
 extern const ssh_hashalg ssh_sha3_384;
 extern const ssh_hashalg ssh_sha3_512;
 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_shake256_114bytes;
 extern const ssh_hashalg ssh_blake2b;
 extern const ssh_hashalg ssh_blake2b;
 extern const ssh_kexes ssh_diffiehellman_group1;
 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_kex ssh_ec_kex_nistp521;
 extern const ssh_kexes ssh_ecdh_kex;
 extern const ssh_kexes ssh_ecdh_kex;
 extern const ssh_kexes ssh_ntru_hybrid_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_dsa;
 extern const ssh_keyalg ssh_rsa;
 extern const ssh_keyalg ssh_rsa;
 extern const ssh_keyalg ssh_rsa_sha256;
 extern const ssh_keyalg ssh_rsa_sha256;
@@ -1234,6 +1294,13 @@ ssh_hash *blake2b_new_general(unsigned hashlen);
 /* Special test function for AES-GCM */
 /* Special test function for AES-GCM */
 void aesgcm_set_prefix_lengths(ssh2_mac *mac, size_t skip, size_t aad);
 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
  * On some systems, you have to detect hardware crypto acceleration by
  * asking the local OS API rather than OS-agnostically asking the CPU
  * 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_sha256_neon_available(void);
 bool platform_sha1_neon_available(void);
 bool platform_sha1_neon_available(void);
 bool platform_sha512_neon_available(void);
 bool platform_sha512_neon_available(void);
+bool platform_dit_available(void);
 
 
 /*
 /*
  * PuTTY version number formatted as an SSH version string.
  * 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_KEXRSA_DONE, 32, SSH2_PKTCTX_RSAKEX)                  \
     K(y, SSH2_MSG_KEX_ECDH_INIT, 30, SSH2_PKTCTX_ECDHKEX)               \
     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_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_REQUEST, 50)                                 \
     X(y, SSH2_MSG_USERAUTH_FAILURE, 51)                                 \
     X(y, SSH2_MSG_USERAUTH_FAILURE, 51)                                 \
     X(y, SSH2_MSG_USERAUTH_SUCCESS, 52)                                 \
     X(y, SSH2_MSG_USERAUTH_SUCCESS, 52)                                 \
@@ -1979,3 +2049,13 @@ enum {
     PLUGIN_NOTYPE = 256, /* packet too short to have a type */
     PLUGIN_NOTYPE = 256, /* packet too short to have a type */
     PLUGIN_EOF = 257 /* EOF from auth plugin */
     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 "putty.h"
 #include "pgssapi.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
 #ifndef NO_GSSAPI
 
 
 #define SSH2_GSS_OIDTYPE 0x06
 #define SSH2_GSS_OIDTYPE 0x06
@@ -49,10 +56,6 @@ struct ssh_gss_library;
  * The free function cleans up the structure, and its associated
  * The free function cleans up the structure, and its associated
  * libraries (if any).
  * libraries (if any).
  */
  */
-struct ssh_gss_liblist {
-    struct ssh_gss_library *libraries;
-    int nlibraries;
-};
 struct ssh_gss_liblist *ssh_gss_setup(Conf *conf);
 struct ssh_gss_liblist *ssh_gss_setup(Conf *conf);
 void ssh_gss_cleanup(struct ssh_gss_liblist *list);
 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);
                      ssh_hash_alg(s->exhash)->text_name);
         sfree(desc);
         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);
         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;
     Ssh *ssh;
 
 
+    enable_dit(); /* just in case main() forgot */
+
     ssh = snew(Ssh);
     ssh = snew(Ssh);
     memset(ssh, 0, sizeof(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++] =
             preferred_kex[n_preferred_kex++] =
                 &ssh_ntru_hybrid_kex;
                 &ssh_ntru_hybrid_kex;
             break;
             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:
           case KEX_WARN:
             /* Flag for later. Don't bother if it's the last in
             /* Flag for later. Don't bother if it's the last in
              * the list. */
              * the list. */
@@ -1166,7 +1174,7 @@ static ScanKexinitsResult ssh2_scan_kexinits(
              * Otherwise, any match failure _is_ a fatal error.
              * Otherwise, any match failure _is_ a fatal error.
              */
              */
             ScanKexinitsResult skr = {
             ScanKexinitsResult skr = {
-                .success = false, .error = SKR_UNKNOWN_ID,
+                .success = false, .error = SKR_NO_AGREEMENT,
                 .kind = kexlist_descr[i], .desc = slists[i],
                 .kind = kexlist_descr[i], .desc = slists[i],
             };
             };
             return skr;
             return skr;

+ 3 - 1
source/putty/sshrand.c

@@ -93,8 +93,10 @@ void random_save_seed(void)
 
 
 void random_ref(void)
 void random_ref(void)
 {
 {
-    if (!random_active++)
+    if (!random_active++) {
+        enable_dit(); /* just in case main() forgot */
         random_create(&ssh_sha256);
         random_create(&ssh_sha256);
+    }
 }
 }
 
 
 void random_setup_custom(const ssh_hashalg *hash)
 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
 #ifdef MINEFIELD
     p = minefield_c_malloc(size);
     p = minefield_c_malloc(size);
 #elif defined ALLOCATION_ALIGNMENT
 #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
 #else
     p = malloc(size);
     p = malloc(size);
 #endif
 #endif

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

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

+ 4 - 4
source/putty/version.h

@@ -1,5 +1,5 @@
 /* Generated by automated build script */
 /* 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;
     char *cpath, *utf8path;
 };
 };
 Filename *filename_from_wstr(const wchar_t *str);
 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);
 FILE *f_open(const Filename *filename, const char *mode, bool isprivate);
 
 
 #ifndef SUPERSEDE_FONTSPEC_FOR_TESTING
 #ifndef SUPERSEDE_FONTSPEC_FOR_TESTING
@@ -295,27 +296,6 @@ void write_aclip(HWND hwnd, int clipboard, char *, int);
  */
  */
 #define sk_getxdmdata(socket, lenp) (NULL)
 #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.
  * Exports from network.c.
  */
  */
@@ -416,12 +396,21 @@ void init_common_controls(void);       /* also does some DLL-loading */
 /*
 /*
  * Exports from utils.
  * 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);
 void pgp_fingerprints_msgbox(HWND owner);
 int message_box(HWND owner, LPCTSTR text, LPCTSTR caption, DWORD style,
 int message_box(HWND owner, LPCTSTR text, LPCTSTR caption, DWORD style,
                 bool utf8, DWORD helpctxid);
                 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 */
     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)
 bool filename_equal(const Filename *f1, const Filename *f2)
 {
 {
     /* wpath is primary: two filenames refer to the same file if they
     /* wpath is primary: two filenames refer to the same file if they